LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
Posterior.m
1classdef Posterior < EnsembleSolver
2 % Posterior Solver wrapper for models with Prior distributions
3 %
4 % Posterior detects Prior distributions in a model, expands the model
5 % into a family of concrete networks (one per Prior alternative), solves
6 % each using the specified solver, and aggregates results using prior
7 % probabilities as weights.
8 %
9 % @brief Solver wrapper for Bayesian-style uncertainty analysis
10 %
11 % Key characteristics:
12 % - Detects and expands Prior distributions in models
13 % - Orchestrates multiple solver runs
14 % - Aggregates results with prior-weighted expectations
15 % - Provides posterior distribution access
16 %
17 % Example:
18 % @code
19 % model = Network('UncertainService');
20 % source = Source(model, 'Source');
21 % queue = Queue(model, 'Queue', SchedStrategy.FCFS);
22 % sink = Sink(model, 'Sink');
23 %
24 % class = OpenClass(model, 'Jobs');
25 % source.setArrival(class, Exp(1.0));
26 % queue.setService(class, Prior({Exp(1), Exp(2)}, [0.5, 0.5]));
27 %
28 % model.link(model.serialRouting(source, queue, sink));
29 %
30 % post = Posterior(model, @SolverMVA);
31 % avgTable = post.getAvgTable(); % Prior-weighted expectations
32 % postTable = post.getPosteriorTable(); % Per-alternative results
33 % postDist = post.getPosteriorDist('R', queue, class); % Response time distribution
34 % @endcode
35 %
36 % Copyright (c) 2012-2026, Imperial College London
37 % All rights reserved.
38
39 properties
40 solverFactory; % Function handle to create solvers: @(model) SolverXXX(model)
41 priorInfo; % Structure with Prior detection info
42 aggregatedResult; % Prior-weighted aggregate metrics
43 originalModel; % Reference to original model with Prior
44 end
45
46 methods
47 function self = Posterior(model, solverFactory, varargin)
48 % POSTERIOR Create a Posterior solver wrapper
49 %
50 % @brief Creates a Posterior wrapper for uncertainty analysis
51 % @param model Network model (may contain Prior distributions)
52 % @param solverFactory Function handle: @(m) SolverXXX(m) or solver class name
53 % @param varargin Optional solver options
54 % @return self Posterior instance
55
56 self@EnsembleSolver(model, mfilename);
57
58 % Handle solver factory - accept class name or function handle
59 if isa(solverFactory, 'function_handle')
60 self.solverFactory = solverFactory;
61 elseif ischar(solverFactory) || isstring(solverFactory)
62 % Convert solver class name to factory
63 className = char(solverFactory);
64 self.solverFactory = str2func(['@(m) ', className, '(m)']);
65 else
66 line_error(mfilename, 'solverFactory must be a function handle or solver class name');
67 end
68
69 if ~isempty(varargin)
70 self.setOptions(Solver.parseOptions(varargin, Posterior.defaultOptions));
71 else
72 self.setOptions(Posterior.defaultOptions);
73 end
74
75 self.priorInfo = [];
76 self.ensemble = {};
77 self.solvers = {};
78 self.results = {};
79 self.aggregatedResult = [];
80 self.originalModel = model;
81
82 % Detect and validate Prior usage
83 self.detectPriors();
84 end
85
86 function detectPriors(self)
87 % DETECTPRIORS Find Prior distributions in the model
88 %
89 % Scans all nodes for Prior distributions and stores location info.
90 % Currently supports only a single Prior in the model.
91
92 priors = {};
93 model = self.model;
94 nodes = model.getNodes();
95 classes = model.getClasses();
96
97 for i = 1:length(nodes)
98 node = nodes{i};
99
100 % Check service distributions (Queue, Delay)
101 if isa(node, 'Queue') || isa(node, 'Delay')
102 for c = 1:length(classes)
103 try
104 dist = node.getService(classes{c});
105 if ~isempty(dist) && isa(dist, 'Prior')
106 priors{end+1} = struct(...
107 'type', 'service', ...
108 'node', node, ...
109 'nodeName', node.name, ...
110 'nodeIdx', i, ...
111 'class', classes{c}, ...
112 'className', classes{c}.name, ...
113 'classIdx', c, ...
114 'prior', dist);
115 end
116 catch
117 % Service not set for this class
118 end
119 end
120 end
121
122 % Check arrival distributions (Source)
123 if isa(node, 'Source')
124 for c = 1:length(classes)
125 if isa(classes{c}, 'OpenClass')
126 try
127 if ~isempty(node.arrivalProcess) && ...
128 length(node.arrivalProcess) >= 1 && ...
129 size(node.arrivalProcess, 2) >= c && ...
130 ~isempty(node.arrivalProcess{1, c})
131 dist = node.arrivalProcess{1, c};
132 if isa(dist, 'Prior')
133 priors{end+1} = struct(...
134 'type', 'arrival', ...
135 'node', node, ...
136 'nodeName', node.name, ...
137 'nodeIdx', i, ...
138 'class', classes{c}, ...
139 'className', classes{c}.name, ...
140 'classIdx', c, ...
141 'prior', dist);
142 end
143 end
144 catch
145 % Arrival not set
146 end
147 end
148 end
149 end
150 end
151
152 % Validate: currently only single Prior supported
153 if length(priors) > 1
154 line_error(mfilename, 'Currently only a single Prior distribution per model is supported');
155 end
156
157 if isempty(priors)
158 self.priorInfo = [];
159 else
160 self.priorInfo = priors{1};
161 end
162 end
163
164 function hasPrior = hasPriorDistribution(self)
165 % HASPRIOR = HASPRIORDISTRIBUTION()
166 % Return true if model contains a Prior distribution
167 hasPrior = ~isempty(self.priorInfo);
168 end
169
170 function n = getNumAlternatives(self)
171 % N = GETNUMALTERNATIVES()
172 % Return number of Prior alternatives (1 if no Prior)
173 if isempty(self.priorInfo)
174 n = 1;
175 else
176 n = self.priorInfo.prior.getNumAlternatives();
177 end
178 end
179
180 function probs = getProbabilities(self)
181 % PROBS = GETPROBABILITIES()
182 % Return vector of prior probabilities
183 if isempty(self.priorInfo)
184 probs = 1;
185 else
186 probs = self.priorInfo.prior.probabilities;
187 end
188 end
189
190 function E = getNumberOfModels(self)
191 % E = GETNUMBEROFMODELS()
192 % Return number of ensemble models (alternatives)
193 %
194 % Overrides EnsembleSolver to return count based on Prior,
195 % since ensemble is not populated until init().
196 if ~isempty(self.ensemble)
197 E = length(self.ensemble);
198 else
199 E = self.getNumAlternatives();
200 end
201 end
202
203 %% EnsembleSolver abstract method implementations
204
205 function init(self)
206 % INIT Initialize the Posterior solver
207 %
208 % Expands the model into a family of concrete models,
209 % one for each Prior alternative.
210
211 if isempty(self.priorInfo)
212 % No Prior - just use the original model
213 self.ensemble = {self.model};
214 self.solvers{1} = self.solverFactory(self.model);
215 return;
216 end
217
218 prior = self.priorInfo.prior;
219 n = prior.getNumAlternatives();
220 self.ensemble = cell(1, n);
221 self.solvers = cell(1, n);
222
223 for i = 1:n
224 % Deep copy the model
225 modelCopy = self.originalModel.copy();
226
227 % Get the corresponding node and class in the copy
228 nodes = modelCopy.getNodes();
229 classes = modelCopy.getClasses();
230 node = nodes{self.priorInfo.nodeIdx};
231 class = classes{self.priorInfo.classIdx};
232
233 % Replace Prior with concrete alternative
234 concreteDist = prior.getAlternative(i);
235
236 if strcmp(self.priorInfo.type, 'service')
237 node.setService(class, concreteDist);
238 elseif strcmp(self.priorInfo.type, 'arrival')
239 node.setArrival(class, concreteDist);
240 end
241
242 % Rename model to indicate alternative
243 modelCopy.name = sprintf('%s_alt%d', self.originalModel.name, i);
244
245 self.ensemble{i} = modelCopy;
246 self.solvers{i} = self.solverFactory(modelCopy);
247 end
248 end
249
250 function pre(self, it)
251 % PRE Pre-iteration operations (no-op for Posterior)
252 % Posterior only needs a single iteration
253 end
254
255 function [result, runtime] = analyze(self, it, e)
256 % ANALYZE Run solver for ensemble model e
257 %
258 % @param it Iteration number
259 % @param e Ensemble model index
260 % @return result Solver result structure
261 % @return runtime Solver execution time
262
263 T0 = tic;
264 solver = self.solvers{e};
265 solver.runAnalyzer();
266 result = solver.result;
267 runtime = toc(T0);
268 end
269
270 function post(self, it)
271 % POST Post-iteration operations
272 %
273 % Aggregates results from all ensemble models using prior weights.
274
275 self.aggregateResults();
276 end
277
278 function finish(self)
279 % FINISH Finalization (no-op for Posterior)
280 end
281
282 function bool = converged(self, it)
283 % CONVERGED Check convergence
284 %
285 % Posterior converges after the first iteration (it >= 1).
286 bool = (it >= 1);
287 end
288
289 function [QN, UN, RN, TN, AN, WN] = getEnsembleAvg(self)
290 % GETENSEMBLEAVG Get per-model average metrics
291 %
292 % Returns cell arrays with metrics from each ensemble model.
293
294 n = self.getNumberOfModels();
295 QN = cell(1, n);
296 UN = cell(1, n);
297 RN = cell(1, n);
298 TN = cell(1, n);
299 AN = cell(1, n);
300 WN = cell(1, n);
301
302 for e = 1:n
303 if ~isempty(self.results) && size(self.results, 2) >= e && ~isempty(self.results{1, e})
304 res = self.results{1, e};
305 if isfield(res, 'Avg')
306 if isfield(res.Avg, 'Q'), QN{e} = res.Avg.Q; end
307 if isfield(res.Avg, 'U'), UN{e} = res.Avg.U; end
308 if isfield(res.Avg, 'R'), RN{e} = res.Avg.R; end
309 if isfield(res.Avg, 'T'), TN{e} = res.Avg.T; end
310 if isfield(res.Avg, 'A'), AN{e} = res.Avg.A; end
311 if isfield(res.Avg, 'W'), WN{e} = res.Avg.W; end
312 end
313 end
314 end
315 end
316
317 %% Result aggregation methods
318
319 function aggregateResults(self)
320 % AGGREGATERESULTS Compute prior-weighted aggregate metrics
321
322 if isempty(self.results)
323 return;
324 end
325
326 n = self.getNumberOfModels();
327 probs = self.getProbabilities();
328
329 % Initialize aggregated result
330 self.aggregatedResult = struct();
331 self.aggregatedResult.solver = 'Posterior';
332 self.aggregatedResult.Avg = struct();
333
334 % Get dimensions from first result
335 firstResult = [];
336 for e = 1:n
337 if ~isempty(self.results{1, e})
338 firstResult = self.results{1, e};
339 break;
340 end
341 end
342
343 if isempty(firstResult) || ~isfield(firstResult, 'Avg')
344 return;
345 end
346
347 % Aggregate each metric field
348 fields = {'Q', 'U', 'R', 'T', 'A', 'W', 'C', 'X'};
349 for f = 1:length(fields)
350 fname = fields{f};
351 if isfield(firstResult.Avg, fname) && ~isempty(firstResult.Avg.(fname))
352 self.aggregatedResult.Avg.(fname) = zeros(size(firstResult.Avg.(fname)));
353 for e = 1:n
354 if ~isempty(self.results{1, e}) && ...
355 isfield(self.results{1, e}, 'Avg') && ...
356 isfield(self.results{1, e}.Avg, fname) && ...
357 ~isempty(self.results{1, e}.Avg.(fname))
358 self.aggregatedResult.Avg.(fname) = self.aggregatedResult.Avg.(fname) + ...
359 probs(e) * self.results{1, e}.Avg.(fname);
360 end
361 end
362 end
363 end
364 end
365
366 %% Override NetworkSolver methods for aggregated results
367
368 function [QN, UN, RN, TN, AN, WN] = getAvg(self, varargin)
369 % GETAVG Return prior-weighted average metrics
370 %
371 % Returns aggregated metrics weighted by prior probabilities.
372
373 % Run solver if not done
374 if isempty(self.results)
375 self.iterate();
376 end
377
378 QN = []; UN = []; RN = []; TN = []; AN = []; WN = [];
379 if ~isempty(self.aggregatedResult) && isfield(self.aggregatedResult, 'Avg')
380 if isfield(self.aggregatedResult.Avg, 'Q'), QN = self.aggregatedResult.Avg.Q; end
381 if isfield(self.aggregatedResult.Avg, 'U'), UN = self.aggregatedResult.Avg.U; end
382 if isfield(self.aggregatedResult.Avg, 'R'), RN = self.aggregatedResult.Avg.R; end
383 if isfield(self.aggregatedResult.Avg, 'T'), TN = self.aggregatedResult.Avg.T; end
384 if isfield(self.aggregatedResult.Avg, 'A'), AN = self.aggregatedResult.Avg.A; end
385 if isfield(self.aggregatedResult.Avg, 'W'), WN = self.aggregatedResult.Avg.W; end
386 end
387 end
388
389 function AvgTable = getAvgTable(self, varargin)
390 % GETAVGTABLE Return prior-weighted average table
391 %
392 % Returns a table of aggregated metrics weighted by prior probabilities.
393
394 % Run solver if not done
395 if isempty(self.results)
396 self.iterate();
397 end
398
399 [QN, UN, RN, TN, AN, WN] = self.getAvg();
400
401 % Get model structure
402 sn = self.originalModel.getStruct(false);
403 M = sn.nstations;
404 K = sn.nclasses;
405
406 % Build table data
407 Station = {};
408 JobClass = {};
409 QLen = [];
410 Util = [];
411 RespT = [];
412 ResidT = [];
413 ArvR = [];
414 Tput = [];
415
416 for ist = 1:M
417 for k = 1:K
418 Q_val = 0; U_val = 0; R_val = 0; T_val = 0; A_val = 0; W_val = 0;
419
420 if ~isempty(QN), Q_val = QN(ist, k); end
421 if ~isempty(UN), U_val = UN(ist, k); end
422 if ~isempty(RN), R_val = RN(ist, k); end
423 if ~isempty(TN), T_val = TN(ist, k); end
424 if ~isempty(AN), A_val = AN(ist, k); end
425 if ~isempty(WN), W_val = WN(ist, k); end
426
427 % Only include rows with non-zero values
428 if Q_val > 0 || U_val > 0 || T_val > 0
429 Station{end+1, 1} = sn.nodenames{sn.stationToNode(ist)};
430 JobClass{end+1, 1} = sn.classnames{k};
431 QLen(end+1, 1) = Q_val;
432 Util(end+1, 1) = U_val;
433 RespT(end+1, 1) = R_val;
434 ResidT(end+1, 1) = W_val;
435 ArvR(end+1, 1) = A_val;
436 Tput(end+1, 1) = T_val;
437 end
438 end
439 end
440
441 if isempty(Station)
442 AvgTable = table();
443 return;
444 end
445
446 Station = categorical(Station);
447 JobClass = categorical(JobClass);
448
449 AvgTable = table(Station, JobClass, QLen, Util, RespT, ResidT, ArvR, Tput);
450 end
451
452 %% Posterior-specific result methods
453
454 function ptable = getPosteriorTable(self)
455 % GETPOSTERIORTABLE Return table with per-alternative results
456 %
457 % Returns a table showing metrics for each Prior alternative
458 % along with its probability.
459
460 % Run solver if not done
461 if isempty(self.results)
462 self.iterate();
463 end
464
465 probs = self.getProbabilities();
466 n = self.getNumberOfModels();
467 sn = self.originalModel.getStruct(false);
468 M = sn.nstations;
469 K = sn.nclasses;
470
471 % Build table data
472 Alternative = [];
473 Probability = [];
474 Station = {};
475 JobClass = {};
476 QLen = [];
477 Util = [];
478 RespT = [];
479 Tput = [];
480
481 for alt = 1:n
482 if isempty(self.results{1, alt})
483 continue;
484 end
485 res = self.results{1, alt};
486 if ~isfield(res, 'Avg')
487 continue;
488 end
489
490 for ist = 1:M
491 for k = 1:K
492 Q_val = 0; U_val = 0; R_val = 0; T_val = 0;
493
494 if isfield(res.Avg, 'Q') && ~isempty(res.Avg.Q)
495 Q_val = res.Avg.Q(ist, k);
496 end
497 if isfield(res.Avg, 'U') && ~isempty(res.Avg.U)
498 U_val = res.Avg.U(ist, k);
499 end
500 if isfield(res.Avg, 'R') && ~isempty(res.Avg.R)
501 R_val = res.Avg.R(ist, k);
502 end
503 if isfield(res.Avg, 'T') && ~isempty(res.Avg.T)
504 T_val = res.Avg.T(ist, k);
505 end
506
507 % Only include rows with non-zero values
508 if Q_val > 0 || U_val > 0 || T_val > 0
509 Alternative(end+1, 1) = alt;
510 Probability(end+1, 1) = probs(alt);
511 Station{end+1, 1} = sn.nodenames{sn.stationToNode(ist)};
512 JobClass{end+1, 1} = sn.classnames{k};
513 QLen(end+1, 1) = Q_val;
514 Util(end+1, 1) = U_val;
515 RespT(end+1, 1) = R_val;
516 Tput(end+1, 1) = T_val;
517 end
518 end
519 end
520 end
521
522 if isempty(Alternative)
523 ptable = table();
524 return;
525 end
526
527 Station = categorical(Station);
528 JobClass = categorical(JobClass);
529
530 ptable = table(Alternative, Probability, Station, JobClass, QLen, Util, RespT, Tput);
531 end
532
533 function empDist = getPosteriorDist(self, metric, station, class)
534 % GETPOSTERIORDIST Return empirical distribution of a metric
535 %
536 % Returns an EmpiricalCDF object representing the posterior
537 % distribution of the specified metric across Prior alternatives.
538 %
539 % @param metric Metric name: 'Q', 'U', 'R', 'T', 'A', 'W'
540 % @param station Station node or station index
541 % @param class JobClass object or class index
542 % @return empDist EmpiricalCDF object
543
544 % Run solver if not done
545 if isempty(self.results)
546 self.iterate();
547 end
548
549 probs = self.getProbabilities();
550 n = self.getNumberOfModels();
551
552 % Get station index
553 if isnumeric(station)
554 ist = station;
555 elseif isa(station, 'Station')
556 ist = station.stationIndex;
557 else
558 line_error(mfilename, 'station must be a Station object or numeric index');
559 end
560
561 % Get class index
562 if isnumeric(class)
563 k = class;
564 elseif isa(class, 'JobClass')
565 k = class.index;
566 else
567 line_error(mfilename, 'class must be a JobClass object or numeric index');
568 end
569
570 % Extract metric values for each alternative
571 values = zeros(n, 1);
572 for e = 1:n
573 if ~isempty(self.results{1, e}) && isfield(self.results{1, e}, 'Avg')
574 res = self.results{1, e}.Avg;
575 if isfield(res, metric) && ~isempty(res.(metric))
576 values(e) = res.(metric)(ist, k);
577 else
578 values(e) = NaN;
579 end
580 else
581 values(e) = NaN;
582 end
583 end
584
585 % Build empirical CDF
586 % Sort values and corresponding probabilities
587 [sortedVals, sortIdx] = sort(values);
588 sortedProbs = probs(sortIdx);
589 cdfVals = cumsum(sortedProbs);
590
591 % Create data matrix for EmpiricalCDF: [CDF_value, X_value]
592 cdfData = [cdfVals(:), sortedVals(:)];
593
594 % Create EmpiricalCDF object
595 empDist = EmpiricalCDF(cdfData);
596 end
597
598 %% Required Solver abstract methods
599
600 function runtime = runAnalyzer(self, options)
601 % RUNANALYZER Run the Posterior analysis
602 %
603 % @param options Solver options (optional)
604 % @return runtime Total runtime in seconds
605
606 T0 = tic;
607 if nargin >= 2 && ~isempty(options)
608 self.setOptions(options);
609 end
610 self.iterate();
611 runtime = toc(T0);
612 end
613
614 function sn = getStruct(self)
615 % GETSTRUCT Return model structure
616 %
617 % Returns the structure of the original model.
618 sn = self.originalModel.getStruct(false);
619 end
620
621 %% Static methods
622
623 function [allMethods] = listValidMethods(self)
624 % LISTVALIDMETHODS Return valid methods
625 allMethods = {'default'};
626 end
627 end
628
629 methods (Static)
630 function featSupported = getFeatureSet()
631 % GETFEATURESET Return supported features
632 featSupported = SolverFeatureSet;
633 featSupported.setTrue('Prior');
634 end
635
636 function [bool, featSupported] = supports(model)
637 % SUPPORTS Check if model is supported
638 bool = true;
639 featSupported = Posterior.getFeatureSet();
640 end
641
642 function options = defaultOptions()
643 % DEFAULTOPTIONS Return default options
644 options = Solver.defaultOptions();
645 options.iter_max = 1; % Single iteration for Posterior
646 end
647 end
648end
Definition mmt.m:92