LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
Env.m
1classdef Env < Ensemble
2 % An environment model defined by a collection of network sub-models
3 % coupled with an environment transition rule that selects the active
4 % sub-model.
5 %
6 % Copyright (c) 2012-2026, Imperial College London
7 % All rights reserved.
8
9 properties
10 env;
11 envGraph;
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
18 end
19
20 methods
21 function self = Env(name)
22 % SELF = ENVIRONMENT(MODELS, ENV)
23 self@Ensemble({});
24 self.name = name;
25 self.envGraph = digraph();
26 self.envGraph.Nodes.Model = cell(0);
27 self.envGraph.Nodes.Type = cell(0);
28 end
29
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);
38 if E>1
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.');
41 end
42 end
43 for e=E
44 for h=1:E
45 self.env{e,h} = Disabled.getInstance();
46 self.env{h,e} = Disabled.getInstance();
47 end
48 end
49 self.ensemble{E} = model;
50 end
51
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;
59 else
60 self.resetFun{e,h} = resetFun;
61 end
62 if nargin<6 %~exist('resetEnvRatesFun','var')
63 self.resetEnvRatesFun{e,h} = @(originalDist, QExit, UExit, TExit) originalDist;
64 else
65 self.resetEnvRatesFun{e,h} = resetEnvRatesFun;
66 end
67 end
68
69 function init(self)
70 E = height(self.envGraph.Nodes);
71 T = height(self.envGraph.Edges);
72 Pemb = zeros(E);
73
74 for t=1:T
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};
78 end
79
80 % analyse holding times
81 emmap = cell(E,1);
82 for e=1:E
83 emmap{e} = cell(1,E);
84 end
85 self.holdTime = {};
86 for e=1:E
87 for h=1:E
88 if isa(self.env{e,h},'Disabled')
89 emmap{e}{h} = {0,0}; % multiclass MMAP representation
90 else
91 emmap{e}{h} = self.env{e,h}.getProcess; % multiclass MMAP representation
92 end
93 for j = 1:E
94 if j == h
95 emmap{e}{h}{2+j} = emmap{e}{h}{2};
96 else
97 emmap{e}{h}{2+j} = 0 * emmap{e}{h}{2};
98 end
99 end
100 end
101 self.holdTime{e} = emmap{e}{e};
102 for h=setdiff(1:E,e)
103 self.holdTime{e}{1} = krons(self.holdTime{e}{1},emmap{e}{h}{1});
104 for j = 2:(E+2)
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;
109 end
110 self.holdTime{e} = mmap_normalize(self.holdTime{e});
111 end
112 count_lambda = mmap_count_lambda(self.holdTime{e}); % completiom rates for the different transitions
113 Pemb(e,:) = count_lambda/sum(count_lambda);
114 end
115 self.proc = emmap;
116
117 %
118 lambda = zeros(1,E);
119 A = zeros(E); I=eye(E);
120 for e=1:E
121 lambda(e) = 1/map_mean(self.holdTime{e});
122 for h=1:E
123 A(e,h) = -lambda(e)*(I(e,h)-Pemb(e,h));
124 end
125 end
126
127 if all(lambda>0)
128 penv = ctmc_solve_reducible(A);
129 self.probEnv = penv;
130 self.probOrig = zeros(E);
131 for e = 1:E
132 for h = 1:E
133 self.probOrig(h,e) = penv(h) * lambda(h) * Pemb(h,e);
134 end
135 if penv(e) > 0
136 self.probOrig(:,e) = self.probOrig(:,e) / sum(self.probOrig(:,e));
137 end
138 end
139 else
140 %T = A(lambda>0, lambda>0), % subgenerator
141 %t = A(lambda>0, lambda==0), % exit vector
142 end
143 end
144
145 function env = getEnv(self)
146 env = self.env;
147 end
148
149 function self = setEnv(self, env)
150 self.envGraph = env;
151 end
152
153 function self = setStageName(self, stageId, name)
154 self.stageNames{stageId} = name;
155 end
156
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;
162 else
163 line_error(mfilename,'Stage type must be of type categorical, e.g., categorical("My Semantics").');
164 end
165 end
166
167 function ET = getStageTable(self)
168 E = height(self.envGraph.Nodes);
169 Stage = [];
170 HoldT = {};
171 type = categorical([]);
172 if isempty(self.probEnv)
173 self.init;
174 end
175 for e=1:E
176 Stage(e,1) = e;
177 type(e,1) = self.envGraph.Nodes.Type{e};
178 end
179 Prob = self.probEnv(:);
180 Name = categorical(self.envGraph.Nodes.Name(:));
181 Model = self.ensemble(:);
182 for e=1:E
183 HoldT{e,1} = self.holdTime{e};
184 end
185 Type = type;
186 ET = Table(Stage, Name, Type, Prob, HoldT, Model);
187 end
188
189 function ET = getStageT(self)
190 % GETSTAGET Short alias for getStageTable
191 ET = self.getStageTable();
192 end
193
194 function RT = getReliabilityTable(self)
195 % RT = GETRELIABILITYTABLE()
196 % Compute system-wide reliability metrics (MTTF, MTTR, MTBF, Availability)
197 %
198 % Returns:
199 % RT - Table with columns: Metric, Value, Unit, Description
200 %
201 % Example:
202 % env = Env('ServerEnv');
203 % env.addNodeFailureRepair(model, 'Server', Exp(0.1), Exp(1.0), Exp(0.5));
204 % env.init();
205 % reliabilityTable = env.getReliabilityTable();
206
207 % Step 1: Initialize and validate
208 if isempty(self.probEnv)
209 self.init();
210 end
211
212 E = height(self.envGraph.Nodes);
213 if E == 0
214 line_error(mfilename, 'Environment has no stages. Add stages before computing reliability metrics.');
215 end
216
217 % Step 2: Identify stage types
218 stageNames = self.envGraph.Nodes.Name;
219 upIdx = find(strcmp(stageNames, 'UP'));
220
221 if isempty(upIdx)
222 line_error(mfilename, 'No UP stage found. Use addNodeBreakdown/addNodeRepair to configure breakdown/repair transitions.');
223 end
224
225 % Find all DOWN stages
226 downIdx = find(startsWith(stageNames, 'DOWN_'));
227
228 if isempty(downIdx)
229 line_error(mfilename, 'No DOWN stages found. Use addNodeBreakdown/addNodeRepair to configure breakdown/repair transitions.');
230 end
231
232 % Step 3: Extract breakdown rates (UP -> DOWN_*)
233 breakdownRates = [];
234 for h = downIdx'
235 if ~isa(self.env{upIdx, h}, 'Disabled')
236 lambda_h = 1 / self.env{upIdx, h}.getMean();
237 breakdownRates(end+1) = lambda_h;
238 end
239 end
240
241 if isempty(breakdownRates)
242 line_error(mfilename, 'No breakdown transitions found (UP -> DOWN_*).');
243 end
244
245 % Total failure rate (competing risks)
246 lambda_total = sum(breakdownRates);
247 MTTF = 1 / lambda_total;
248
249 % Step 4: Extract repair rates (DOWN_* -> UP)
250 repairRates = [];
251 downProbs = [];
252
253 for e = downIdx'
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);
258 end
259 end
260
261 if isempty(repairRates)
262 line_error(mfilename, 'No repair transitions found (DOWN_* -> UP).');
263 end
264
265 % Normalize probabilities over DOWN states only
266 totalDownProb = sum(downProbs);
267 if totalDownProb > 0
268 downProbsNorm = downProbs / totalDownProb;
269 % Weighted average repair time
270 MTTR = sum(downProbsNorm ./ repairRates);
271 else
272 % Fallback: simple average if no steady-state probability
273 MTTR = mean(1 ./ repairRates);
274 end
275
276 % Step 5: Compute derived metrics
277 MTBF = MTTF + MTTR;
278
279 % Availability from steady-state probabilities
280 availUp = self.probEnv(upIdx);
281 availDown = sum(self.probEnv(downIdx));
282 Availability = availUp / (availUp + availDown);
283
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'
293 });
294
295 RT = Table(Metric, Value, Unit, Description);
296 end
297
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
301 %
302 % Parameters:
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)
309 %
310 % Example:
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
315 %
316 % env = Env('ServerEnv');
317 % env.addNodeBreakdown(model, 'Server1', Exp(0.1), Exp(0.5));
318
319 if nargin < 6
320 resetFun = @(q) q;
321 else
322 resetFun = varargin{1};
323 end
324
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);
329 end
330
331 % Create DOWN stage with modified service rate for the specified node
332 downModel = baseModel.copy();
333 nodes = downModel.getNodes();
334 nodeIdx = [];
335 for i = 1:length(nodes)
336 if strcmp(nodes{i}.name, nodeName)
337 nodeIdx = i;
338 break;
339 end
340 end
341
342 if isempty(nodeIdx)
343 line_error(mfilename, sprintf('Node "%s" not found in the base model.', nodeName));
344 end
345
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);
350 end
351
352 % Add DOWN stage
353 downStageName = sprintf('DOWN_%s', nodeName);
354 self.addStage(downStageName, 'failed', downModel);
355
356 % Add breakdown transition (UP -> DOWN)
357 self.addTransition('UP', downStageName, breakdownDist, resetFun);
358 end
359
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
363 %
364 % Parameters:
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)
369 %
370 % Example:
371 % env.addNodeRepair('Server1', Exp(1.0));
372
373 if nargin < 4
374 resetFun = @(q) q;
375 else
376 resetFun = varargin{1};
377 end
378
379 downStageName = sprintf('DOWN_%s', nodeName);
380
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));
384 end
385
386 % Add repair transition (DOWN -> UP)
387 self.addTransition(downStageName, 'UP', repairDist, resetFun);
388 end
389
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
393 %
394 % Parameters:
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
402 %
403 % Example:
404 % env = Env('ServerEnv');
405 % env.addNodeFailureRepair(model, 'Server1', Exp(0.1), Exp(1.0), Exp(0.5));
406
407 resetBreakdown = @(q) q;
408 resetRepair = @(q) q;
409
410 if nargin >= 7
411 resetBreakdown = varargin{1};
412 end
413 if nargin >= 8
414 resetRepair = varargin{2};
415 end
416
417 self.addNodeBreakdown(baseModel, nodeName, breakdownDist, downServiceDist, resetBreakdown);
418 self.addNodeRepair(nodeName, repairDist, resetRepair);
419 end
420
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
424 %
425 % Parameters:
426 % nodeName - Name of the node
427 % resetFun - Function to reset queue lengths: resetFun(q) -> q_new
428 % Common policies:
429 % @(q) q - Keep all jobs (default)
430 % @(q) 0*q - Clear all queues
431 % @(q) min(q, N) - Cap queue lengths
432
433 downStageName = sprintf('DOWN_%s', nodeName);
434 e = self.envGraph.findnode('UP');
435 h = self.envGraph.findnode(downStageName);
436
437 if isempty(e) || isempty(h)
438 line_error(mfilename, sprintf('Breakdown transition for node "%s" not found.', nodeName));
439 end
440
441 self.resetFun{e,h} = resetFun;
442 end
443
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
447 %
448 % Parameters:
449 % nodeName - Name of the node
450 % resetFun - Function to reset queue lengths: resetFun(q) -> q_new
451
452 downStageName = sprintf('DOWN_%s', nodeName);
453 e = self.envGraph.findnode(downStageName);
454 h = self.envGraph.findnode('UP');
455
456 if isempty(e) || isempty(h)
457 line_error(mfilename, sprintf('Repair transition for node "%s" not found.', nodeName));
458 end
459
460 self.resetFun{e,h} = resetFun;
461 end
462 end
463
464 methods (Access = protected)
465 % Override copyElement method:
466 function clone = copyElement(self)
467 % CLONE = COPYELEMENT()
468
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);
473 end
474 end
475end