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 line_debug('Posterior init: expanding model into %d alternatives', self.getNumAlternatives());
212 if isempty(self.priorInfo)
213 % No Prior - just use the original model
214 self.ensemble = {self.model};
215 self.solvers{1} = self.solverFactory(self.model);
216 return;
217 end
218
219 prior = self.priorInfo.prior;
220 n = prior.getNumAlternatives();
221 self.ensemble = cell(1, n);
222 self.solvers = cell(1, n);
223
224 for i = 1:n
225 % Deep copy the model
226 modelCopy = self.originalModel.copy();
227
228 % Get the corresponding node and class in the copy
229 nodes = modelCopy.getNodes();
230 classes = modelCopy.getClasses();
231 node = nodes{self.priorInfo.nodeIdx};
232 class = classes{self.priorInfo.classIdx};
233
234 % Replace Prior with concrete alternative
235 concreteDist = prior.getAlternative(i);
236
237 if strcmp(self.priorInfo.type, 'service')
238 node.setService(class, concreteDist);
239 elseif strcmp(self.priorInfo.type, 'arrival')
240 node.setArrival(class, concreteDist);
241 end
242
243 % Rename model to indicate alternative
244 modelCopy.name = sprintf('%s_alt%d', self.originalModel.name, i);
245
246 self.ensemble{i} = modelCopy;
247 self.solvers{i} = self.solverFactory(modelCopy);
248 end
249 end
250
251 function pre(self, it)
252 % PRE Pre-iteration operations (no-op for Posterior)
253 % Posterior only needs a single iteration
254 end
255
256 function [result, runtime] = analyze(self, it, e)
257 % ANALYZE Run solver for ensemble model e
258 %
259 % @param it Iteration number
260 % @param e Ensemble model index
261 % @return result Solver result structure
262 % @return runtime Solver execution time
263
264 T0 = tic;
265 solver = self.solvers{e};
266 solver.runAnalyzer();
267 result = solver.result;
268 runtime = toc(T0);
269 end
270
271 function post(self, it)
272 % POST Post-iteration operations
273 %
274 % Aggregates results from all ensemble models using prior weights.
275
276 self.aggregateResults();
277 end
278
279 function finish(self)
280 % FINISH Finalization (no-op for Posterior)
281 end
282
283 function bool = converged(self, it)
284 % CONVERGED Check convergence
285 %
286 % Posterior converges after the first iteration (it >= 1).
287 bool = (it >= 1);
288 end
289
290 function [QN, UN, RN, TN, AN, WN] = getEnsembleAvg(self)
291 % GETENSEMBLEAVG Get per-model average metrics
292 %
293 % Returns cell arrays with metrics from each ensemble model.
294
295 n = self.getNumberOfModels();
296 QN = cell(1, n);
297 UN = cell(1, n);
298 RN = cell(1, n);
299 TN = cell(1, n);
300 AN = cell(1, n);
301 WN = cell(1, n);
302
303 for e = 1:n
304 if ~isempty(self.results) && size(self.results, 2) >= e && ~isempty(self.results{1, e})
305 res = self.results{1, e};
306 if isfield(res, 'Avg')
307 if isfield(res.Avg, 'Q'), QN{e} = res.Avg.Q; end
308 if isfield(res.Avg, 'U'), UN{e} = res.Avg.U; end
309 if isfield(res.Avg, 'R'), RN{e} = res.Avg.R; end
310 if isfield(res.Avg, 'T'), TN{e} = res.Avg.T; end
311 if isfield(res.Avg, 'A'), AN{e} = res.Avg.A; end
312 if isfield(res.Avg, 'W'), WN{e} = res.Avg.W; end
313 end
314 end
315 end
316 end
317
318 %% Result aggregation methods
319
320 function aggregateResults(self)
321 % AGGREGATERESULTS Compute prior-weighted aggregate metrics
322
323 if isempty(self.results)
324 return;
325 end
326
327 n = self.getNumberOfModels();
328 probs = self.getProbabilities();
329
330 % Initialize aggregated result
331 self.aggregatedResult = struct();
332 self.aggregatedResult.solver = 'Posterior';
333 self.aggregatedResult.Avg = struct();
334
335 % Get dimensions from first result
336 firstResult = [];
337 for e = 1:n
338 if ~isempty(self.results{1, e})
339 firstResult = self.results{1, e};
340 break;
341 end
342 end
343
344 if isempty(firstResult) || ~isfield(firstResult, 'Avg')
345 return;
346 end
347
348 % Aggregate each metric field
349 fields = {'Q', 'U', 'R', 'T', 'A', 'W', 'C', 'X'};
350 for f = 1:length(fields)
351 fname = fields{f};
352 if isfield(firstResult.Avg, fname) && ~isempty(firstResult.Avg.(fname))
353 self.aggregatedResult.Avg.(fname) = zeros(size(firstResult.Avg.(fname)));
354 for e = 1:n
355 if ~isempty(self.results{1, e}) && ...
356 isfield(self.results{1, e}, 'Avg') && ...
357 isfield(self.results{1, e}.Avg, fname) && ...
358 ~isempty(self.results{1, e}.Avg.(fname))
359 self.aggregatedResult.Avg.(fname) = self.aggregatedResult.Avg.(fname) + ...
360 probs(e) * self.results{1, e}.Avg.(fname);
361 end
362 end
363 end
364 end
365 end
366
367 %% Override NetworkSolver methods for aggregated results
368
369 function [QN, UN, RN, TN, AN, WN] = getAvg(self, varargin)
370 % GETAVG Return prior-weighted average metrics
371 %
372 % Returns aggregated metrics weighted by prior probabilities.
373
374 % Run solver if not done
375 if isempty(self.results)
376 self.iterate();
377 end
378
379 QN = []; UN = []; RN = []; TN = []; AN = []; WN = [];
380 if ~isempty(self.aggregatedResult) && isfield(self.aggregatedResult, 'Avg')
381 if isfield(self.aggregatedResult.Avg, 'Q'), QN = self.aggregatedResult.Avg.Q; end
382 if isfield(self.aggregatedResult.Avg, 'U'), UN = self.aggregatedResult.Avg.U; end
383 if isfield(self.aggregatedResult.Avg, 'R'), RN = self.aggregatedResult.Avg.R; end
384 if isfield(self.aggregatedResult.Avg, 'T'), TN = self.aggregatedResult.Avg.T; end
385 if isfield(self.aggregatedResult.Avg, 'A'), AN = self.aggregatedResult.Avg.A; end
386 if isfield(self.aggregatedResult.Avg, 'W'), WN = self.aggregatedResult.Avg.W; end
387 end
388 end
389
390 function AvgTable = getAvgTable(self, varargin)
391 % GETAVGTABLE Return prior-weighted average table
392 %
393 % Returns a table of aggregated metrics weighted by prior probabilities.
394
395 % Run solver if not done
396 if isempty(self.results)
397 self.iterate();
398 end
399
400 [QN, UN, RN, TN, AN, WN] = self.getAvg();
401
402 % Get model structure
403 sn = self.originalModel.getStruct(false);
404 M = sn.nstations;
405 K = sn.nclasses;
406
407 % Build table data
408 Station = {};
409 JobClass = {};
410 QLen = [];
411 Util = [];
412 RespT = [];
413 ResidT = [];
414 ArvR = [];
415 Tput = [];
416
417 for ist = 1:M
418 for k = 1:K
419 Q_val = 0; U_val = 0; R_val = 0; T_val = 0; A_val = 0; W_val = 0;
420
421 if ~isempty(QN), Q_val = QN(ist, k); end
422 if ~isempty(UN), U_val = UN(ist, k); end
423 if ~isempty(RN), R_val = RN(ist, k); end
424 if ~isempty(TN), T_val = TN(ist, k); end
425 if ~isempty(AN), A_val = AN(ist, k); end
426 if ~isempty(WN), W_val = WN(ist, k); end
427
428 % Only include rows with non-zero values
429 if Q_val > 0 || U_val > 0 || T_val > 0
430 Station{end+1, 1} = sn.nodenames{sn.stationToNode(ist)};
431 JobClass{end+1, 1} = sn.classnames{k};
432 QLen(end+1, 1) = Q_val;
433 Util(end+1, 1) = U_val;
434 RespT(end+1, 1) = R_val;
435 ResidT(end+1, 1) = W_val;
436 ArvR(end+1, 1) = A_val;
437 Tput(end+1, 1) = T_val;
438 end
439 end
440 end
441
442 if isempty(Station)
443 AvgTable = table();
444 return;
445 end
446
447 Station = categorical(Station);
448 JobClass = categorical(JobClass);
449
450 AvgTable = table(Station, JobClass, QLen, Util, RespT, ResidT, ArvR, Tput);
451 end
452
453 %% Posterior-specific result methods
454
455 function ptable = getPosteriorTable(self)
456 % GETPOSTERIORTABLE Return table with per-alternative results
457 %
458 % Returns a table showing metrics for each Prior alternative
459 % along with its probability.
460
461 % Run solver if not done
462 if isempty(self.results)
463 self.iterate();
464 end
465
466 probs = self.getProbabilities();
467 n = self.getNumberOfModels();
468 sn = self.originalModel.getStruct(false);
469 M = sn.nstations;
470 K = sn.nclasses;
471
472 % Build table data
473 Alternative = [];
474 Probability = [];
475 Station = {};
476 JobClass = {};
477 QLen = [];
478 Util = [];
479 RespT = [];
480 Tput = [];
481
482 for alt = 1:n
483 if isempty(self.results{1, alt})
484 continue;
485 end
486 res = self.results{1, alt};
487 if ~isfield(res, 'Avg')
488 continue;
489 end
490
491 for ist = 1:M
492 for k = 1:K
493 Q_val = 0; U_val = 0; R_val = 0; T_val = 0;
494
495 if isfield(res.Avg, 'Q') && ~isempty(res.Avg.Q)
496 Q_val = res.Avg.Q(ist, k);
497 end
498 if isfield(res.Avg, 'U') && ~isempty(res.Avg.U)
499 U_val = res.Avg.U(ist, k);
500 end
501 if isfield(res.Avg, 'R') && ~isempty(res.Avg.R)
502 R_val = res.Avg.R(ist, k);
503 end
504 if isfield(res.Avg, 'T') && ~isempty(res.Avg.T)
505 T_val = res.Avg.T(ist, k);
506 end
507
508 % Only include rows with non-zero values
509 if Q_val > 0 || U_val > 0 || T_val > 0
510 Alternative(end+1, 1) = alt;
511 Probability(end+1, 1) = probs(alt);
512 Station{end+1, 1} = sn.nodenames{sn.stationToNode(ist)};
513 JobClass{end+1, 1} = sn.classnames{k};
514 QLen(end+1, 1) = Q_val;
515 Util(end+1, 1) = U_val;
516 RespT(end+1, 1) = R_val;
517 Tput(end+1, 1) = T_val;
518 end
519 end
520 end
521 end
522
523 if isempty(Alternative)
524 ptable = table();
525 return;
526 end
527
528 Station = categorical(Station);
529 JobClass = categorical(JobClass);
530
531 ptable = table(Alternative, Probability, Station, JobClass, QLen, Util, RespT, Tput);
532 end
533
534 function empDist = getPosteriorDist(self, metric, station, class)
535 % GETPOSTERIORDIST Return empirical distribution of a metric
536 %
537 % Returns an EmpiricalCDF object representing the posterior
538 % distribution of the specified metric across Prior alternatives.
539 %
540 % @param metric Metric name: 'Q', 'U', 'R', 'T', 'A', 'W'
541 % @param station Station node or station index
542 % @param class JobClass object or class index
543 % @return empDist EmpiricalCDF object
544
545 % Run solver if not done
546 if isempty(self.results)
547 self.iterate();
548 end
549
550 probs = self.getProbabilities();
551 n = self.getNumberOfModels();
552
553 % Get station index
554 if isnumeric(station)
555 ist = station;
556 elseif isa(station, 'Station')
557 ist = station.stationIndex;
558 else
559 line_error(mfilename, 'station must be a Station object or numeric index');
560 end
561
562 % Get class index
563 if isnumeric(class)
564 k = class;
565 elseif isa(class, 'JobClass')
566 k = class.index;
567 else
568 line_error(mfilename, 'class must be a JobClass object or numeric index');
569 end
570
571 % Extract metric values for each alternative
572 values = zeros(n, 1);
573 for e = 1:n
574 if ~isempty(self.results{1, e}) && isfield(self.results{1, e}, 'Avg')
575 res = self.results{1, e}.Avg;
576 if isfield(res, metric) && ~isempty(res.(metric))
577 values(e) = res.(metric)(ist, k);
578 else
579 values(e) = NaN;
580 end
581 else
582 values(e) = NaN;
583 end
584 end
585
586 % Build empirical CDF
587 % Sort values and corresponding probabilities
588 [sortedVals, sortIdx] = sort(values);
589 sortedProbs = probs(sortIdx);
590 cdfVals = cumsum(sortedProbs);
591
592 % Create data matrix for EmpiricalCDF: [CDF_value, X_value]
593 cdfData = [cdfVals(:), sortedVals(:)];
594
595 % Create EmpiricalCDF object
596 empDist = EmpiricalCDF(cdfData);
597 end
598
599 %% Required Solver abstract methods
600
601 function runtime = runAnalyzer(self, options)
602 % RUNANALYZER Run the Posterior analysis
603 %
604 % @param options Solver options (optional)
605 % @return runtime Total runtime in seconds
606
607 T0 = tic;
608 if nargin >= 2 && ~isempty(options)
609 self.setOptions(options);
610 end
611 line_debug('Posterior solver starting: nalternatives=%d', self.getNumAlternatives());
612 line_debug('Default method: using Posterior ensemble analysis\n');
613 self.iterate();
614 runtime = toc(T0);
615 end
616
617 function sn = getStruct(self)
618 % GETSTRUCT Return model structure
619 %
620 % Returns the structure of the original model.
621 sn = self.originalModel.getStruct(false);
622 end
623
624 %% Static methods
625
626 function [allMethods] = listValidMethods(self)
627 % LISTVALIDMETHODS Return valid methods
628 allMethods = {'default'};
629 end
630 end
631
632 methods (Static)
633 function featSupported = getFeatureSet()
634 % GETFEATURESET Return supported features
635 featSupported = SolverFeatureSet;
636 featSupported.setTrue('Prior');
637 end
638
639 function [bool, featSupported] = supports(model)
640 % SUPPORTS Check if model is supported
641 bool = true;
642 featSupported = Posterior.getFeatureSet();
643 end
644
645 function options = defaultOptions()
646 % DEFAULTOPTIONS Return default options
647 options = Solver.defaultOptions();
648 options.iter_max = 1; % Single iteration for Posterior
649 end
650 end
651end
Definition mmt.m:92