1classdef Posterior < EnsembleSolver
2 % Posterior Solver wrapper
for models with Prior distributions
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.
9 % @brief Solver wrapper
for Bayesian-style uncertainty analysis
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
19 % model = Network(
'UncertainService');
20 % source = Source(model,
'Source');
21 % queue = Queue(model,
'Queue', SchedStrategy.FCFS);
22 % sink = Sink(model,
'Sink');
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]));
28 % model.link(model.serialRouting(source, queue, sink));
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
36 % Copyright (c) 2012-2026, Imperial College London
37 % All rights reserved.
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
47 function self = Posterior(model, solverFactory, varargin)
48 % POSTERIOR Create a Posterior solver wrapper
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
56 self@EnsembleSolver(model, mfilename);
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)']);
66 line_error(mfilename,
'solverFactory must be a function handle or solver class name');
70 self.setOptions(Solver.parseOptions(varargin, Posterior.defaultOptions));
72 self.setOptions(Posterior.defaultOptions);
79 self.aggregatedResult = [];
80 self.originalModel = model;
82 % Detect and validate Prior usage
86 function detectPriors(self)
87 % DETECTPRIORS Find Prior distributions in the model
89 % Scans all
nodes for Prior distributions and stores location info.
90 % Currently supports only a single Prior in the model.
94 nodes = model.getNodes();
97 for i = 1:length(
nodes)
100 % Check service distributions (Queue, Delay)
101 if isa(node,
'Queue') || isa(node,
'Delay')
104 dist = node.getService(
classes{c});
105 if ~isempty(dist) && isa(dist,
'Prior')
106 priors{end+1} = struct(...
107 'type', 'service', ...
109 'nodeName', node.name, ...
112 'className',
classes{c}.name, ...
117 % Service not set
for this class
122 % Check arrival distributions (Source)
123 if isa(node,
'Source')
125 if isa(
classes{c},
'OpenClass')
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', ...
136 'nodeName', node.name, ...
139 'className',
classes{c}.name, ...
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');
160 self.priorInfo = priors{1};
164 function hasPrior = hasPriorDistribution(self)
165 % HASPRIOR = HASPRIORDISTRIBUTION()
166 % Return
true if model contains a Prior distribution
167 hasPrior = ~isempty(self.priorInfo);
170 function n = getNumAlternatives(self)
171 % N = GETNUMALTERNATIVES()
172 % Return number of Prior alternatives (1
if no Prior)
173 if isempty(self.priorInfo)
176 n = self.priorInfo.prior.getNumAlternatives();
180 function probs = getProbabilities(self)
181 % PROBS = GETPROBABILITIES()
182 % Return vector of prior probabilities
183 if isempty(self.priorInfo)
186 probs = self.priorInfo.prior.probabilities;
190 function E = getNumberOfModels(self)
191 % E = GETNUMBEROFMODELS()
192 % Return number of ensemble models (alternatives)
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);
199 E = self.getNumAlternatives();
203 %% EnsembleSolver abstract method implementations
206 % INIT Initialize the Posterior solver
208 % Expands the model into a family of concrete models,
209 % one for each Prior alternative.
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);
218 prior = self.priorInfo.prior;
219 n = prior.getNumAlternatives();
220 self.ensemble = cell(1, n);
221 self.solvers = cell(1, n);
224 % Deep copy the model
225 modelCopy = self.originalModel.copy();
227 % Get the corresponding node and
class in the copy
228 nodes = modelCopy.getNodes();
229 classes = modelCopy.getClasses();
230 node =
nodes{self.priorInfo.nodeIdx};
233 % Replace Prior with concrete alternative
234 concreteDist = prior.getAlternative(i);
236 if strcmp(self.priorInfo.type,
'service')
237 node.setService(class, concreteDist);
238 elseif strcmp(self.priorInfo.type, 'arrival')
239 node.setArrival(class, concreteDist);
242 % Rename model to indicate alternative
243 modelCopy.name = sprintf('%s_alt%d', self.originalModel.name, i);
245 self.ensemble{i} = modelCopy;
246 self.solvers{i} = self.solverFactory(modelCopy);
250 function pre(self, it)
251 % PRE Pre-iteration operations (no-op for Posterior)
252 % Posterior only needs a single iteration
255 function [result, runtime] = analyze(self, it, e)
256 % ANALYZE Run solver for ensemble model e
258 % @param it Iteration number
259 % @param e Ensemble model index
260 % @return result Solver result structure
261 % @return runtime Solver execution time
264 solver = self.solvers{e};
265 solver.runAnalyzer();
266 result = solver.result;
270 function post(self, it)
271 % POST Post-iteration operations
273 % Aggregates results from all ensemble models using prior weights.
275 self.aggregateResults();
278 function finish(self)
279 % FINISH Finalization (no-op
for Posterior)
282 function
bool = converged(self, it)
283 % CONVERGED Check convergence
285 % Posterior converges after the first iteration (it >= 1).
289 function [QN, UN, RN, TN, AN, WN] = getEnsembleAvg(self)
290 % GETENSEMBLEAVG Get per-model average metrics
292 % Returns cell arrays with metrics from each ensemble model.
294 n = self.getNumberOfModels();
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
317 %% Result aggregation methods
319 function aggregateResults(self)
320 % AGGREGATERESULTS Compute prior-weighted aggregate metrics
322 if isempty(self.results)
326 n = self.getNumberOfModels();
327 probs = self.getProbabilities();
329 % Initialize aggregated result
330 self.aggregatedResult =
struct();
331 self.aggregatedResult.solver =
'Posterior';
332 self.aggregatedResult.Avg =
struct();
334 % Get dimensions from first result
337 if ~isempty(self.results{1, e})
338 firstResult = self.results{1, e};
343 if isempty(firstResult) || ~isfield(firstResult,
'Avg')
347 % Aggregate each metric field
348 fields = {
'Q',
'U',
'R',
'T',
'A',
'W',
'C',
'X'};
349 for f = 1:length(fields)
351 if isfield(firstResult.Avg, fname) && ~isempty(firstResult.Avg.(fname))
352 self.aggregatedResult.Avg.(fname) = zeros(size(firstResult.Avg.(fname)));
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);
366 %% Override NetworkSolver methods
for aggregated results
368 function [QN, UN, RN, TN, AN, WN] = getAvg(self, varargin)
369 % GETAVG Return prior-weighted average metrics
371 % Returns aggregated metrics weighted by prior probabilities.
373 % Run solver if not done
374 if isempty(self.results)
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
389 function AvgTable = getAvgTable(self, varargin)
390 % GETAVGTABLE Return prior-weighted average table
392 % Returns a table of aggregated metrics weighted by prior probabilities.
394 % Run solver if not done
395 if isempty(self.results)
399 [QN, UN, RN, TN, AN, WN] = self.getAvg();
401 % Get model structure
402 sn = self.originalModel.getStruct(false);
418 Q_val = 0; U_val = 0; R_val = 0; T_val = 0; A_val = 0; W_val = 0;
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
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;
446 Station = categorical(Station);
447 JobClass = categorical(JobClass);
449 AvgTable = table(Station, JobClass, QLen, Util, RespT, ResidT, ArvR, Tput);
452 %% Posterior-specific result methods
454 function ptable = getPosteriorTable(self)
455 % GETPOSTERIORTABLE Return table with per-alternative results
457 % Returns a table showing metrics for each Prior alternative
458 % along with its probability.
460 % Run solver if not done
461 if isempty(self.results)
465 probs = self.getProbabilities();
466 n = self.getNumberOfModels();
467 sn = self.originalModel.getStruct(
false);
482 if isempty(self.results{1, alt})
485 res = self.results{1, alt};
486 if ~isfield(res,
'Avg')
492 Q_val = 0; U_val = 0; R_val = 0; T_val = 0;
494 if isfield(res.Avg, 'Q') && ~isempty(res.Avg.Q)
495 Q_val = res.Avg.Q(ist, k);
497 if isfield(res.Avg, 'U') && ~isempty(res.Avg.U)
498 U_val = res.Avg.U(ist, k);
500 if isfield(res.Avg, 'R') && ~isempty(res.Avg.R)
501 R_val = res.Avg.R(ist, k);
503 if isfield(res.Avg, 'T') && ~isempty(res.Avg.T)
504 T_val = res.Avg.T(ist, k);
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;
522 if isempty(Alternative)
527 Station = categorical(Station);
528 JobClass = categorical(JobClass);
530 ptable = table(Alternative, Probability, Station, JobClass, QLen, Util, RespT, Tput);
533 function empDist = getPosteriorDist(self, metric, station,
class)
534 % GETPOSTERIORDIST Return empirical distribution of a metric
536 % Returns an EmpiricalCDF
object representing the posterior
537 % distribution of the specified metric across Prior alternatives.
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
544 % Run solver
if not done
545 if isempty(self.results)
549 probs = self.getProbabilities();
550 n = self.getNumberOfModels();
553 if isnumeric(station)
555 elseif isa(station, 'Station')
556 ist = station.stationIndex;
558 line_error(mfilename, 'station must be a Station
object or numeric index');
564 elseif isa(class, 'JobClass')
567 line_error(mfilename, 'class must be a JobClass
object or numeric index');
570 % Extract metric values for each alternative
571 values = zeros(n, 1);
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);
585 % Build empirical CDF
586 % Sort values and corresponding probabilities
587 [sortedVals, sortIdx] = sort(values);
588 sortedProbs = probs(sortIdx);
589 cdfVals = cumsum(sortedProbs);
591 % Create data matrix
for EmpiricalCDF: [CDF_value, X_value]
592 cdfData = [cdfVals(:), sortedVals(:)];
594 % Create EmpiricalCDF
object
595 empDist = EmpiricalCDF(cdfData);
598 %% Required Solver abstract methods
600 function runtime = runAnalyzer(self, options)
601 % RUNANALYZER Run the Posterior analysis
603 % @param options Solver options (optional)
604 % @
return runtime Total runtime in seconds
607 if nargin >= 2 && ~isempty(options)
608 self.setOptions(options);
614 function sn = getStruct(self)
615 % GETSTRUCT Return model structure
617 % Returns the structure of the original model.
618 sn = self.originalModel.getStruct(
false);
623 function [allMethods] = listValidMethods(self)
624 % LISTVALIDMETHODS Return valid methods
625 allMethods = {
'default'};
630 function featSupported = getFeatureSet()
631 % GETFEATURESET Return supported features
632 featSupported = SolverFeatureSet;
633 featSupported.setTrue(
'Prior');
636 function [bool, featSupported] = supports(model)
637 % SUPPORTS Check
if model
is supported
639 featSupported = Posterior.getFeatureSet();
642 function options = defaultOptions()
643 % DEFAULTOPTIONS Return
default options
644 options = Solver.defaultOptions();
645 options.iter_max = 1; % Single iteration
for Posterior