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 resetStateFun; % function implementing the reset policy for the full state-probability vector (SolverENV statevec analyzer)
20 end
21
22 methods
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.
28 if nargin < 2
29 num_stages = 0;
30 end
31 self@Ensemble({});
32 self.name = name;
33 self.num_stages = num_stages;
34 self.envGraph = digraph();
35 self.envGraph.Nodes.Model = cell(0);
36 self.envGraph.Nodes.Type = cell(0);
37 end
38
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);
47 if E>1
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.');
50 end
51 end
52 for e=E
53 for h=1:E
54 self.env{e,h} = Disabled.getInstance();
55 self.env{h,e} = Disabled.getInstance();
56 end
57 end
58 self.ensemble{E} = model;
59 end
60
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;
68 else
69 self.resetFun{e,h} = resetFun;
70 end
71 if nargin<6 %~exist('resetEnvRatesFun','var')
72 self.resetEnvRatesFun{e,h} = @(originalDist, QExit, UExit, TExit) originalDist;
73 else
74 self.resetEnvRatesFun{e,h} = resetEnvRatesFun;
75 end
76 if nargin<7 %~exist('resetStateFun','var')
77 self.resetStateFun{e,h} = @(pi) pi; % identity: carry the exit distribution unchanged
78 else
79 self.resetStateFun{e,h} = resetStateFun;
80 end
81 end
82
83 function init(self)
84 E = height(self.envGraph.Nodes);
85 T = height(self.envGraph.Edges);
86 Pemb = zeros(E);
87
88 for t=1:T
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};
92 end
93
94 % analyse holding times
95 emmap = cell(E,1);
96 for e=1:E
97 emmap{e} = cell(1,E);
98 end
99 self.holdTime = {};
100 for e=1:E
101 for h=1:E
102 if isa(self.env{e,h},'Disabled')
103 emmap{e}{h} = {0,0}; % multiclass MMAP representation
104 else
105 emmap{e}{h} = self.env{e,h}.getProcess; % multiclass MMAP representation
106 end
107 for j = 1:E
108 if j == h
109 emmap{e}{h}{2+j} = emmap{e}{h}{2};
110 else
111 emmap{e}{h}{2+j} = 0 * emmap{e}{h}{2};
112 end
113 end
114 end
115 self.holdTime{e} = emmap{e}{e};
116 for h=setdiff(1:E,e)
117 self.holdTime{e}{1} = krons(self.holdTime{e}{1},emmap{e}{h}{1});
118 for j = 2:(E+2)
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;
123 end
124 self.holdTime{e} = mmap_normalize(self.holdTime{e});
125 end
126 count_lambda = mmap_count_lambda(self.holdTime{e}); % completiom rates for the different transitions
127 Pemb(e,:) = count_lambda/sum(count_lambda);
128 end
129 self.proc = emmap;
130
131 %
132 lambda = zeros(1,E);
133 A = zeros(E); I=eye(E);
134 for e=1:E
135 lambda(e) = 1/map_mean(self.holdTime{e});
136 for h=1:E
137 A(e,h) = -lambda(e)*(I(e,h)-Pemb(e,h));
138 end
139 end
140
141 if all(lambda>0)
142 penv = ctmc_solve_reducible(A);
143 self.probEnv = penv;
144 self.probOrig = zeros(E);
145 for e = 1:E
146 for h = 1:E
147 self.probOrig(h,e) = penv(h) * lambda(h) * Pemb(h,e);
148 end
149 if penv(e) > 0
150 self.probOrig(:,e) = self.probOrig(:,e) / sum(self.probOrig(:,e));
151 end
152 end
153 else
154 %T = A(lambda>0, lambda>0), % subgenerator
155 %t = A(lambda>0, lambda==0), % exit vector
156 end
157 end
158
159 function env = getEnv(self)
160 env = self.env;
161 end
162
163 function self = setEnv(self, env)
164 self.envGraph = env;
165 end
166
167 function self = setStageName(self, stageId, name)
168 self.stageNames{stageId} = name;
169 end
170
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;
176 else
177 line_error(mfilename,'Stage type must be of type categorical, e.g., categorical("My Semantics").');
178 end
179 end
180
181 function ET = getStageTable(self)
182 E = height(self.envGraph.Nodes);
183 Stage = [];
184 HoldT = {};
185 type = categorical([]);
186 if isempty(self.probEnv)
187 self.init;
188 end
189 for e=1:E
190 Stage(e,1) = e;
191 type(e,1) = self.envGraph.Nodes.Type{e};
192 end
193 Prob = self.probEnv(:);
194 Name = categorical(self.envGraph.Nodes.Name(:));
195 Model = self.ensemble(:);
196 for e=1:E
197 HoldT{e,1} = self.holdTime{e};
198 end
199 Type = type;
200 ET = Table(Stage, Name, Type, Prob, HoldT, Model);
201 end
202
203 function ET = getStageT(self)
204 % GETSTAGET Short alias for getStageTable
205 ET = self.getStageTable();
206 end
207
208 function RT = getReliabilityTable(self)
209 % RT = GETRELIABILITYTABLE()
210 % Compute system-wide reliability metrics (MTTF, MTTR, MTBF, Availability)
211 %
212 % Returns:
213 % RT - Table with columns: Metric, Value, Unit, Description
214 %
215 % Example:
216 % env = Environment('ServerEnv');
217 % env.addNodeFailureRepair(model, 'Server', Exp(0.1), Exp(1.0), Exp(0.5));
218 % env.init();
219 % reliabilityTable = env.getReliabilityTable();
220
221 % Step 1: Initialize and validate
222 if isempty(self.probEnv)
223 self.init();
224 end
225
226 E = height(self.envGraph.Nodes);
227 if E == 0
228 line_error(mfilename, 'Environment has no stages. Add stages before computing reliability metrics.');
229 end
230
231 % Step 2: Identify stage types
232 stageNames = self.envGraph.Nodes.Name;
233 upIdx = find(strcmp(stageNames, 'UP'));
234
235 if isempty(upIdx)
236 line_error(mfilename, 'No UP stage found. Use addNodeBreakdown/addNodeRepair to configure breakdown/repair transitions.');
237 end
238
239 % Find all DOWN stages
240 downIdx = find(startsWith(stageNames, 'DOWN_'));
241
242 if isempty(downIdx)
243 line_error(mfilename, 'No DOWN stages found. Use addNodeBreakdown/addNodeRepair to configure breakdown/repair transitions.');
244 end
245
246 % Step 3: Extract breakdown rates (UP -> DOWN_*)
247 breakdownRates = [];
248 for h = downIdx'
249 if ~isa(self.env{upIdx, h}, 'Disabled')
250 lambda_h = 1 / self.env{upIdx, h}.getMean();
251 breakdownRates(end+1) = lambda_h;
252 end
253 end
254
255 if isempty(breakdownRates)
256 line_error(mfilename, 'No breakdown transitions found (UP -> DOWN_*).');
257 end
258
259 % Total failure rate (competing risks)
260 lambda_total = sum(breakdownRates);
261 MTTF = 1 / lambda_total;
262
263 % Step 4: Extract repair rates (DOWN_* -> UP)
264 repairRates = [];
265 downProbs = [];
266
267 for e = downIdx'
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);
272 end
273 end
274
275 if isempty(repairRates)
276 line_error(mfilename, 'No repair transitions found (DOWN_* -> UP).');
277 end
278
279 % Normalize probabilities over DOWN states only
280 totalDownProb = sum(downProbs);
281 if totalDownProb > 0
282 downProbsNorm = downProbs / totalDownProb;
283 % Weighted average repair time
284 MTTR = sum(downProbsNorm ./ repairRates);
285 else
286 % Fallback: simple average if no steady-state probability
287 MTTR = mean(1 ./ repairRates);
288 end
289
290 % Step 5: Compute derived metrics
291 MTBF = MTTF + MTTR;
292
293 % Availability from steady-state probabilities
294 availUp = self.probEnv(upIdx);
295 availDown = sum(self.probEnv(downIdx));
296 Availability = availUp / (availUp + availDown);
297
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'
307 });
308
309 RT = Table(Metric, Value, Unit, Description);
310 end
311
312 function RT = relT(self)
313 % RELT Short alias for getReliabilityTable
314 RT = self.getReliabilityTable();
315 end
316
317 function RT = getRelT(self)
318 % GETRELT Short alias for getReliabilityTable
319 RT = self.getReliabilityTable();
320 end
321
322 function RT = relTable(self)
323 % RELTABLE Short alias for getReliabilityTable
324 RT = self.getReliabilityTable();
325 end
326
327 function RT = getRelTable(self)
328 % GETRELTABLE Short alias for getReliabilityTable
329 RT = self.getReliabilityTable();
330 end
331
332 function printStageTable(self)
333 % PRINTSTAGETABLE Print a formatted table showing all stages, their properties, and transitions
334 %
335 % Displays stage names, types, associated networks, and transition rates.
336 %
337 % Example:
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();
343
344 E = height(self.envGraph.Nodes);
345 fprintf('Stage Table:\n');
346 fprintf('============\n');
347
348 for e = 1:E
349 stageName = self.envGraph.Nodes.Name{e};
350 stageType = self.envGraph.Nodes.Type{e};
351 model = self.envGraph.Nodes.Model{e};
352
353 fprintf('Stage %d: %s (Type: %s)\n', e, stageName, stageType);
354 if ~isempty(model)
355 fprintf(' - Network: %s\n', model.getName());
356 fprintf(' - Nodes: %d\n', model.getNumberOfNodes());
357 fprintf(' - Classes: %d\n', model.getNumberOfClasses());
358 end
359 end
360
361 % Print transitions
362 T = height(self.envGraph.Edges);
363 if T > 0
364 fprintf('\nTransitions:\n');
365 for t = 1:T
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);
373 end
374 end
375 end
376 end
377
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
381 %
382 % Parameters:
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)
389 %
390 % Example:
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
395 %
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));
400
401 % Extract node name if a Node object is passed
402 if isa(nodeOrName, 'Node')
403 nodeName = nodeOrName.name;
404 else
405 nodeName = nodeOrName;
406 end
407
408 if nargin < 6
409 resetFun = @(q) q;
410 else
411 resetFun = varargin{1};
412 end
413
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);
418 end
419
420 % Create DOWN stage with modified service rate for the specified node
421 downModel = baseModel.copy();
422 nodes = downModel.getNodes();
423 nodeIdx = [];
424 for i = 1:length(nodes)
425 if strcmp(nodes{i}.name, nodeName)
426 nodeIdx = i;
427 break;
428 end
429 end
430
431 if isempty(nodeIdx)
432 line_error(mfilename, sprintf('Node "%s" not found in the base model.', nodeName));
433 end
434
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);
439 end
440
441 % Add DOWN stage
442 downStageName = sprintf('DOWN_%s', nodeName);
443 self.addStage(downStageName, 'failed', downModel);
444
445 % Add breakdown transition (UP -> DOWN)
446 self.addTransition('UP', downStageName, breakdownDist, resetFun);
447 end
448
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
452 %
453 % Parameters:
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)
458 %
459 % Example:
460 % env.addNodeRepair('Server1', Exp(1.0));
461 % % Or using node object:
462 % env.addNodeRepair(queue, Exp(1.0));
463
464 % Extract node name if a Node object is passed
465 if isa(nodeOrName, 'Node')
466 nodeName = nodeOrName.name;
467 else
468 nodeName = nodeOrName;
469 end
470
471 if nargin < 4
472 resetFun = @(q) q;
473 else
474 resetFun = varargin{1};
475 end
476
477 downStageName = sprintf('DOWN_%s', nodeName);
478
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));
482 end
483
484 % Add repair transition (DOWN -> UP)
485 self.addTransition(downStageName, 'UP', repairDist, resetFun);
486 end
487
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
491 %
492 % Parameters:
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
500 %
501 % Example:
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));
506
507 % Extract node name if a Node object is passed
508 if isa(nodeOrName, 'Node')
509 nodeName = nodeOrName.name;
510 else
511 nodeName = nodeOrName;
512 end
513
514 resetBreakdown = @(q) q;
515 resetRepair = @(q) q;
516
517 if nargin >= 7
518 resetBreakdown = varargin{1};
519 end
520 if nargin >= 8
521 resetRepair = varargin{2};
522 end
523
524 self.addNodeBreakdown(baseModel, nodeName, breakdownDist, downServiceDist, resetBreakdown);
525 self.addNodeRepair(nodeName, repairDist, resetRepair);
526 end
527
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
531 %
532 % Parameters:
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
536 %
537 % Example:
538 % env.setBreakdownResetPolicy('Server1', @(q) 0*q);
539 % % Or using node object:
540 % env.setBreakdownResetPolicy(queue, @(q) 0*q);
541
542 % Extract node name if a Node object is passed
543 if isa(nodeOrName, 'Node')
544 nodeName = nodeOrName.name;
545 else
546 nodeName = nodeOrName;
547 end
548
549 downStageName = sprintf('DOWN_%s', nodeName);
550
551 % Find UP and DOWN stage indices
552 upIdx = self.envGraph.findnode('UP');
553 downIdx = self.envGraph.findnode(downStageName);
554
555 if isempty(upIdx) || upIdx == 0
556 line_error(mfilename, 'UP stage not found. Call addNodeBreakdown first.');
557 end
558 if isempty(downIdx) || downIdx == 0
559 line_error(mfilename, sprintf('DOWN stage for node "%s" not found. Call addNodeBreakdown first.', nodeName));
560 end
561
562 % Update the reset function for the breakdown transition (UP -> DOWN)
563 self.resetFun{upIdx, downIdx} = resetFun;
564 end
565
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
569 %
570 % Parameters:
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
574 %
575 % Example:
576 % env.setRepairResetPolicy('Server1', @(q) q);
577 % % Or using node object:
578 % env.setRepairResetPolicy(queue, @(q) q);
579
580 % Extract node name if a Node object is passed
581 if isa(nodeOrName, 'Node')
582 nodeName = nodeOrName.name;
583 else
584 nodeName = nodeOrName;
585 end
586
587 downStageName = sprintf('DOWN_%s', nodeName);
588
589 % Find UP and DOWN stage indices
590 upIdx = self.envGraph.findnode('UP');
591 downIdx = self.envGraph.findnode(downStageName);
592
593 if isempty(upIdx) || upIdx == 0
594 line_error(mfilename, 'UP stage not found. Call addNodeBreakdown first.');
595 end
596 if isempty(downIdx) || downIdx == 0
597 line_error(mfilename, sprintf('DOWN stage for node "%s" not found. Call addNodeBreakdown first.', nodeName));
598 end
599
600 % Update the reset function for the repair transition (DOWN -> UP)
601 self.resetFun{downIdx, upIdx} = resetFun;
602 end
603 end
604end