LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
UQ.m
1classdef UQ < EnsembleSolver
2 % UQ Solver wrapper for models with Prior distributions
3 %
4 % UQ 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 = UQ(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 (Constant)
40 % Cap on the tensor-product design size. A design point is one full
41 % solver run, so this bounds the cost of a quadrature design over
42 % several Priors; beyond it the Monte Carlo design is the right tool.
43 MaxDesignPoints = 4096;
44 end
45
46 properties
47 solverFactory; % Function handle to create solvers: @(model) SolverXXX(model)
48 priorInfo; % Struct array with Prior detection info, one entry per Prior
49 design; % Struct array of design points: .weight and .dists (one per Prior)
50 aggregatedResult; % Prior-weighted aggregate metrics
51 originalModel; % Reference to original model with Prior
52 end
53
54 methods
55 function self = UQ(model, solverFactory, varargin)
56 % UQ Create a UQ solver wrapper
57 %
58 % @brief Creates a UQ wrapper for uncertainty analysis
59 % @param model Network model (may contain Prior distributions)
60 % @param solverFactory Function handle: @(m) SolverXXX(m) or solver class name
61 % @param varargin Optional solver options
62 % @return self UQ instance
63
64 self@EnsembleSolver(model, mfilename);
65
66 % Handle solver factory - accept class name or function handle
67 if isa(solverFactory, 'function_handle')
68 self.solverFactory = solverFactory;
69 elseif ischar(solverFactory) || isstring(solverFactory)
70 % Convert solver class name to factory
71 className = char(solverFactory);
72 self.solverFactory = str2func(['@(m) ', className, '(m)']);
73 else
74 line_error(mfilename, 'solverFactory must be a function handle or solver class name');
75 end
76
77 if ~isempty(varargin)
78 self.setOptions(Solver.parseOptions(varargin, UQ.defaultOptions));
79 else
80 self.setOptions(UQ.defaultOptions);
81 end
82
83 self.priorInfo = [];
84 self.design = [];
85 self.ensemble = {};
86 self.solvers = {};
87 self.results = {};
88 self.aggregatedResult = [];
89 self.originalModel = model;
90
91 % Detect and validate Prior usage
92 self.detectPriors();
93 end
94
95 function detectPriors(self)
96 % DETECTPRIORS Find Prior distributions in the model
97 %
98 % Scans all nodes for Prior distributions and stores location info.
99 % Currently supports only a single Prior in the model.
100
101 priors = {};
102 model = self.model;
103 nodes = model.getNodes();
104 classes = model.getClasses();
105
106 for i = 1:length(nodes)
107 node = nodes{i};
108
109 % Check service distributions (Queue, Delay)
110 if isa(node, 'Queue') || isa(node, 'Delay')
111 for c = 1:length(classes)
112 try
113 dist = node.getService(classes{c});
114 if ~isempty(dist) && isa(dist, 'Prior')
115 priors{end+1} = struct(...
116 'type', 'service', ...
117 'node', node, ...
118 'nodeName', node.name, ...
119 'nodeIdx', i, ...
120 'class', classes{c}, ...
121 'className', classes{c}.name, ...
122 'classIdx', c, ...
123 'prior', dist);
124 end
125 catch
126 % Service not set for this class
127 end
128 end
129 end
130
131 % Check arrival distributions (Source)
132 if isa(node, 'Source')
133 for c = 1:length(classes)
134 if isa(classes{c}, 'OpenClass')
135 try
136 if ~isempty(node.arrivalProcess) && ...
137 length(node.arrivalProcess) >= 1 && ...
138 size(node.arrivalProcess, 2) >= c && ...
139 ~isempty(node.arrivalProcess{1, c})
140 dist = node.arrivalProcess{1, c};
141 if isa(dist, 'Prior')
142 priors{end+1} = struct(...
143 'type', 'arrival', ...
144 'node', node, ...
145 'nodeName', node.name, ...
146 'nodeIdx', i, ...
147 'class', classes{c}, ...
148 'className', classes{c}.name, ...
149 'classIdx', c, ...
150 'prior', dist);
151 end
152 end
153 catch
154 % Arrival not set
155 end
156 end
157 end
158 end
159 end
160
161 if isempty(priors)
162 self.priorInfo = [];
163 else
164 self.priorInfo = [priors{:}];
165 end
166 end
167
168 function buildDesign(self)
169 % BUILDDESIGN Reduce the detected Priors to weighted design points
170 %
171 % Each design point assigns one concrete Distribution to every
172 % Prior in the model and carries the weight of that joint
173 % assignment. Discrete and quadrature designs take the tensor
174 % product of the per-Prior alternatives, so their weights are the
175 % products of the marginal weights: this is the product-density
176 % case of the joint f(theta_1,...,theta_l) in Trivedi and Bobbio
177 % (2017), Eq. (3.67), and assumes the Priors are independent.
178 % Monte Carlo instead draws all Priors jointly, so its cost is
179 % independent of the number of Priors.
180
181 if isempty(self.priorInfo)
182 self.design = struct('weight', 1, 'dists', {{}});
183 return;
184 end
185
186 L = length(self.priorInfo);
187 method = self.getUQMethod();
188 n = self.getUQNodes();
189
190 if strcmp(method, 'montecarlo')
191 self.design = repmat(struct('weight', 1/n, 'dists', {{}}), 1, n);
192 for i = 1:n
193 dists = cell(1, L);
194 for l = 1:L
195 [d, ~] = self.priorInfo(l).prior.discretize(1, 'montecarlo');
196 dists{l} = d{1};
197 end
198 self.design(i).weight = 1/n;
199 self.design(i).dists = dists;
200 end
201 return;
202 end
203
204 % Tensor product of the per-Prior alternatives
205 margDists = cell(1, L);
206 margWeights = cell(1, L);
207 for l = 1:L
208 [margDists{l}, margWeights{l}] = self.priorInfo(l).prior.discretize(n, 'quadrature');
209 end
210
211 counts = cellfun(@length, margDists);
212 total = prod(counts);
213 if total > UQ.MaxDesignPoints
214 line_error(mfilename, sprintf(['Tensor-product design has %d points, above the limit of %d. ', ...
215 'Use options.method = ''montecarlo'' or reduce options.samples.'], ...
216 total, UQ.MaxDesignPoints));
217 end
218
219 self.design = repmat(struct('weight', 1, 'dists', {{}}), 1, total);
220 for i = 1:total
221 idx = UQ.unrankIndex(i, counts);
222 dists = cell(1, L);
223 w = 1;
224 for l = 1:L
225 dists{l} = margDists{l}{idx(l)};
226 w = w * margWeights{l}(idx(l));
227 end
228 self.design(i).weight = w;
229 self.design(i).dists = dists;
230 end
231 end
232
233 function method = getUQMethod(self)
234 % METHOD = GETUQMETHOD()
235 % Resolve the discretization method from the solver options.
236 %
237 % 'default' keeps the historical behaviour: discrete Priors are
238 % expanded as given, continuous Priors are discretized by
239 % quadrature.
240 method = 'quadrature';
241 if isfield(self.options, 'method') && ~isempty(self.options.method)
242 m = char(self.options.method);
243 switch m
244 case {'default', 'discrete', 'quadrature'}
245 method = 'quadrature';
246 case 'montecarlo'
247 method = 'montecarlo';
248 otherwise
249 line_error(mfilename, sprintf('Unknown UQ method: %s', m));
250 end
251 end
252 end
253
254 function n = getUQNodes(self)
255 % N = GETUQNODES()
256 % Number of nodes per continuous Prior, from options.samples.
257 n = 11;
258 if isfield(self.options, 'samples') && ~isempty(self.options.samples)
259 n = round(self.options.samples);
260 end
261 if n < 1
262 line_error(mfilename, 'options.samples must be at least 1');
263 end
264 end
265
266 function hasPrior = hasPriorDistribution(self)
267 % HASPRIOR = HASPRIORDISTRIBUTION()
268 % Return true if model contains a Prior distribution
269 hasPrior = ~isempty(self.priorInfo);
270 end
271
272 function n = getNumAlternatives(self)
273 % N = GETNUMALTERNATIVES()
274 % Return number of design points (1 if no Prior)
275 if isempty(self.design)
276 self.buildDesign();
277 end
278 n = length(self.design);
279 end
280
281 function probs = getProbabilities(self)
282 % PROBS = GETPROBABILITIES()
283 % Return vector of design-point weights
284 if isempty(self.design)
285 self.buildDesign();
286 end
287 probs = [self.design.weight];
288 end
289
290 function E = getNumberOfModels(self)
291 % E = GETNUMBEROFMODELS()
292 % Return number of ensemble models (design points)
293 %
294 % Overrides EnsembleSolver to return count based on the design,
295 % since ensemble is not populated until init().
296 if ~isempty(self.ensemble)
297 E = length(self.ensemble);
298 else
299 E = self.getNumAlternatives();
300 end
301 end
302
303 %% EnsembleSolver abstract method implementations
304
305 function init(self)
306 % INIT Initialize the UQ solver
307 %
308 % Expands the model into a family of concrete models,
309 % one for each Prior alternative.
310
311 if isempty(self.design)
312 self.buildDesign();
313 end
314 line_debug('UQ init: expanding model into %d alternatives', length(self.design));
315 if isempty(self.priorInfo)
316 % No Prior - just use the original model
317 self.ensemble = {self.model};
318 self.solvers{1} = self.solverFactory(self.model);
319 return;
320 end
321
322 n = length(self.design);
323 L = length(self.priorInfo);
324 self.ensemble = cell(1, n);
325 self.solvers = cell(1, n);
326
327 for i = 1:n
328 % Deep copy the model
329 modelCopy = self.originalModel.copy();
330
331 nodes = modelCopy.getNodes();
332 classes = modelCopy.getClasses();
333
334 % Replace every Prior with its concrete alternative at this
335 % design point
336 for l = 1:L
337 node = nodes{self.priorInfo(l).nodeIdx};
338 class = classes{self.priorInfo(l).classIdx};
339 concreteDist = self.design(i).dists{l};
340
341 if strcmp(self.priorInfo(l).type, 'service')
342 node.setService(class, concreteDist);
343 elseif strcmp(self.priorInfo(l).type, 'arrival')
344 node.setArrival(class, concreteDist);
345 end
346 end
347
348 % Rename model to indicate alternative
349 modelCopy.name = sprintf('%s_alt%d', self.originalModel.name, i);
350
351 self.ensemble{i} = modelCopy;
352 self.solvers{i} = self.solverFactory(modelCopy);
353 end
354 end
355
356 function pre(self, it)
357 % PRE Pre-iteration operations (no-op for UQ)
358 % UQ only needs a single iteration
359 end
360
361 function [result, runtime] = analyze(self, it, e)
362 % ANALYZE Run solver for ensemble model e
363 %
364 % @param it Iteration number
365 % @param e Ensemble model index
366 % @return result Solver result structure
367 % @return runtime Solver execution time
368
369 T0 = tic;
370 solver = self.solvers{e};
371 solver.runAnalyzer();
372 result = solver.result;
373 runtime = toc(T0);
374 end
375
376 function post(self, it)
377 % POST Post-iteration operations
378 %
379 % Aggregates results from all ensemble models using prior weights.
380
381 self.aggregateResults();
382 end
383
384 function finish(self)
385 % FINISH Finalization (no-op for UQ)
386 end
387
388 function bool = converged(self, it)
389 % CONVERGED Check convergence
390 %
391 % UQ converges after the first iteration (it >= 1).
392 bool = (it >= 1);
393 end
394
395 function [QN, UN, RN, TN, AN, WN] = getEnsembleAvg(self)
396 % GETENSEMBLEAVG Get per-model average metrics
397 %
398 % Returns cell arrays with metrics from each ensemble model.
399
400 n = self.getNumberOfModels();
401 QN = cell(1, n);
402 UN = cell(1, n);
403 RN = cell(1, n);
404 TN = cell(1, n);
405 AN = cell(1, n);
406 WN = cell(1, n);
407
408 for e = 1:n
409 if ~isempty(self.results) && size(self.results, 2) >= e && ~isempty(self.results{1, e})
410 res = self.results{1, e};
411 if isfield(res, 'Avg')
412 if isfield(res.Avg, 'Q'), QN{e} = res.Avg.Q; end
413 if isfield(res.Avg, 'U'), UN{e} = res.Avg.U; end
414 if isfield(res.Avg, 'R'), RN{e} = res.Avg.R; end
415 if isfield(res.Avg, 'T'), TN{e} = res.Avg.T; end
416 if isfield(res.Avg, 'A'), AN{e} = res.Avg.A; end
417 if isfield(res.Avg, 'W'), WN{e} = res.Avg.W; end
418 end
419 end
420 end
421 end
422
423 %% Result aggregation methods
424
425 function aggregateResults(self)
426 % AGGREGATERESULTS Compute prior-weighted aggregate metrics
427
428 if isempty(self.results)
429 return;
430 end
431
432 n = self.getNumberOfModels();
433 probs = self.getProbabilities();
434
435 % Initialize aggregated result
436 self.aggregatedResult = struct();
437 self.aggregatedResult.solver = 'UQ';
438 self.aggregatedResult.Avg = struct();
439
440 % Get dimensions from first result
441 firstResult = [];
442 for e = 1:n
443 if ~isempty(self.results{1, e})
444 firstResult = self.results{1, e};
445 break;
446 end
447 end
448
449 if isempty(firstResult) || ~isfield(firstResult, 'Avg')
450 return;
451 end
452
453 % Aggregate each metric field
454 fields = {'Q', 'U', 'R', 'T', 'A', 'W', 'C', 'X'};
455 for f = 1:length(fields)
456 fname = fields{f};
457 if isfield(firstResult.Avg, fname) && ~isempty(firstResult.Avg.(fname))
458 self.aggregatedResult.Avg.(fname) = zeros(size(firstResult.Avg.(fname)));
459 for e = 1:n
460 if ~isempty(self.results{1, e}) && ...
461 isfield(self.results{1, e}, 'Avg') && ...
462 isfield(self.results{1, e}.Avg, fname) && ...
463 ~isempty(self.results{1, e}.Avg.(fname))
464 self.aggregatedResult.Avg.(fname) = self.aggregatedResult.Avg.(fname) + ...
465 probs(e) * self.results{1, e}.Avg.(fname);
466 end
467 end
468 end
469 end
470 end
471
472 %% Override NetworkSolver methods for aggregated results
473
474 function [QN, UN, RN, TN, AN, WN] = getAvg(self, varargin)
475 % GETAVG Return prior-weighted average metrics
476 %
477 % Returns aggregated metrics weighted by prior probabilities.
478
479 % Run solver if not done
480 if isempty(self.results)
481 self.iterate();
482 end
483
484 QN = []; UN = []; RN = []; TN = []; AN = []; WN = [];
485 if ~isempty(self.aggregatedResult) && isfield(self.aggregatedResult, 'Avg')
486 if isfield(self.aggregatedResult.Avg, 'Q'), QN = self.aggregatedResult.Avg.Q; end
487 if isfield(self.aggregatedResult.Avg, 'U'), UN = self.aggregatedResult.Avg.U; end
488 if isfield(self.aggregatedResult.Avg, 'R'), RN = self.aggregatedResult.Avg.R; end
489 if isfield(self.aggregatedResult.Avg, 'T'), TN = self.aggregatedResult.Avg.T; end
490 if isfield(self.aggregatedResult.Avg, 'A'), AN = self.aggregatedResult.Avg.A; end
491 if isfield(self.aggregatedResult.Avg, 'W'), WN = self.aggregatedResult.Avg.W; end
492 end
493 end
494
495 function AvgTable = getAvgTable(self, varargin)
496 % GETAVGTABLE Return prior-weighted average table
497 %
498 % Returns a table of aggregated metrics weighted by prior probabilities.
499
500 % Run solver if not done
501 if isempty(self.results)
502 self.iterate();
503 end
504
505 [QN, UN, RN, TN, AN, WN] = self.getAvg();
506
507 % Get model structure
508 sn = self.originalModel.getStruct(false);
509 M = sn.nstations;
510 K = sn.nclasses;
511
512 % Build table data
513 Station = {};
514 JobClass = {};
515 QLen = [];
516 Util = [];
517 RespT = [];
518 ResidT = [];
519 ArvR = [];
520 Tput = [];
521
522 for ist = 1:M
523 for k = 1:K
524 Q_val = 0; U_val = 0; R_val = 0; T_val = 0; A_val = 0; W_val = 0;
525
526 if ~isempty(QN), Q_val = QN(ist, k); end
527 if ~isempty(UN), U_val = UN(ist, k); end
528 if ~isempty(RN), R_val = RN(ist, k); end
529 if ~isempty(TN), T_val = TN(ist, k); end
530 if ~isempty(AN), A_val = AN(ist, k); end
531 if ~isempty(WN), W_val = WN(ist, k); end
532
533 % Only include rows with non-zero values
534 if Q_val > 0 || U_val > 0 || T_val > 0
535 Station{end+1, 1} = sn.nodenames{sn.stationToNode(ist)};
536 JobClass{end+1, 1} = sn.classnames{k};
537 QLen(end+1, 1) = Q_val;
538 Util(end+1, 1) = U_val;
539 RespT(end+1, 1) = R_val;
540 ResidT(end+1, 1) = W_val;
541 ArvR(end+1, 1) = A_val;
542 Tput(end+1, 1) = T_val;
543 end
544 end
545 end
546
547 if isempty(Station)
548 AvgTable = table();
549 return;
550 end
551
552 Station = categorical(Station);
553 JobClass = categorical(JobClass);
554
555 AvgTable = table(Station, JobClass, QLen, Util, RespT, ResidT, ArvR, Tput);
556 end
557
558 %% UQ-specific result methods
559
560 function ptable = getPosteriorTable(self)
561 % GETPOSTERIORTABLE Return table with per-alternative results
562 %
563 % Returns a table showing metrics for each Prior alternative
564 % along with its probability.
565
566 % Run solver if not done
567 if isempty(self.results)
568 self.iterate();
569 end
570
571 probs = self.getProbabilities();
572 n = self.getNumberOfModels();
573 sn = self.originalModel.getStruct(false);
574 M = sn.nstations;
575 K = sn.nclasses;
576
577 % Build table data
578 Alternative = [];
579 Probability = [];
580 Station = {};
581 JobClass = {};
582 QLen = [];
583 Util = [];
584 RespT = [];
585 Tput = [];
586
587 for alt = 1:n
588 if isempty(self.results{1, alt})
589 continue;
590 end
591 res = self.results{1, alt};
592 if ~isfield(res, 'Avg')
593 continue;
594 end
595
596 for ist = 1:M
597 for k = 1:K
598 Q_val = 0; U_val = 0; R_val = 0; T_val = 0;
599
600 if isfield(res.Avg, 'Q') && ~isempty(res.Avg.Q)
601 Q_val = res.Avg.Q(ist, k);
602 end
603 if isfield(res.Avg, 'U') && ~isempty(res.Avg.U)
604 U_val = res.Avg.U(ist, k);
605 end
606 if isfield(res.Avg, 'R') && ~isempty(res.Avg.R)
607 R_val = res.Avg.R(ist, k);
608 end
609 if isfield(res.Avg, 'T') && ~isempty(res.Avg.T)
610 T_val = res.Avg.T(ist, k);
611 end
612
613 % Only include rows with non-zero values
614 if Q_val > 0 || U_val > 0 || T_val > 0
615 Alternative(end+1, 1) = alt;
616 Probability(end+1, 1) = probs(alt);
617 Station{end+1, 1} = sn.nodenames{sn.stationToNode(ist)};
618 JobClass{end+1, 1} = sn.classnames{k};
619 QLen(end+1, 1) = Q_val;
620 Util(end+1, 1) = U_val;
621 RespT(end+1, 1) = R_val;
622 Tput(end+1, 1) = T_val;
623 end
624 end
625 end
626 end
627
628 if isempty(Alternative)
629 ptable = table();
630 return;
631 end
632
633 Station = categorical(Station);
634 JobClass = categorical(JobClass);
635
636 ptable = table(Alternative, Probability, Station, JobClass, QLen, Util, RespT, Tput);
637 end
638
639 function empDist = getPosteriorDist(self, metric, station, class)
640 % GETPOSTERIORDIST Return empirical distribution of a metric
641 %
642 % Returns an EmpiricalCDF object representing the posterior
643 % distribution of the specified metric across Prior alternatives.
644 %
645 % @param metric Metric name: 'Q', 'U', 'R', 'T', 'A', 'W'
646 % @param station Station node or station index
647 % @param class JobClass object or class index
648 % @return empDist EmpiricalCDF object
649
650 % Run solver if not done
651 if isempty(self.results)
652 self.iterate();
653 end
654
655 probs = self.getProbabilities();
656 n = self.getNumberOfModels();
657
658 % Get station index
659 if isnumeric(station)
660 ist = station;
661 elseif isa(station, 'Station')
662 ist = station.stationIndex;
663 else
664 line_error(mfilename, 'station must be a Station object or numeric index');
665 end
666
667 % Get class index
668 if isnumeric(class)
669 k = class;
670 elseif isa(class, 'JobClass')
671 k = class.index;
672 else
673 line_error(mfilename, 'class must be a JobClass object or numeric index');
674 end
675
676 % Extract metric values for each alternative
677 values = zeros(n, 1);
678 for e = 1:n
679 if ~isempty(self.results{1, e}) && isfield(self.results{1, e}, 'Avg')
680 res = self.results{1, e}.Avg;
681 if isfield(res, metric) && ~isempty(res.(metric))
682 values(e) = res.(metric)(ist, k);
683 else
684 values(e) = NaN;
685 end
686 else
687 values(e) = NaN;
688 end
689 end
690
691 % Build empirical CDF
692 % Sort values and corresponding probabilities
693 [sortedVals, sortIdx] = sort(values);
694 sortedProbs = probs(sortIdx);
695 cdfVals = cumsum(sortedProbs);
696
697 % Create data matrix for EmpiricalCDF: [CDF_value, X_value]
698 cdfData = [cdfVals(:), sortedVals(:)];
699
700 % Create EmpiricalCDF object
701 empDist = EmpiricalCDF(cdfData);
702 end
703
704 %% Required Solver abstract methods
705
706 function runtime = runAnalyzer(self, options)
707 % RUNANALYZER Run the UQ analysis
708 %
709 % @param options Solver options (optional)
710 % @return runtime Total runtime in seconds
711
712 T0 = tic;
713 if nargin >= 2 && ~isempty(options)
714 self.setOptions(options);
715 end
716 line_debug('UQ solver starting: nalternatives=%d', self.getNumAlternatives());
717 line_debug('Default method: using UQ ensemble analysis\n');
718 self.iterate();
719 runtime = toc(T0);
720 end
721
722 function sn = getStruct(self)
723 % GETSTRUCT Return model structure
724 %
725 % Returns the structure of the original model.
726 sn = self.originalModel.getStruct(false);
727 end
728
729 %% Static methods
730
731 function [allMethods] = listValidMethods(self)
732 % LISTVALIDMETHODS Return valid methods
733 allMethods = {'default', 'discrete', 'quadrature', 'montecarlo'};
734 end
735
736 %% Uncertainty summaries
737
738 function [m, v] = getMoments(self, metric, station, class)
739 % [M, V] = GETMOMENTS(METRIC, STATION, CLASS)
740 % Weighted mean and variance of a metric over the design.
741 %
742 % The mean is the unconditional expectation of Trivedi and Bobbio
743 % (2017), Eq. (3.68); the variance is the second moment of the
744 % same weighting, as derived for the cold-standby case in their
745 % Sec. 8.5.1. Both are exact for a discrete Prior and quadrature-
746 % or sample-approximate for a continuous one.
747 %
748 % @param metric Metric name ('Q','U','R','T','A','W')
749 % @param station Station object or index
750 % @param class Class object or index
751 % @return m Weighted mean
752 % @return v Weighted variance
753
754 [vals, w] = self.getSamples(metric, station, class);
755 m = sum(w .* vals);
756 v = sum(w .* (vals - m).^2);
757 end
758
759 function ci = getCredibleInterval(self, metric, station, class, level)
760 % CI = GETCREDIBLEINTERVAL(METRIC, STATION, CLASS, LEVEL)
761 % Equal-tailed credible interval from the weighted empirical CDF.
762 %
763 % @param metric Metric name ('Q','U','R','T','A','W')
764 % @param station Station object or index
765 % @param class Class object or index
766 % @param level Coverage level in (0,1), default 0.95
767 % @return ci Two-element vector [lower, upper]
768
769 if nargin < 5 || isempty(level)
770 level = 0.95;
771 end
772 if level <= 0 || level >= 1
773 line_error(mfilename, 'level must lie strictly between 0 and 1');
774 end
775
776 [vals, w] = self.getSamples(metric, station, class);
777 [vals, ord] = sort(vals);
778 w = w(ord);
779 cw = cumsum(w) / sum(w);
780
781 alpha = (1 - level) / 2;
782 lo = vals(find(cw >= alpha, 1, 'first'));
783 hi = vals(find(cw >= 1 - alpha, 1, 'first'));
784 if isempty(lo), lo = vals(1); end
785 if isempty(hi), hi = vals(end); end
786 ci = [lo, hi];
787 end
788
789 function [vals, w] = getSamples(self, metric, station, class)
790 % [VALS, W] = GETSAMPLES(METRIC, STATION, CLASS)
791 % Per-design-point metric values and their weights.
792
793 if isempty(self.results)
794 self.iterate();
795 end
796
797 ist = self.resolveStationIndex(station);
798 k = self.resolveClassIndex(class);
799
800 n = self.getNumberOfModels();
801 w = self.getProbabilities();
802 vals = zeros(1, n);
803 for e = 1:n
804 res = self.results{1, e};
805 if isempty(res) || ~isfield(res, 'Avg') || ~isfield(res.Avg, metric) || isempty(res.Avg.(metric))
806 line_error(mfilename, sprintf('Metric %s unavailable for design point %d', metric, e));
807 end
808 vals(e) = res.Avg.(metric)(ist, k);
809 end
810 end
811
812 function ist = resolveStationIndex(self, station)
813 % IST = RESOLVESTATIONINDEX(STATION)
814 % Accept a Station object or a numeric index.
815 if isnumeric(station)
816 ist = station;
817 elseif isa(station, 'Station')
818 ist = station.stationIndex;
819 else
820 line_error(mfilename, 'station must be a Station object or numeric index');
821 end
822 end
823
824 function k = resolveClassIndex(self, class)
825 % K = RESOLVECLASSINDEX(CLASS)
826 % Accept a JobClass object or a numeric index.
827 if isnumeric(class)
828 k = class;
829 elseif isa(class, 'JobClass')
830 k = class.index;
831 else
832 line_error(mfilename, 'class must be a JobClass object or numeric index');
833 end
834 end
835 end
836
837 methods (Static)
838 function idx = unrankIndex(i, counts)
839 % IDX = UNRANKINDEX(I, COUNTS)
840 % Map the linear index I in 1..prod(COUNTS) to a subscript vector
841 % over a mixed-radix grid, first coordinate varying fastest.
842 L = length(counts);
843 idx = zeros(1, L);
844 rem = i - 1;
845 for l = 1:L
846 idx(l) = mod(rem, counts(l)) + 1;
847 rem = floor(rem / counts(l));
848 end
849 end
850
851 function featSupported = getFeatureSet()
852 % GETFEATURESET Return supported features
853 featSupported = SolverFeatureSet;
854 featSupported.setTrue('Prior');
855 end
856
857 function [bool, featSupported] = supports(model)
858 % SUPPORTS Check if model is supported
859 bool = true;
860 featSupported = UQ.getFeatureSet();
861 end
862
863 function options = defaultOptions()
864 % DEFAULTOPTIONS Return default options
865 options = Solver.defaultOptions();
866 options.iter_max = 1; % Single iteration for UQ
867 % Nodes per continuous Prior. Each node is a full solver run, so
868 % the simulation-oriented default of Solver.defaultOptions is far
869 % too large here.
870 options.samples = 11;
871 end
872 end
873end
Definition fjtag.m:157
Definition Station.m:245