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
19 resetStateFun; % function implementing the reset policy
for the full state-probability vector (SolverENV statevec analyzer)
23 function self = Environment(name, num_stages)
24 % SELF = ENVIRONMENT(NAME, NUM_STAGES)
25 % NAME - Name of the environment
26 % NUM_STAGES - (Optional) Expected number of stages. If provided, used
for validation only.
27 % Stages can still be added/removed dynamically.
33 self.num_stages = num_stages;
34 self.envGraph = digraph();
35 self.envGraph.Nodes.Model = cell(0);
36 self.envGraph.Nodes.Type = cell(0);
39 function name = addStage(self, name, type, model)
40 wcfg = warning; % store warning configuration
41 warning(
'off',
'MATLAB:table:RowsAddedExistingVars');
42 self.envGraph = self.envGraph.addnode(name);
43 warning(wcfg); % restore warning configuration
44 self.envGraph.Nodes.Model{end} = model;
45 self.envGraph.Nodes.Type{end} = type;
46 E = height(self.envGraph.Nodes);
48 if self.envGraph.Nodes.Model{1}.getNumberOfStatefulNodes ~= model.getNumberOfStatefulNodes
49 line_error(mfilename,
'Unsupported feature. Random environment stages must map to networks with identical number of stateful nodes.');
54 self.env{e,h} = Disabled.getInstance();
55 self.env{h,e} = Disabled.getInstance();
58 self.ensemble{E} = model;
61 function self = addTransition(self, fromName, toName, distrib, resetFun, resetEnvRatesFun, resetStateFun)
62 self.envGraph = self.envGraph.addedge(fromName, toName);
63 e = self.envGraph.findnode(fromName);
64 h = self.envGraph.findnode(toName);
65 self.env{e,h} = distrib;
66 if nargin<5 %~exist(
'resetFun',
'var')
67 self.resetFun{e,h} = @(q) q;
69 self.resetFun{e,h} = resetFun;
71 if nargin<6 %~exist(
'resetEnvRatesFun',
'var')
72 self.resetEnvRatesFun{e,h} = @(originalDist, QExit, UExit, TExit) originalDist;
74 self.resetEnvRatesFun{e,h} = resetEnvRatesFun;
76 if nargin<7 %~exist(
'resetStateFun',
'var')
77 self.resetStateFun{e,h} = @(pi) pi; % identity: carry the exit distribution unchanged
79 self.resetStateFun{e,h} = resetStateFun;
84 E = height(self.envGraph.Nodes);
85 T = height(self.envGraph.Edges);
89 e = self.envGraph.findnode(self.envGraph.Edges.EndNodes{t,1});
90 h = self.envGraph.findnode(self.envGraph.Edges.EndNodes{t,2});
91 self.envGraph.Edges.Distribution{t} = self.env{e,h};
94 % analyse holding times
102 if isa(self.env{e,h},
'Disabled')
103 emmap{e}{h} = {0,0}; % multiclass
MMAP representation
105 emmap{e}{h} = self.env{e,h}.getProcess; % multiclass
MMAP representation
109 emmap{e}{h}{2+j} = emmap{e}{h}{2};
111 emmap{e}{h}{2+j} = 0 * emmap{e}{h}{2};
115 self.holdTime{e} = emmap{e}{e};
117 self.holdTime{e}{1} = krons(self.holdTime{e}{1},emmap{e}{h}{1});
119 self.holdTime{e}{j} = krons(self.holdTime{e}{j},emmap{e}{h}{j});
120 completion_rates = self.holdTime{e}{j}*ones(length(self.holdTime{e}{j}),1);
121 self.holdTime{e}{j} = 0*self.holdTime{e}{j};
122 self.holdTime{e}{j}(:,1) = completion_rates;
124 self.holdTime{e} = mmap_normalize(self.holdTime{e});
126 count_lambda = mmap_count_lambda(self.holdTime{e}); % completiom rates
for the different transitions
127 Pemb(e,:) = count_lambda/sum(count_lambda);
133 A = zeros(E); I=eye(E);
135 lambda(e) = 1/map_mean(self.holdTime{e});
137 A(e,h) = -lambda(e)*(I(e,h)-Pemb(e,h));
142 penv = ctmc_solve_reducible(A);
144 self.probOrig = zeros(E);
147 self.probOrig(h,e) = penv(h) * lambda(h) * Pemb(h,e);
150 self.probOrig(:,e) = self.probOrig(:,e) / sum(self.probOrig(:,e));
154 %T = A(lambda>0, lambda>0), % subgenerator
155 %t = A(lambda>0, lambda==0), % exit vector
159 function env = getEnv(self)
163 function self = setEnv(self, env)
167 function self = setStageName(self, stageId, name)
168 self.stageNames{stageId} = name;
171 function self = setStageType(self, stageId, stageCategory)
172 if ischar(stageCategory)
173 self.stageTypes(stageId) = categorical(stageCategory);
174 elseif iscategorical(stageCategory)
175 self.stageTypes(stageId) = stageCategory;
177 line_error(mfilename,
'Stage type must be of type categorical, e.g., categorical("My Semantics").');
181 function ET = getStageTable(self)
182 E = height(self.envGraph.Nodes);
185 type = categorical([]);
186 if isempty(self.probEnv)
191 type(e,1) = self.envGraph.Nodes.Type{e};
193 Prob = self.probEnv(:);
194 Name = categorical(self.envGraph.Nodes.Name(:));
195 Model = self.ensemble(:);
197 HoldT{e,1} = self.holdTime{e};
200 ET = Table(Stage, Name, Type, Prob, HoldT, Model);
203 function ET = getStageT(self)
204 % GETSTAGET Short alias
for getStageTable
205 ET = self.getStageTable();
208 function
RT = getReliabilityTable(self)
209 %
RT = GETRELIABILITYTABLE()
210 % Compute system-wide reliability metrics (MTTF, MTTR, MTBF, Availability)
213 %
RT - Table with columns: Metric, Value, Unit, Description
216 % env = Environment(
'ServerEnv');
217 % env.addNodeFailureRepair(model,
'Server', Exp(0.1), Exp(1.0), Exp(0.5));
219 % reliabilityTable = env.getReliabilityTable();
221 % Step 1: Initialize and validate
222 if isempty(self.probEnv)
226 E = height(self.envGraph.Nodes);
228 line_error(mfilename, 'Environment has no stages. Add stages before computing reliability metrics.');
231 % Step 2: Identify stage types
232 stageNames = self.envGraph.Nodes.Name;
233 upIdx = find(strcmp(stageNames, 'UP'));
236 line_error(mfilename, 'No UP stage found. Use addNodeBreakdown/addNodeRepair to configure breakdown/repair transitions.');
239 % Find all DOWN stages
240 downIdx = find(startsWith(stageNames, 'DOWN_'));
243 line_error(mfilename, 'No DOWN stages found. Use addNodeBreakdown/addNodeRepair to configure breakdown/repair transitions.');
246 % Step 3: Extract breakdown rates (UP -> DOWN_*)
249 if ~isa(self.env{upIdx, h},
'Disabled')
250 lambda_h = 1 / self.env{upIdx, h}.getMean();
251 breakdownRates(end+1) = lambda_h;
255 if isempty(breakdownRates)
256 line_error(mfilename,
'No breakdown transitions found (UP -> DOWN_*).');
259 % Total failure rate (competing risks)
260 lambda_total = sum(breakdownRates);
261 MTTF = 1 / lambda_total;
263 % Step 4: Extract repair rates (DOWN_* -> UP)
268 if ~isa(self.env{e, upIdx}, 'Disabled
')
269 mu_e = 1 / self.env{e, upIdx}.getMean();
270 repairRates(end+1) = mu_e;
271 downProbs(end+1) = self.probEnv(e);
275 if isempty(repairRates)
276 line_error(mfilename, 'No repair transitions found (DOWN_* -> UP).
');
279 % Normalize probabilities over DOWN states only
280 totalDownProb = sum(downProbs);
282 downProbsNorm = downProbs / totalDownProb;
283 % Weighted average repair time
284 MTTR = sum(downProbsNorm ./ repairRates);
286 % Fallback: simple average if no steady-state probability
287 MTTR = mean(1 ./ repairRates);
290 % Step 5: Compute derived metrics
293 % Availability from steady-state probabilities
294 availUp = self.probEnv(upIdx);
295 availDown = sum(self.probEnv(downIdx));
296 Availability = availUp / (availUp + availDown);
298 % Step 6: Create output table
299 Metric = categorical({'MTTF
'; 'MTTR
'; 'MTBF
'; 'Availability
'});
300 Value = [MTTF; MTTR; MTBF; Availability];
301 Unit = categorical({'time units
'; 'time units
'; 'time units
'; 'probability
'});
302 Description = categorical({
303 'Mean time to failure (UP -> DOWN)
';
304 'Mean time to repair (DOWN -> UP)
';
305 'Mean time between failures (MTTF + MTTR)
';
306 'Steady-state probability of UP state
'
309 RT = Table(Metric, Value, Unit, Description);
312 function RT = relT(self)
313 % RELT Short alias for getReliabilityTable
314 RT = self.getReliabilityTable();
317 function RT = getRelT(self)
318 % GETRELT Short alias for getReliabilityTable
319 RT = self.getReliabilityTable();
322 function RT = relTable(self)
323 % RELTABLE Short alias for getReliabilityTable
324 RT = self.getReliabilityTable();
327 function RT = getRelTable(self)
328 % GETRELTABLE Short alias for getReliabilityTable
329 RT = self.getReliabilityTable();
332 function printStageTable(self)
333 % PRINTSTAGETABLE Print a formatted table showing all stages, their properties, and transitions
335 % Displays stage names, types, associated networks, and transition rates.
338 % env = Environment('MyEnv
');
339 % env.addStage('UP
', 'operational
', model1);
340 % env.addStage('DOWN
', 'failed
', model2);
341 % env.addTransition('UP
', 'DOWN
', Exp(0.1));
342 % env.printStageTable();
344 E = height(self.envGraph.Nodes);
345 fprintf('Stage Table:\n
');
346 fprintf('============\n
');
349 stageName = self.envGraph.Nodes.Name{e};
350 stageType = self.envGraph.Nodes.Type{e};
351 model = self.envGraph.Nodes.Model{e};
353 fprintf('Stage %d: %s (Type: %s)\n
', e, stageName, stageType);
355 fprintf(' - Network: %s\n
', model.getName());
356 fprintf(' - Nodes: %d\n
', model.getNumberOfNodes());
357 fprintf(' - Classes: %d\n
', model.getNumberOfClasses());
362 T = height(self.envGraph.Edges);
364 fprintf('\nTransitions:\n
');
366 fromName = self.envGraph.Edges.EndNodes{t, 1};
367 toName = self.envGraph.Edges.EndNodes{t, 2};
368 e = self.envGraph.findnode(fromName);
369 h = self.envGraph.findnode(toName);
370 if ~isa(self.env{e, h}, 'Disabled
')
371 rate = 1 / self.env{e, h}.getMean();
372 fprintf(' %s -> %s: rate = %.4f\n
', fromName, toName, rate);
378 function self = addNodeBreakdown(self, baseModel, nodeOrName, breakdownDist, downServiceDist, varargin)
379 % SELF = ADDNODEBREAKDOWN(BASEMODEL, NODEORNAME, BREAKDOWNDIST, DOWNSERVICEDIST, RESETFUN)
380 % Adds UP and DOWN stages for a node that can break down and repair
383 % baseModel - The base network model with normal (UP) service rates
384 % nodeOrName - Node object or name of the node that can break down
385 % breakdownDist - Distribution for time until breakdown (UP->DOWN transition)
386 % downServiceDist - Service distribution when the node is down
387 % resetFun - (Optional) Function to reset queue lengths on breakdown
388 % Default: @(q) q (no reset)
391 % model = Network('MyNetwork
');
392 % queue = Queue(model, 'Server1
', SchedStrategy.FCFS);
393 % class = ClosedClass(model, 'Jobs
', 10, queue, 0);
394 % queue.setService(class, Exp(2)); % UP service rate
396 % env = Environment('ServerEnv
');
397 % env.addNodeBreakdown(model, 'Server1
', Exp(0.1), Exp(0.5));
398 % % Or using node object:
399 % env.addNodeBreakdown(model, queue, Exp(0.1), Exp(0.5));
401 % Extract node name if a Node object is passed
402 if isa(nodeOrName, 'Node
')
403 nodeName = nodeOrName.name;
405 nodeName = nodeOrName;
411 resetFun = varargin{1};
414 % Create UP stage (if this is the first call)
415 if height(self.envGraph.Nodes) == 0
416 upModel = baseModel.copy();
417 self.addStage('UP
', 'operational
', upModel);
420 % Create DOWN stage with modified service rate for the specified node
421 downModel = baseModel.copy();
422 nodes = downModel.getNodes();
424 for i = 1:length(nodes)
425 if strcmp(nodes{i}.name, nodeName)
432 line_error(mfilename, sprintf('Node
"%s" not found in the base model.
', nodeName));
435 % Update service distribution for the down node
436 classes = downModel.getClasses();
437 for c = 1:length(classes)
438 nodes{nodeIdx}.setService(classes{c}, downServiceDist);
442 downStageName = sprintf('DOWN_%s
', nodeName);
443 self.addStage(downStageName, 'failed
', downModel);
445 % Add breakdown transition (UP -> DOWN)
446 self.addTransition('UP
', downStageName, breakdownDist, resetFun);
449 function self = addNodeRepair(self, nodeOrName, repairDist, varargin)
450 % SELF = ADDNODEREPAIR(NODEORNAME, REPAIRDIST, RESETFUN)
451 % Adds repair transition from DOWN to UP stage for a previously added breakdown
454 % nodeOrName - Node object or name of the node that can be repaired
455 % repairDist - Distribution for repair time (DOWN->UP transition)
456 % resetFun - (Optional) Function to reset queue lengths on repair
457 % Default: @(q) q (no reset)
460 % env.addNodeRepair('Server1
', Exp(1.0));
461 % % Or using node object:
462 % env.addNodeRepair(queue, Exp(1.0));
464 % Extract node name if a Node object is passed
465 if isa(nodeOrName, 'Node
')
466 nodeName = nodeOrName.name;
468 nodeName = nodeOrName;
474 resetFun = varargin{1};
477 downStageName = sprintf('DOWN_%s
', nodeName);
479 % Verify DOWN stage exists
480 if isempty(self.envGraph.findnode(downStageName))
481 line_error(mfilename, sprintf('DOWN stage for node
"%s" not found. Call addNodeBreakdown first.
', nodeName));
484 % Add repair transition (DOWN -> UP)
485 self.addTransition(downStageName, 'UP
', repairDist, resetFun);
488 function self = addNodeFailureRepair(self, baseModel, nodeOrName, breakdownDist, repairDist, downServiceDist, varargin)
489 % SELF = ADDNODEFAILUREREPAIR(BASEMODEL, NODEORNAME, BREAKDOWNDIST, REPAIRDIST, DOWNSERVICEDIST, RESETBREAKDOWN, RESETREPAIR)
490 % Convenience method to add both breakdown and repair for a node
493 % baseModel - The base network model with normal (UP) service rates
494 % nodeOrName - Node object or name of the node that can break down and repair
495 % breakdownDist - Distribution for time until breakdown
496 % repairDist - Distribution for repair time
497 % downServiceDist - Service distribution when the node is down
498 % resetBreakdown - (Optional) Reset function for breakdown transition
499 % resetRepair - (Optional) Reset function for repair transition
502 % env = Environment('ServerEnv
');
503 % env.addNodeFailureRepair(model, 'Server1
', Exp(0.1), Exp(1.0), Exp(0.5));
504 % % Or using node object:
505 % env.addNodeFailureRepair(model, queue, Exp(0.1), Exp(1.0), Exp(0.5));
507 % Extract node name if a Node object is passed
508 if isa(nodeOrName, 'Node
')
509 nodeName = nodeOrName.name;
511 nodeName = nodeOrName;
514 resetBreakdown = @(q) q;
515 resetRepair = @(q) q;
518 resetBreakdown = varargin{1};
521 resetRepair = varargin{2};
524 self.addNodeBreakdown(baseModel, nodeName, breakdownDist, downServiceDist, resetBreakdown);
525 self.addNodeRepair(nodeName, repairDist, resetRepair);
528 function self = setBreakdownResetPolicy(self, nodeOrName, resetFun)
529 % SELF = SETBREAKDOWNRESETPOLICY(NODEORNAME, RESETFUN)
530 % Update the reset policy for breakdown transitions (UP -> DOWN) of a node
533 % nodeOrName - Node object or name of the node
534 % resetFun - Function to reset queue lengths on breakdown
535 % Example: @(q) 0*q to clear queues, @(q) q to keep jobs
538 % env.setBreakdownResetPolicy('Server1
', @(q) 0*q);
539 % % Or using node object:
540 % env.setBreakdownResetPolicy(queue, @(q) 0*q);
542 % Extract node name if a Node object is passed
543 if isa(nodeOrName, 'Node
')
544 nodeName = nodeOrName.name;
546 nodeName = nodeOrName;
549 downStageName = sprintf('DOWN_%s
', nodeName);
551 % Find UP and DOWN stage indices
552 upIdx = self.envGraph.findnode('UP
');
553 downIdx = self.envGraph.findnode(downStageName);
555 if isempty(upIdx) || upIdx == 0
556 line_error(mfilename, 'UP stage not found. Call addNodeBreakdown first.
');
558 if isempty(downIdx) || downIdx == 0
559 line_error(mfilename, sprintf('DOWN stage for node
"%s" not found. Call addNodeBreakdown first.
', nodeName));
562 % Update the reset function for the breakdown transition (UP -> DOWN)
563 self.resetFun{upIdx, downIdx} = resetFun;
566 function self = setRepairResetPolicy(self, nodeOrName, resetFun)
567 % SELF = SETREPAIRRESETPOLICY(NODEORNAME, RESETFUN)
568 % Update the reset policy for repair transitions (DOWN -> UP) of a node
571 % nodeOrName - Node object or name of the node
572 % resetFun - Function to reset queue lengths on repair
573 % Example: @(q) q to keep jobs, @(q) 0*q to clear queues
576 % env.setRepairResetPolicy('Server1
', @(q) q);
577 % % Or using node object:
578 % env.setRepairResetPolicy(queue, @(q) q);
580 % Extract node name if a Node object is passed
581 if isa(nodeOrName, 'Node
')
582 nodeName = nodeOrName.name;
584 nodeName = nodeOrName;
587 downStageName = sprintf('DOWN_%s
', nodeName);
589 % Find UP and DOWN stage indices
590 upIdx = self.envGraph.findnode('UP
');
591 downIdx = self.envGraph.findnode(downStageName);
593 if isempty(upIdx) || upIdx == 0
594 line_error(mfilename, 'UP stage not found. Call addNodeBreakdown first.
');
596 if isempty(downIdx) || downIdx == 0
597 line_error(mfilename, sprintf('DOWN stage for node
"%s" not found. Call addNodeBreakdown first.
', nodeName));
600 % Update the reset function for the repair transition (DOWN -> UP)
601 self.resetFun{downIdx, upIdx} = resetFun;