1classdef Environment < Ensemble
2 % An environment model defined by a collection of network sub-models
3 % coupled with an environment transition rule that selects the active
6 % Copyright (c) 2012-2026, Imperial College London
12 num_stages; % Expected number of stages (optional,
for API consistency)
13 proc; % Markovian representation of each stage transition
14 holdTime; % holding times
15 probEnv; % steady-stage probability of the environment
16 probOrig; % probability that a request originated from phase
17 resetFun; % function implementing the reset policy
for queue lengths
18 resetEnvRatesFun; % function implementing the reset policy
for environment rates
22 function self = Environment(name, num_stages)
23 % SELF = ENVIRONMENT(NAME, NUM_STAGES)
24 % NAME - Name of the environment
25 % NUM_STAGES - (Optional) Expected number of stages. If provided, used
for validation only.
26 % Stages can still be added/removed dynamically.
32 self.num_stages = num_stages;
33 self.envGraph = digraph();
34 self.envGraph.Nodes.Model = cell(0);
35 self.envGraph.Nodes.Type = cell(0);
38 function name = addStage(self, name, type, model)
39 wcfg = warning; % store warning configuration
40 warning(
'off',
'MATLAB:table:RowsAddedExistingVars');
41 self.envGraph = self.envGraph.addnode(name);
42 warning(wcfg); % restore warning configuration
43 self.envGraph.Nodes.Model{end} = model;
44 self.envGraph.Nodes.Type{end} = type;
45 E = height(self.envGraph.Nodes);
47 if self.envGraph.Nodes.Model{1}.getNumberOfStatefulNodes ~= model.getNumberOfStatefulNodes
48 line_error(mfilename,
'Unsupported feature. Random environment stages must map to networks with identical number of stateful nodes.');
53 self.env{e,h} = Disabled.getInstance();
54 self.env{h,e} = Disabled.getInstance();
57 self.ensemble{E} = model;
60 function self = addTransition(self, fromName, toName, distrib, resetFun, resetEnvRatesFun)
61 self.envGraph = self.envGraph.addedge(fromName, toName);
62 e = self.envGraph.findnode(fromName);
63 h = self.envGraph.findnode(toName);
64 self.env{e,h} = distrib;
65 if nargin<5 %~exist(
'resetFun',
'var')
66 self.resetFun{e,h} = @(q) q;
68 self.resetFun{e,h} = resetFun;
70 if nargin<6 %~exist(
'resetEnvRatesFun',
'var')
71 self.resetEnvRatesFun{e,h} = @(originalDist, QExit, UExit, TExit) originalDist;
73 self.resetEnvRatesFun{e,h} = resetEnvRatesFun;
78 E = height(self.envGraph.Nodes);
79 T = height(self.envGraph.Edges);
83 e = self.envGraph.findnode(self.envGraph.Edges.EndNodes{t,1});
84 h = self.envGraph.findnode(self.envGraph.Edges.EndNodes{t,2});
85 self.envGraph.Edges.Distribution{t} = self.env{e,h};
88 % analyse holding times
96 if isa(self.env{e,h},
'Disabled')
97 emmap{e}{h} = {0,0}; % multiclass
MMAP representation
99 emmap{e}{h} = self.env{e,h}.getProcess; % multiclass
MMAP representation
103 emmap{e}{h}{2+j} = emmap{e}{h}{2};
105 emmap{e}{h}{2+j} = 0 * emmap{e}{h}{2};
109 self.holdTime{e} = emmap{e}{e};
111 self.holdTime{e}{1} = krons(self.holdTime{e}{1},emmap{e}{h}{1});
113 self.holdTime{e}{j} = krons(self.holdTime{e}{j},emmap{e}{h}{j});
114 completion_rates = self.holdTime{e}{j}*ones(length(self.holdTime{e}{j}),1);
115 self.holdTime{e}{j} = 0*self.holdTime{e}{j};
116 self.holdTime{e}{j}(:,1) = completion_rates;
118 self.holdTime{e} = mmap_normalize(self.holdTime{e});
120 count_lambda = mmap_count_lambda(self.holdTime{e}); % completiom rates
for the different transitions
121 Pemb(e,:) = count_lambda/sum(count_lambda);
127 A = zeros(E); I=eye(E);
129 lambda(e) = 1/map_mean(self.holdTime{e});
131 A(e,h) = -lambda(e)*(I(e,h)-Pemb(e,h));
136 penv = ctmc_solve_reducible(A);
138 self.probOrig = zeros(E);
141 self.probOrig(h,e) = penv(h) * lambda(h) * Pemb(h,e);
144 self.probOrig(:,e) = self.probOrig(:,e) / sum(self.probOrig(:,e));
148 %T = A(lambda>0, lambda>0), % subgenerator
149 %t = A(lambda>0, lambda==0), % exit vector
153 function env = getEnv(self)
157 function self = setEnv(self, env)
161 function self = setStageName(self, stageId, name)
162 self.stageNames{stageId} = name;
165 function self = setStageType(self, stageId, stageCategory)
166 if ischar(stageCategory)
167 self.stageTypes(stageId) = categorical(stageCategory);
168 elseif iscategorical(stageCategory)
169 self.stageTypes(stageId) = stageCategory;
171 line_error(mfilename,
'Stage type must be of type categorical, e.g., categorical("My Semantics").');
175 function ET = getStageTable(self)
176 E = height(self.envGraph.Nodes);
179 type = categorical([]);
180 if isempty(self.probEnv)
185 type(e,1) = self.envGraph.Nodes.Type{e};
187 Prob = self.probEnv(:);
188 Name = categorical(self.envGraph.Nodes.Name(:));
189 Model = self.ensemble(:);
191 HoldT{e,1} = self.holdTime{e};
194 ET = Table(Stage, Name, Type, Prob, HoldT, Model);
197 function ET = getStageT(self)
198 % GETSTAGET Short alias
for getStageTable
199 ET = self.getStageTable();
202 function
RT = getReliabilityTable(self)
203 %
RT = GETRELIABILITYTABLE()
204 % Compute system-wide reliability metrics (MTTF, MTTR, MTBF, Availability)
207 %
RT - Table with columns: Metric, Value, Unit, Description
210 % env = Environment(
'ServerEnv');
211 % env.addNodeFailureRepair(model,
'Server', Exp(0.1), Exp(1.0), Exp(0.5));
213 % reliabilityTable = env.getReliabilityTable();
215 % Step 1: Initialize and validate
216 if isempty(self.probEnv)
220 E = height(self.envGraph.Nodes);
222 line_error(mfilename, 'Environment has no stages. Add stages before computing reliability metrics.');
225 % Step 2: Identify stage types
226 stageNames = self.envGraph.Nodes.Name;
227 upIdx = find(strcmp(stageNames, 'UP'));
230 line_error(mfilename, 'No UP stage found. Use addNodeBreakdown/addNodeRepair to configure breakdown/repair transitions.');
233 % Find all DOWN stages
234 downIdx = find(startsWith(stageNames, 'DOWN_'));
237 line_error(mfilename, 'No DOWN stages found. Use addNodeBreakdown/addNodeRepair to configure breakdown/repair transitions.');
240 % Step 3: Extract breakdown rates (UP -> DOWN_*)
243 if ~isa(self.env{upIdx, h},
'Disabled')
244 lambda_h = 1 / self.env{upIdx, h}.getMean();
245 breakdownRates(end+1) = lambda_h;
249 if isempty(breakdownRates)
250 line_error(mfilename,
'No breakdown transitions found (UP -> DOWN_*).');
253 % Total failure rate (competing risks)
254 lambda_total = sum(breakdownRates);
255 MTTF = 1 / lambda_total;
257 % Step 4: Extract repair rates (DOWN_* -> UP)
262 if ~isa(self.env{e, upIdx}, 'Disabled
')
263 mu_e = 1 / self.env{e, upIdx}.getMean();
264 repairRates(end+1) = mu_e;
265 downProbs(end+1) = self.probEnv(e);
269 if isempty(repairRates)
270 line_error(mfilename, 'No repair transitions found (DOWN_* -> UP).
');
273 % Normalize probabilities over DOWN states only
274 totalDownProb = sum(downProbs);
276 downProbsNorm = downProbs / totalDownProb;
277 % Weighted average repair time
278 MTTR = sum(downProbsNorm ./ repairRates);
280 % Fallback: simple average if no steady-state probability
281 MTTR = mean(1 ./ repairRates);
284 % Step 5: Compute derived metrics
287 % Availability from steady-state probabilities
288 availUp = self.probEnv(upIdx);
289 availDown = sum(self.probEnv(downIdx));
290 Availability = availUp / (availUp + availDown);
292 % Step 6: Create output table
293 Metric = categorical({'MTTF
'; 'MTTR
'; 'MTBF
'; 'Availability
'});
294 Value = [MTTF; MTTR; MTBF; Availability];
295 Unit = categorical({'time units
'; 'time units
'; 'time units
'; 'probability
'});
296 Description = categorical({
297 'Mean time to failure (UP -> DOWN)
';
298 'Mean time to repair (DOWN -> UP)
';
299 'Mean time between failures (MTTF + MTTR)
';
300 'Steady-state probability of UP state
'
303 RT = Table(Metric, Value, Unit, Description);
306 function RT = relT(self)
307 % RELT Short alias for getReliabilityTable
308 RT = self.getReliabilityTable();
311 function RT = getRelT(self)
312 % GETRELT Short alias for getReliabilityTable
313 RT = self.getReliabilityTable();
316 function RT = relTable(self)
317 % RELTABLE Short alias for getReliabilityTable
318 RT = self.getReliabilityTable();
321 function RT = getRelTable(self)
322 % GETRELTABLE Short alias for getReliabilityTable
323 RT = self.getReliabilityTable();
326 function printStageTable(self)
327 % PRINTSTAGETABLE Print a formatted table showing all stages, their properties, and transitions
329 % Displays stage names, types, associated networks, and transition rates.
332 % env = Environment('MyEnv
');
333 % env.addStage('UP
', 'operational
', model1);
334 % env.addStage('DOWN
', 'failed
', model2);
335 % env.addTransition('UP
', 'DOWN
', Exp(0.1));
336 % env.printStageTable();
338 E = height(self.envGraph.Nodes);
339 fprintf('Stage Table:\n
');
340 fprintf('============\n
');
343 stageName = self.envGraph.Nodes.Name{e};
344 stageType = self.envGraph.Nodes.Type{e};
345 model = self.envGraph.Nodes.Model{e};
347 fprintf('Stage %d: %s (Type: %s)\n
', e, stageName, stageType);
349 fprintf(' - Network: %s\n
', model.getName());
350 fprintf(' - Nodes: %d\n
', model.getNumberOfNodes());
351 fprintf(' - Classes: %d\n
', model.getNumberOfClasses());
356 T = height(self.envGraph.Edges);
358 fprintf('\nTransitions:\n
');
360 fromName = self.envGraph.Edges.EndNodes{t, 1};
361 toName = self.envGraph.Edges.EndNodes{t, 2};
362 e = self.envGraph.findnode(fromName);
363 h = self.envGraph.findnode(toName);
364 if ~isa(self.env{e, h}, 'Disabled
')
365 rate = 1 / self.env{e, h}.getMean();
366 fprintf(' %s -> %s: rate = %.4f\n
', fromName, toName, rate);
372 function self = addNodeBreakdown(self, baseModel, nodeOrName, breakdownDist, downServiceDist, varargin)
373 % SELF = ADDNODEBREAKDOWN(BASEMODEL, NODEORNAME, BREAKDOWNDIST, DOWNSERVICEDIST, RESETFUN)
374 % Adds UP and DOWN stages for a node that can break down and repair
377 % baseModel - The base network model with normal (UP) service rates
378 % nodeOrName - Node object or name of the node that can break down
379 % breakdownDist - Distribution for time until breakdown (UP->DOWN transition)
380 % downServiceDist - Service distribution when the node is down
381 % resetFun - (Optional) Function to reset queue lengths on breakdown
382 % Default: @(q) q (no reset)
385 % model = Network('MyNetwork
');
386 % queue = Queue(model, 'Server1
', SchedStrategy.FCFS);
387 % class = ClosedClass(model, 'Jobs
', 10, queue, 0);
388 % queue.setService(class, Exp(2)); % UP service rate
390 % env = Environment('ServerEnv
');
391 % env.addNodeBreakdown(model, 'Server1
', Exp(0.1), Exp(0.5));
392 % % Or using node object:
393 % env.addNodeBreakdown(model, queue, Exp(0.1), Exp(0.5));
395 % Extract node name if a Node object is passed
396 if isa(nodeOrName, 'Node
')
397 nodeName = nodeOrName.name;
399 nodeName = nodeOrName;
405 resetFun = varargin{1};
408 % Create UP stage (if this is the first call)
409 if height(self.envGraph.Nodes) == 0
410 upModel = baseModel.copy();
411 self.addStage('UP
', 'operational
', upModel);
414 % Create DOWN stage with modified service rate for the specified node
415 downModel = baseModel.copy();
416 nodes = downModel.getNodes();
418 for i = 1:length(nodes)
419 if strcmp(nodes{i}.name, nodeName)
426 line_error(mfilename, sprintf('Node
"%s" not found in the base model.
', nodeName));
429 % Update service distribution for the down node
430 classes = downModel.getClasses();
431 for c = 1:length(classes)
432 nodes{nodeIdx}.setService(classes{c}, downServiceDist);
436 downStageName = sprintf('DOWN_%s
', nodeName);
437 self.addStage(downStageName, 'failed
', downModel);
439 % Add breakdown transition (UP -> DOWN)
440 self.addTransition('UP
', downStageName, breakdownDist, resetFun);
443 function self = addNodeRepair(self, nodeOrName, repairDist, varargin)
444 % SELF = ADDNODEREPAIR(NODEORNAME, REPAIRDIST, RESETFUN)
445 % Adds repair transition from DOWN to UP stage for a previously added breakdown
448 % nodeOrName - Node object or name of the node that can be repaired
449 % repairDist - Distribution for repair time (DOWN->UP transition)
450 % resetFun - (Optional) Function to reset queue lengths on repair
451 % Default: @(q) q (no reset)
454 % env.addNodeRepair('Server1
', Exp(1.0));
455 % % Or using node object:
456 % env.addNodeRepair(queue, Exp(1.0));
458 % Extract node name if a Node object is passed
459 if isa(nodeOrName, 'Node
')
460 nodeName = nodeOrName.name;
462 nodeName = nodeOrName;
468 resetFun = varargin{1};
471 downStageName = sprintf('DOWN_%s
', nodeName);
473 % Verify DOWN stage exists
474 if isempty(self.envGraph.findnode(downStageName))
475 line_error(mfilename, sprintf('DOWN stage for node
"%s" not found. Call addNodeBreakdown first.
', nodeName));
478 % Add repair transition (DOWN -> UP)
479 self.addTransition(downStageName, 'UP
', repairDist, resetFun);
482 function self = addNodeFailureRepair(self, baseModel, nodeOrName, breakdownDist, repairDist, downServiceDist, varargin)
483 % SELF = ADDNODEFAILUREREPAIR(BASEMODEL, NODEORNAME, BREAKDOWNDIST, REPAIRDIST, DOWNSERVICEDIST, RESETBREAKDOWN, RESETREPAIR)
484 % Convenience method to add both breakdown and repair for a node
487 % baseModel - The base network model with normal (UP) service rates
488 % nodeOrName - Node object or name of the node that can break down and repair
489 % breakdownDist - Distribution for time until breakdown
490 % repairDist - Distribution for repair time
491 % downServiceDist - Service distribution when the node is down
492 % resetBreakdown - (Optional) Reset function for breakdown transition
493 % resetRepair - (Optional) Reset function for repair transition
496 % env = Environment('ServerEnv
');
497 % env.addNodeFailureRepair(model, 'Server1
', Exp(0.1), Exp(1.0), Exp(0.5));
498 % % Or using node object:
499 % env.addNodeFailureRepair(model, queue, Exp(0.1), Exp(1.0), Exp(0.5));
501 % Extract node name if a Node object is passed
502 if isa(nodeOrName, 'Node
')
503 nodeName = nodeOrName.name;
505 nodeName = nodeOrName;
508 resetBreakdown = @(q) q;
509 resetRepair = @(q) q;
512 resetBreakdown = varargin{1};
515 resetRepair = varargin{2};
518 self.addNodeBreakdown(baseModel, nodeName, breakdownDist, downServiceDist, resetBreakdown);
519 self.addNodeRepair(nodeName, repairDist, resetRepair);