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 proc; % Markovian representation of each stage transition
13 holdTime; % holding times
14 probEnv; % steady-stage probability of the environment
15 probOrig; % probability that a request originated from phase
16 resetFun; % function implementing the reset policy
for queue lengths
17 resetEnvRatesFun; % function implementing the reset policy
for environment rates
21 function self = Env(name)
22 % SELF = ENVIRONMENT(MODELS, ENV)
25 self.envGraph = digraph();
26 self.envGraph.Nodes.Model = cell(0);
27 self.envGraph.Nodes.Type = cell(0);
30 function name = addStage(self, name, type, model)
31 wcfg = warning; % store warning configuration
32 warning(
'off',
'MATLAB:table:RowsAddedExistingVars');
33 self.envGraph = self.envGraph.addnode(name);
34 warning(wcfg); % restore warning configuration
35 self.envGraph.Nodes.Model{end} = model;
36 self.envGraph.Nodes.Type{end} = type;
37 E = height(self.envGraph.Nodes);
39 if self.envGraph.Nodes.Model{1}.getNumberOfStatefulNodes ~= model.getNumberOfStatefulNodes
40 line_error(mfilename,
'Unsupported feature. Random environment stages must map to networks with identical number of stateful nodes.');
45 self.env{e,h} = Disabled.getInstance();
46 self.env{h,e} = Disabled.getInstance();
49 self.ensemble{E} = model;
52 function self = addTransition(self, fromName, toName, distrib, resetFun, resetEnvRatesFun)
53 self.envGraph = self.envGraph.addedge(fromName, toName);
54 e = self.envGraph.findnode(fromName);
55 h = self.envGraph.findnode(toName);
56 self.env{e,h} = distrib;
57 if nargin<5 %~exist(
'resetFun',
'var')
58 self.resetFun{e,h} = @(q) q;
60 self.resetFun{e,h} = resetFun;
62 if nargin<6 %~exist(
'resetEnvRatesFun',
'var')
63 self.resetEnvRatesFun{e,h} = @(originalDist, QExit, UExit, TExit) originalDist;
65 self.resetEnvRatesFun{e,h} = resetEnvRatesFun;
70 E = height(self.envGraph.Nodes);
71 T = height(self.envGraph.Edges);
75 e = self.envGraph.findnode(self.envGraph.Edges.EndNodes{t,1});
76 h = self.envGraph.findnode(self.envGraph.Edges.EndNodes{t,2});
77 self.envGraph.Edges.Distribution{t} = self.env{e,h};
80 % analyse holding times
88 if isa(self.env{e,h},
'Disabled')
89 emmap{e}{h} = {0,0}; % multiclass
MMAP representation
91 emmap{e}{h} = self.env{e,h}.getProcess; % multiclass
MMAP representation
95 emmap{e}{h}{2+j} = emmap{e}{h}{2};
97 emmap{e}{h}{2+j} = 0 * emmap{e}{h}{2};
101 self.holdTime{e} = emmap{e}{e};
103 self.holdTime{e}{1} = krons(self.holdTime{e}{1},emmap{e}{h}{1});
105 self.holdTime{e}{j} = krons(self.holdTime{e}{j},emmap{e}{h}{j});
106 completion_rates = self.holdTime{e}{j}*ones(length(self.holdTime{e}{j}),1);
107 self.holdTime{e}{j} = 0*self.holdTime{e}{j};
108 self.holdTime{e}{j}(:,1) = completion_rates;
110 self.holdTime{e} = mmap_normalize(self.holdTime{e});
112 count_lambda = mmap_count_lambda(self.holdTime{e}); % completiom rates
for the different transitions
113 Pemb(e,:) = count_lambda/sum(count_lambda);
119 A = zeros(E); I=eye(E);
121 lambda(e) = 1/map_mean(self.holdTime{e});
123 A(e,h) = -lambda(e)*(I(e,h)-Pemb(e,h));
128 penv = ctmc_solve_reducible(A);
130 self.probOrig = zeros(E);
133 self.probOrig(h,e) = penv(h) * lambda(h) * Pemb(h,e);
136 self.probOrig(:,e) = self.probOrig(:,e) / sum(self.probOrig(:,e));
140 %T = A(lambda>0, lambda>0), % subgenerator
141 %t = A(lambda>0, lambda==0), % exit vector
145 function env = getEnv(self)
149 function self = setEnv(self, env)
153 function self = setStageName(self, stageId, name)
154 self.stageNames{stageId} = name;
157 function self = setStageType(self, stageId, stageCategory)
158 if ischar(stageCategory)
159 self.stageTypes(stageId) = categorical(stageCategory);
160 elseif iscategorical(stageCategory)
161 self.stageTypes(stageId) = stageCategory;
163 line_error(mfilename,
'Stage type must be of type categorical, e.g., categorical("My Semantics").');
167 function ET = getStageTable(self)
168 E = height(self.envGraph.Nodes);
171 type = categorical([]);
172 if isempty(self.probEnv)
177 type(e,1) = self.envGraph.Nodes.Type{e};
179 Prob = self.probEnv(:);
180 Name = categorical(self.envGraph.Nodes.Name(:));
181 Model = self.ensemble(:);
183 HoldT{e,1} = self.holdTime{e};
186 ET = Table(Stage, Name, Type, Prob, HoldT, Model);
189 function ET = getStageT(self)
190 % GETSTAGET Short alias
for getStageTable
191 ET = self.getStageTable();
194 function
RT = getReliabilityTable(self)
195 %
RT = GETRELIABILITYTABLE()
196 % Compute system-wide reliability metrics (MTTF, MTTR, MTBF, Availability)
199 %
RT - Table with columns: Metric, Value, Unit, Description
202 % env = Env(
'ServerEnv');
203 % env.addNodeFailureRepair(model,
'Server', Exp(0.1), Exp(1.0), Exp(0.5));
205 % reliabilityTable = env.getReliabilityTable();
207 % Step 1: Initialize and validate
208 if isempty(self.probEnv)
212 E = height(self.envGraph.Nodes);
214 line_error(mfilename, 'Environment has no stages. Add stages before computing reliability metrics.');
217 % Step 2: Identify stage types
218 stageNames = self.envGraph.Nodes.Name;
219 upIdx = find(strcmp(stageNames, 'UP'));
222 line_error(mfilename, 'No UP stage found. Use addNodeBreakdown/addNodeRepair to configure breakdown/repair transitions.');
225 % Find all DOWN stages
226 downIdx = find(startsWith(stageNames, 'DOWN_'));
229 line_error(mfilename, 'No DOWN stages found. Use addNodeBreakdown/addNodeRepair to configure breakdown/repair transitions.');
232 % Step 3: Extract breakdown rates (UP -> DOWN_*)
235 if ~isa(self.env{upIdx, h},
'Disabled')
236 lambda_h = 1 / self.env{upIdx, h}.getMean();
237 breakdownRates(end+1) = lambda_h;
241 if isempty(breakdownRates)
242 line_error(mfilename,
'No breakdown transitions found (UP -> DOWN_*).');
245 % Total failure rate (competing risks)
246 lambda_total = sum(breakdownRates);
247 MTTF = 1 / lambda_total;
249 % Step 4: Extract repair rates (DOWN_* -> UP)
254 if ~isa(self.env{e, upIdx}, 'Disabled
')
255 mu_e = 1 / self.env{e, upIdx}.getMean();
256 repairRates(end+1) = mu_e;
257 downProbs(end+1) = self.probEnv(e);
261 if isempty(repairRates)
262 line_error(mfilename, 'No repair transitions found (DOWN_* -> UP).
');
265 % Normalize probabilities over DOWN states only
266 totalDownProb = sum(downProbs);
268 downProbsNorm = downProbs / totalDownProb;
269 % Weighted average repair time
270 MTTR = sum(downProbsNorm ./ repairRates);
272 % Fallback: simple average if no steady-state probability
273 MTTR = mean(1 ./ repairRates);
276 % Step 5: Compute derived metrics
279 % Availability from steady-state probabilities
280 availUp = self.probEnv(upIdx);
281 availDown = sum(self.probEnv(downIdx));
282 Availability = availUp / (availUp + availDown);
284 % Step 6: Create output table
285 Metric = categorical({'MTTF
'; 'MTTR
'; 'MTBF
'; 'Availability
'});
286 Value = [MTTF; MTTR; MTBF; Availability];
287 Unit = categorical({'time units
'; 'time units
'; 'time units
'; 'probability
'});
288 Description = categorical({
289 'Mean time to failure (UP -> DOWN)
';
290 'Mean time to repair (DOWN -> UP)
';
291 'Mean time between failures (MTTF + MTTR)
';
292 'Steady-state probability of UP state
'
295 RT = Table(Metric, Value, Unit, Description);
298 function self = addNodeBreakdown(self, baseModel, nodeName, breakdownDist, downServiceDist, varargin)
299 % SELF = ADDNODEBREAKDOWN(BASEMODEL, NODENAME, BREAKDOWNDIST, DOWNSERVICEDIST, RESETFUN)
300 % Adds UP and DOWN stages for a node that can break down and repair
303 % baseModel - The base network model with normal (UP) service rates
304 % nodeName - Name of the node that can break down
305 % breakdownDist - Distribution for time until breakdown (UP->DOWN transition)
306 % downServiceDist - Service distribution when the node is down
307 % resetFun - (Optional) Function to reset queue lengths on breakdown
308 % Default: @(q) q (no reset)
311 % model = Network('MyNetwork
');
312 % queue = Queue(model, 'Server1
', SchedStrategy.FCFS);
313 % class = ClosedClass(model, 'Jobs
', 10, queue, 0);
314 % queue.setService(class, Exp(2)); % UP service rate
316 % env = Env('ServerEnv
');
317 % env.addNodeBreakdown(model, 'Server1
', Exp(0.1), Exp(0.5));
322 resetFun = varargin{1};
325 % Create UP stage (if this is the first call)
326 if height(self.envGraph.Nodes) == 0
327 upModel = baseModel.copy();
328 self.addStage('UP
', 'operational
', upModel);
331 % Create DOWN stage with modified service rate for the specified node
332 downModel = baseModel.copy();
333 nodes = downModel.getNodes();
335 for i = 1:length(nodes)
336 if strcmp(nodes{i}.name, nodeName)
343 line_error(mfilename, sprintf('Node
"%s" not found in the base model.
', nodeName));
346 % Update service distribution for the down node
347 classes = downModel.getClasses();
348 for c = 1:length(classes)
349 nodes{nodeIdx}.setService(classes{c}, downServiceDist);
353 downStageName = sprintf('DOWN_%s
', nodeName);
354 self.addStage(downStageName, 'failed
', downModel);
356 % Add breakdown transition (UP -> DOWN)
357 self.addTransition('UP
', downStageName, breakdownDist, resetFun);
360 function self = addNodeRepair(self, nodeName, repairDist, varargin)
361 % SELF = ADDNODEREPAIR(NODENAME, REPAIRDIST, RESETFUN)
362 % Adds repair transition from DOWN to UP stage for a previously added breakdown
365 % nodeName - Name of the node that can be repaired
366 % repairDist - Distribution for repair time (DOWN->UP transition)
367 % resetFun - (Optional) Function to reset queue lengths on repair
368 % Default: @(q) q (no reset)
371 % env.addNodeRepair('Server1
', Exp(1.0));
376 resetFun = varargin{1};
379 downStageName = sprintf('DOWN_%s
', nodeName);
381 % Verify DOWN stage exists
382 if isempty(self.envGraph.findnode(downStageName))
383 line_error(mfilename, sprintf('DOWN stage
for node
"%s" not found. Call addNodeBreakdown first.
', nodeName));
386 % Add repair transition (DOWN -> UP)
387 self.addTransition(downStageName, 'UP
', repairDist, resetFun);
390 function self = addNodeFailureRepair(self, baseModel, nodeName, breakdownDist, repairDist, downServiceDist, varargin)
391 % SELF = ADDNODEFAILUREREPAIR(BASEMODEL, NODENAME, BREAKDOWNDIST, REPAIRDIST, DOWNSERVICEDIST, RESETBREAKDOWN, RESETREPAIR)
392 % Convenience method to add both breakdown and repair for a node
395 % baseModel - The base network model with normal (UP) service rates
396 % nodeName - Name of the node that can break down and repair
397 % breakdownDist - Distribution for time until breakdown
398 % repairDist - Distribution for repair time
399 % downServiceDist - Service distribution when the node is down
400 % resetBreakdown - (Optional) Reset function for breakdown transition
401 % resetRepair - (Optional) Reset function for repair transition
404 % env = Env('ServerEnv
');
405 % env.addNodeFailureRepair(model, 'Server1
', Exp(0.1), Exp(1.0), Exp(0.5));
407 resetBreakdown = @(q) q;
408 resetRepair = @(q) q;
411 resetBreakdown = varargin{1};
414 resetRepair = varargin{2};
417 self.addNodeBreakdown(baseModel, nodeName, breakdownDist, downServiceDist, resetBreakdown);
418 self.addNodeRepair(nodeName, repairDist, resetRepair);
421 function self = setBreakdownResetPolicy(self, nodeName, resetFun)
422 % SELF = SETBREAKDOWNRESETPOLICY(NODENAME, RESETFUN)
423 % Sets the reset policy for queue lengths when a node breaks down
426 % nodeName - Name of the node
427 % resetFun - Function to reset queue lengths: resetFun(q) -> q_new
429 % @(q) q - Keep all jobs (default)
430 % @(q) 0*q - Clear all queues
431 % @(q) min(q, N) - Cap queue lengths
433 downStageName = sprintf('DOWN_%s
', nodeName);
434 e = self.envGraph.findnode('UP
');
435 h = self.envGraph.findnode(downStageName);
437 if isempty(e) || isempty(h)
438 line_error(mfilename, sprintf('Breakdown transition
for node
"%s" not found.
', nodeName));
441 self.resetFun{e,h} = resetFun;
444 function self = setRepairResetPolicy(self, nodeName, resetFun)
445 % SELF = SETREPAIRRESETPOLICY(NODENAME, RESETFUN)
446 % Sets the reset policy for queue lengths when a node is repaired
449 % nodeName - Name of the node
450 % resetFun - Function to reset queue lengths: resetFun(q) -> q_new
452 downStageName = sprintf('DOWN_%s
', nodeName);
453 e = self.envGraph.findnode(downStageName);
454 h = self.envGraph.findnode('UP
');
456 if isempty(e) || isempty(h)
457 line_error(mfilename, sprintf('Repair transition
for node
"%s" not found.
', nodeName));
460 self.resetFun{e,h} = resetFun;
464 methods (Access = protected)
465 % Override copyElement method:
466 function clone = copyElement(self)
467 % CLONE = COPYELEMENT()
469 % Make a shallow copy of all properties
470 clone = copyElement@Ensemble(self);
471 % Make a deep copy of each ensemble object
472 clone.env = copy(self.envGraph);