LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
Environment.m
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
4 % sub-model.
5 %
6 % Copyright (c) 2012-2026, Imperial College London
7 % All rights reserved.
8
9 properties
10 env;
11 envGraph;
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 end
20
21 methods
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.
27 if nargin < 2
28 num_stages = 0;
29 end
30 self@Ensemble({});
31 self.name = name;
32 self.num_stages = num_stages;
33 self.envGraph = digraph();
34 self.envGraph.Nodes.Model = cell(0);
35 self.envGraph.Nodes.Type = cell(0);
36 end
37
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);
46 if E>1
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.');
49 end
50 end
51 for e=E
52 for h=1:E
53 self.env{e,h} = Disabled.getInstance();
54 self.env{h,e} = Disabled.getInstance();
55 end
56 end
57 self.ensemble{E} = model;
58 end
59
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;
67 else
68 self.resetFun{e,h} = resetFun;
69 end
70 if nargin<6 %~exist('resetEnvRatesFun','var')
71 self.resetEnvRatesFun{e,h} = @(originalDist, QExit, UExit, TExit) originalDist;
72 else
73 self.resetEnvRatesFun{e,h} = resetEnvRatesFun;
74 end
75 end
76
77 function init(self)
78 E = height(self.envGraph.Nodes);
79 T = height(self.envGraph.Edges);
80 Pemb = zeros(E);
81
82 for t=1:T
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};
86 end
87
88 % analyse holding times
89 emmap = cell(E,1);
90 for e=1:E
91 emmap{e} = cell(1,E);
92 end
93 self.holdTime = {};
94 for e=1:E
95 for h=1:E
96 if isa(self.env{e,h},'Disabled')
97 emmap{e}{h} = {0,0}; % multiclass MMAP representation
98 else
99 emmap{e}{h} = self.env{e,h}.getProcess; % multiclass MMAP representation
100 end
101 for j = 1:E
102 if j == h
103 emmap{e}{h}{2+j} = emmap{e}{h}{2};
104 else
105 emmap{e}{h}{2+j} = 0 * emmap{e}{h}{2};
106 end
107 end
108 end
109 self.holdTime{e} = emmap{e}{e};
110 for h=setdiff(1:E,e)
111 self.holdTime{e}{1} = krons(self.holdTime{e}{1},emmap{e}{h}{1});
112 for j = 2:(E+2)
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;
117 end
118 self.holdTime{e} = mmap_normalize(self.holdTime{e});
119 end
120 count_lambda = mmap_count_lambda(self.holdTime{e}); % completiom rates for the different transitions
121 Pemb(e,:) = count_lambda/sum(count_lambda);
122 end
123 self.proc = emmap;
124
125 %
126 lambda = zeros(1,E);
127 A = zeros(E); I=eye(E);
128 for e=1:E
129 lambda(e) = 1/map_mean(self.holdTime{e});
130 for h=1:E
131 A(e,h) = -lambda(e)*(I(e,h)-Pemb(e,h));
132 end
133 end
134
135 if all(lambda>0)
136 penv = ctmc_solve_reducible(A);
137 self.probEnv = penv;
138 self.probOrig = zeros(E);
139 for e = 1:E
140 for h = 1:E
141 self.probOrig(h,e) = penv(h) * lambda(h) * Pemb(h,e);
142 end
143 if penv(e) > 0
144 self.probOrig(:,e) = self.probOrig(:,e) / sum(self.probOrig(:,e));
145 end
146 end
147 else
148 %T = A(lambda>0, lambda>0), % subgenerator
149 %t = A(lambda>0, lambda==0), % exit vector
150 end
151 end
152
153 function env = getEnv(self)
154 env = self.env;
155 end
156
157 function self = setEnv(self, env)
158 self.envGraph = env;
159 end
160
161 function self = setStageName(self, stageId, name)
162 self.stageNames{stageId} = name;
163 end
164
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;
170 else
171 line_error(mfilename,'Stage type must be of type categorical, e.g., categorical("My Semantics").');
172 end
173 end
174
175 function ET = getStageTable(self)
176 E = height(self.envGraph.Nodes);
177 Stage = [];
178 HoldT = {};
179 type = categorical([]);
180 if isempty(self.probEnv)
181 self.init;
182 end
183 for e=1:E
184 Stage(e,1) = e;
185 type(e,1) = self.envGraph.Nodes.Type{e};
186 end
187 Prob = self.probEnv(:);
188 Name = categorical(self.envGraph.Nodes.Name(:));
189 Model = self.ensemble(:);
190 for e=1:E
191 HoldT{e,1} = self.holdTime{e};
192 end
193 Type = type;
194 ET = Table(Stage, Name, Type, Prob, HoldT, Model);
195 end
196
197 function ET = getStageT(self)
198 % GETSTAGET Short alias for getStageTable
199 ET = self.getStageTable();
200 end
201
202 function RT = getReliabilityTable(self)
203 % RT = GETRELIABILITYTABLE()
204 % Compute system-wide reliability metrics (MTTF, MTTR, MTBF, Availability)
205 %
206 % Returns:
207 % RT - Table with columns: Metric, Value, Unit, Description
208 %
209 % Example:
210 % env = Environment('ServerEnv');
211 % env.addNodeFailureRepair(model, 'Server', Exp(0.1), Exp(1.0), Exp(0.5));
212 % env.init();
213 % reliabilityTable = env.getReliabilityTable();
214
215 % Step 1: Initialize and validate
216 if isempty(self.probEnv)
217 self.init();
218 end
219
220 E = height(self.envGraph.Nodes);
221 if E == 0
222 line_error(mfilename, 'Environment has no stages. Add stages before computing reliability metrics.');
223 end
224
225 % Step 2: Identify stage types
226 stageNames = self.envGraph.Nodes.Name;
227 upIdx = find(strcmp(stageNames, 'UP'));
228
229 if isempty(upIdx)
230 line_error(mfilename, 'No UP stage found. Use addNodeBreakdown/addNodeRepair to configure breakdown/repair transitions.');
231 end
232
233 % Find all DOWN stages
234 downIdx = find(startsWith(stageNames, 'DOWN_'));
235
236 if isempty(downIdx)
237 line_error(mfilename, 'No DOWN stages found. Use addNodeBreakdown/addNodeRepair to configure breakdown/repair transitions.');
238 end
239
240 % Step 3: Extract breakdown rates (UP -> DOWN_*)
241 breakdownRates = [];
242 for h = downIdx'
243 if ~isa(self.env{upIdx, h}, 'Disabled')
244 lambda_h = 1 / self.env{upIdx, h}.getMean();
245 breakdownRates(end+1) = lambda_h;
246 end
247 end
248
249 if isempty(breakdownRates)
250 line_error(mfilename, 'No breakdown transitions found (UP -> DOWN_*).');
251 end
252
253 % Total failure rate (competing risks)
254 lambda_total = sum(breakdownRates);
255 MTTF = 1 / lambda_total;
256
257 % Step 4: Extract repair rates (DOWN_* -> UP)
258 repairRates = [];
259 downProbs = [];
260
261 for e = downIdx'
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);
266 end
267 end
268
269 if isempty(repairRates)
270 line_error(mfilename, 'No repair transitions found (DOWN_* -> UP).');
271 end
272
273 % Normalize probabilities over DOWN states only
274 totalDownProb = sum(downProbs);
275 if totalDownProb > 0
276 downProbsNorm = downProbs / totalDownProb;
277 % Weighted average repair time
278 MTTR = sum(downProbsNorm ./ repairRates);
279 else
280 % Fallback: simple average if no steady-state probability
281 MTTR = mean(1 ./ repairRates);
282 end
283
284 % Step 5: Compute derived metrics
285 MTBF = MTTF + MTTR;
286
287 % Availability from steady-state probabilities
288 availUp = self.probEnv(upIdx);
289 availDown = sum(self.probEnv(downIdx));
290 Availability = availUp / (availUp + availDown);
291
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'
301 });
302
303 RT = Table(Metric, Value, Unit, Description);
304 end
305
306 function RT = relT(self)
307 % RELT Short alias for getReliabilityTable
308 RT = self.getReliabilityTable();
309 end
310
311 function RT = getRelT(self)
312 % GETRELT Short alias for getReliabilityTable
313 RT = self.getReliabilityTable();
314 end
315
316 function RT = relTable(self)
317 % RELTABLE Short alias for getReliabilityTable
318 RT = self.getReliabilityTable();
319 end
320
321 function RT = getRelTable(self)
322 % GETRELTABLE Short alias for getReliabilityTable
323 RT = self.getReliabilityTable();
324 end
325
326 function printStageTable(self)
327 % PRINTSTAGETABLE Print a formatted table showing all stages, their properties, and transitions
328 %
329 % Displays stage names, types, associated networks, and transition rates.
330 %
331 % Example:
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();
337
338 E = height(self.envGraph.Nodes);
339 fprintf('Stage Table:\n');
340 fprintf('============\n');
341
342 for e = 1:E
343 stageName = self.envGraph.Nodes.Name{e};
344 stageType = self.envGraph.Nodes.Type{e};
345 model = self.envGraph.Nodes.Model{e};
346
347 fprintf('Stage %d: %s (Type: %s)\n', e, stageName, stageType);
348 if ~isempty(model)
349 fprintf(' - Network: %s\n', model.getName());
350 fprintf(' - Nodes: %d\n', model.getNumberOfNodes());
351 fprintf(' - Classes: %d\n', model.getNumberOfClasses());
352 end
353 end
354
355 % Print transitions
356 T = height(self.envGraph.Edges);
357 if T > 0
358 fprintf('\nTransitions:\n');
359 for t = 1:T
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);
367 end
368 end
369 end
370 end
371
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
375 %
376 % Parameters:
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)
383 %
384 % Example:
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
389 %
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));
394
395 % Extract node name if a Node object is passed
396 if isa(nodeOrName, 'Node')
397 nodeName = nodeOrName.name;
398 else
399 nodeName = nodeOrName;
400 end
401
402 if nargin < 6
403 resetFun = @(q) q;
404 else
405 resetFun = varargin{1};
406 end
407
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);
412 end
413
414 % Create DOWN stage with modified service rate for the specified node
415 downModel = baseModel.copy();
416 nodes = downModel.getNodes();
417 nodeIdx = [];
418 for i = 1:length(nodes)
419 if strcmp(nodes{i}.name, nodeName)
420 nodeIdx = i;
421 break;
422 end
423 end
424
425 if isempty(nodeIdx)
426 line_error(mfilename, sprintf('Node "%s" not found in the base model.', nodeName));
427 end
428
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);
433 end
434
435 % Add DOWN stage
436 downStageName = sprintf('DOWN_%s', nodeName);
437 self.addStage(downStageName, 'failed', downModel);
438
439 % Add breakdown transition (UP -> DOWN)
440 self.addTransition('UP', downStageName, breakdownDist, resetFun);
441 end
442
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
446 %
447 % Parameters:
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)
452 %
453 % Example:
454 % env.addNodeRepair('Server1', Exp(1.0));
455 % % Or using node object:
456 % env.addNodeRepair(queue, Exp(1.0));
457
458 % Extract node name if a Node object is passed
459 if isa(nodeOrName, 'Node')
460 nodeName = nodeOrName.name;
461 else
462 nodeName = nodeOrName;
463 end
464
465 if nargin < 4
466 resetFun = @(q) q;
467 else
468 resetFun = varargin{1};
469 end
470
471 downStageName = sprintf('DOWN_%s', nodeName);
472
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));
476 end
477
478 % Add repair transition (DOWN -> UP)
479 self.addTransition(downStageName, 'UP', repairDist, resetFun);
480 end
481
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
485 %
486 % Parameters:
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
494 %
495 % Example:
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));
500
501 % Extract node name if a Node object is passed
502 if isa(nodeOrName, 'Node')
503 nodeName = nodeOrName.name;
504 else
505 nodeName = nodeOrName;
506 end
507
508 resetBreakdown = @(q) q;
509 resetRepair = @(q) q;
510
511 if nargin >= 7
512 resetBreakdown = varargin{1};
513 end
514 if nargin >= 8
515 resetRepair = varargin{2};
516 end
517
518 self.addNodeBreakdown(baseModel, nodeName, breakdownDist, downServiceDist, resetBreakdown);
519 self.addNodeRepair(nodeName, repairDist, resetRepair);
520 end
521 end
522end