LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_uq.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_SOLVERS_UQ_SOLVER_UQ_H
6#define LINE_SOLVERS_UQ_SOLVER_UQ_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * SolverUQ: uncertainty quantification by expansion over a Prior.
12 *
13 * Port of `matlab/src/solvers/UQ/@@UQ/UQ.m`. The model carries one or more
14 * `Prior` distributions (lang/prior.h); UQ reduces them to a set of weighted
15 * DESIGN POINTS, each of which is a concrete model with every Prior replaced by
16 * one alternative, solves each with an ordinary solver, and reports the
17 * prior-weighted expectation of every metric together with the per-point
18 * results the expectation was formed from.
19 *
20 * WHAT THE WEIGHTS MEAN. E[Q] = sum_l w_l Q(theta_l) is the unconditional
21 * expectation of Trivedi and Bobbio (2017), Eq. (3.68): an average over MODELS,
22 * not over jobs. Its spread -- `uq_moments`, `uq_credible_interval` -- is the
23 * epistemic uncertainty in the answer, and it is the reason the per-point table
24 * is kept rather than reduced away: a mean of 4.2 over points at 1.1 and 12.4
25 * is a different statement from a mean of 4.2 over points at 4.1 and 4.3, and
26 * only the design carries the difference.
27 *
28 * THE DESIGN, and why it is a tensor product. Each Prior is discretized on its
29 * own and the design is the product of the per-Prior alternative sets, so the
30 * joint weight is the product of the marginal weights. That is the
31 * product-density case of f(theta_1, ..., theta_l) in Eq. (3.67) and it ASSUMES
32 * THE PRIORS ARE INDEPENDENT; a joint prior over several parameters is not
33 * expressible here, in this port or in the reference. The size is capped at
34 * `kMaxDesignPoints` because a design point is a full solver run, and beyond
35 * the cap the Monte Carlo design -- whose cost does not grow with the number of
36 * Priors -- is the right tool. The cap REFUSES rather than truncating: a design
37 * silently cut to 4096 of 20000 points would report an expectation against a
38 * prior nobody wrote.
39 *
40 * WHAT SOLVES A DESIGN POINT is supplied by the caller as a `UqStageSolver`,
41 * the C++ spelling of the reference's `solverFactory` argument (`UQ(model,
42 * @@SolverMVA)`). There is no default: the inner solver decides both the
43 * accuracy and the admissible feature set of every number reported here, and
44 * choosing one silently would answer a question the caller did not ask.
45 * `uq_dispatch.h` builds one from a solver name.
46 *
47 * THE FEATURE GATE IS THE INNER SOLVER'S. UQ itself declares only `Prior`
48 * (`uq_feature_set`) and applies no feature gate of its own; what `UQ.supports`
49 * decides is only whether the model carries a Prior at all. The real check
50 * happens per design point, inside the stage solver, on a model from which the
51 * Prior has already been removed; that is what makes "SolverMVA cannot solve
52 * this model" reach the caller as SolverMVA's own refusal instead of a UQ
53 * paraphrase of it.
54 */
55
56#include <algorithm>
57#include <cstddef>
58#include <functional>
59#include <string>
60#include <utility>
61#include <vector>
62
65#include "line/lang/prior.h"
69#include "line/num/number.h"
71#include "line/util/error.h"
72
73namespace line {
74namespace uq {
75
76/**
77 * The cap on the tensor-product design, MATLAB `UQ.MaxDesignPoints`.
78 *
79 * A design point is one full solver run, so this bounds the cost of a
80 * quadrature design over several Priors.
81 */
82inline constexpr std::size_t kMaxDesignPoints = 4096;
83
84/** `UQ.defaultOptions` plus the stream the Monte Carlo design draws from. */
85struct UqOptions {
86 /**
87 * `default` | `discrete` | `quadrature` | `montecarlo`, MATLAB
88 * `UQ.listValidMethods`. The first three all resolve to the quadrature
89 * design: a discrete Prior is expanded as given, a continuous one is placed
90 * at stratum medians. Only `montecarlo` draws.
91 */
92 std::string method = "default";
93 /**
94 * Nodes per continuous Prior, or design points under `montecarlo`; the
95 * reference's `options.samples`, defaulted to 11 rather than to the
96 * simulation-oriented default of `Solver.defaultOptions` because each node
97 * is a full solver run.
98 */
100 /** The Monte Carlo stream; unread by a quadrature design, which draws nothing. */
101 unsigned long seed = 23000;
102};
103
104/** Where a Prior sits in the model, MATLAB's `priorInfo` entry. */
105template <class T>
106struct PriorSite {
107 std::size_t node = 0; ///< 1-based node index
108 std::size_t station = 0; ///< 1-based station index
109 std::size_t cls = 0; ///< 1-based class index
110 /** True at a Source, where the Prior is an ARRIVAL process, not a service one. */
111 bool arrival = false;
112 /** The Prior itself, copied out of the service table. */
114};
115
116/** One design point: a concrete distribution for every Prior, and its weight. */
117template <class T>
120 std::vector<lang::Distrib<T>> dists; ///< one per site, in site order
121};
122
123/** What `solver_uq_run_analyzer` returns. */
124template <class T>
126 /** The prior-weighted expectation of every metric, (nstations x nclasses). */
128 /** The result at each design point, in design order. */
129 std::vector<mva::AvgResult<T>> points;
130 /** The design weights, summing to 1. */
131 std::vector<T> weights;
132 /** The alternatives each point substituted, one per site. */
133 std::vector<UqDesignPoint<T>> design;
134 /** Where the Priors were found. */
135 std::vector<PriorSite<T>> sites;
136 /** The RESOLVED discretization method: `quadrature` or `montecarlo`. */
137 std::string method;
138 /** The options the design was built with; `uq_interval` reads `samples`. */
140};
141
142/**
143 * `UQ.getUQMethod`: resolve the discretization method.
144 *
145 * `default` and `discrete` are aliases of `quadrature` and not separate rules:
146 * a discrete Prior is already exact, so expanding it as given IS the quadrature
147 * design for it, and the name survives only because the reference lists it.
148 */
149inline std::string uq_resolve_method(const std::string& m) {
150 if (m.empty() || m == "default" || m == "discrete" || m == "quadrature") return "quadrature";
151 if (m == "montecarlo") return "montecarlo";
152 throw InputError("SolverUQ: unknown method '" + m +
153 "'; the valid names are default, discrete, quadrature and montecarlo");
154}
155
156/** `UQ.listValidMethods`. */
157inline std::vector<std::string> uq_list_valid_methods() {
158 return std::vector<std::string>{"default", "discrete", "quadrature", "montecarlo"};
159}
160
161/**
162 * `UQ.getFeatureSet`: the one construct UQ adds, and nothing else.
163 *
164 * IT IS NOT USED AS A FEATURE GATE, here or in the reference: UQ solves nothing
165 * itself, so the set of models it admits is the inner solver's, applied per
166 * design point once the Prior is gone. It is declared because the registry is
167 * the vocabulary in which a capability is stated, and "SolverUQ is the solver
168 * that understands Prior" is a statement worth being able to make.
169 *
170 * THE ONE THING `UQ.supports` DOES DECIDE is whether the model carries a Prior
171 * at all: a model with no uncertain parameter is not a UQ model, and its
172 * posterior is a single design point equal to the point estimate the inner
173 * solver already returns. `has_prior_distribution()` is that test here. MATLAB
174 * used to return true unconditionally -- which made SolverAUTO offer every
175 * 'uq.*' method name on an ordinary network -- and now gates on `UQ.modelHasPrior`,
176 * matching the JAR (`detectPrior() != null`) and native python
177 * (`hasPriorDistribution`).
178 */
181 f.set(qn::Feature::Prior);
182 return f;
183}
184
185/**
186 * `UQ.detectPriors`: find every Prior, in node order and then class order.
187 *
188 * SERVICE AT A QUEUE OR DELAY, ARRIVAL AT A SOURCE, which is the reference's own
189 * pair of branches. A Prior anywhere else -- a Cache's read process, a
190 * Transition's firing law -- is REFUSED by name rather than skipped: the
191 * reference's loop would ignore it and then solve a model in which the
192 * uncertainty silently became the mixture moments, which is a confident answer
193 * to a question nobody asked.
194 */
195template <class T>
196std::vector<PriorSite<T>> uq_detect_priors(const qn::NetworkStruct<T>& sn) {
197 std::vector<PriorSite<T>> sites;
198 for (std::size_t nd = 1; nd <= sn.nodes.size(); ++nd) {
199 const std::size_t ist = sn.nodes[nd - 1].station;
200 if (ist == 0 || ist > sn.service.size()) continue;
201 const qn::NodeType ty = sn.nodes[nd - 1].nodetype;
202 const bool servicer = (ty == qn::NodeType::Queue || ty == qn::NodeType::Delay);
203 const bool source = (ty == qn::NodeType::Source);
204 for (std::size_t r = 1; r <= sn.service[ist - 1].size(); ++r) {
205 const lang::Distrib<T>& d = sn.service[ist - 1][r - 1];
206 if (!d.is_prior()) continue;
207 if (!servicer && !source)
208 throw UnsupportedError(
209 "SolverUQ: node '" + sn.nodes[nd - 1].name +
210 "' carries a Prior, but a Prior is expanded only where the reference expands "
211 "one: the service process of a Queue or a Delay, or the arrival process of a "
212 "Source");
213 PriorSite<T> s;
214 s.node = nd;
215 s.station = ist;
216 s.cls = r;
217 s.arrival = source;
218 s.prior = d;
219 sites.push_back(s);
220 }
221 }
222 return sites;
223}
224
225namespace detail {
226
227/**
228 * `UQ.unrankIndex`: the linear index i in [0, prod(counts)) as a subscript
229 * vector over a mixed-radix grid, FIRST COORDINATE VARYING FASTEST.
230 *
231 * The order is the reference's and is kept because it is what makes design
232 * point k the same model in both codebases; any other unranking would permute
233 * the per-point table while leaving the expectation unchanged, which is the
234 * hardest kind of divergence to notice.
235 */
236inline std::vector<std::size_t> unrank_index(std::size_t i,
237 const std::vector<std::size_t>& counts) {
238 std::vector<std::size_t> idx(counts.size(), 0);
239 std::size_t rem = i;
240 for (std::size_t l = 0; l < counts.size(); ++l) {
241 idx[l] = rem % counts[l];
242 rem /= counts[l];
243 }
244 return idx;
245}
246
247} // namespace detail
248
249/**
250 * `UQ.buildDesign`: reduce the detected Priors to weighted design points.
251 *
252 * With no Prior there is ONE point of weight 1 substituting nothing, so the
253 * original model is solved once and the expectation is that solve -- the
254 * degenerate case the reference also carries, and the reason a model without a
255 * Prior is not an error here.
256 */
257template <class T>
258std::vector<UqDesignPoint<T>> uq_build_design(const std::vector<PriorSite<T>>& sites,
259 const UqOptions& opt) {
260 const std::string method = uq_resolve_method(opt.method);
261 const std::size_t n = opt.samples;
262 if (n < 1) throw InputError("SolverUQ: options.samples must be at least 1");
263 std::vector<UqDesignPoint<T>> design;
264 if (sites.empty()) {
265 design.push_back(UqDesignPoint<T>());
266 return design;
267 }
268 const std::size_t L = sites.size();
269 lang::PriorRng rng(opt.seed);
270
271 if (method == "montecarlo") {
272 // ALL PRIORS ARE DRAWN JOINTLY at each point, so the cost is n runs
273 // whatever L is; that independence from L is the whole reason the
274 // method exists beside the tensor product.
275 const T w = T(num_traits<T>::from_int(1) / num_traits<T>::from_int(static_cast<long>(n)));
276 for (std::size_t i = 0; i < n; ++i) {
278 p.weight = w;
279 for (std::size_t l = 0; l < L; ++l) {
280 const lang::PriorDesign<T> g =
281 lang::prior_discretize(sites[l].prior, 1, "montecarlo", rng);
282 p.dists.push_back(g.dists[0]);
283 }
284 design.push_back(p);
285 }
286 return design;
287 }
288
289 std::vector<lang::PriorDesign<T>> marg(L);
290 std::vector<std::size_t> counts(L, 0);
291 std::size_t total = 1;
292 for (std::size_t l = 0; l < L; ++l) {
293 marg[l] = lang::prior_discretize(sites[l].prior, n, "quadrature", rng);
294 counts[l] = marg[l].dists.size();
295 if (counts[l] == 0) throw NumericError("SolverUQ: a Prior discretized to no alternative");
296 // SATURATE RATHER THAN OVERFLOW: the product of a dozen 11-node Priors
297 // does not fit a size_t, and a wrapped one would pass the cap check.
298 if (total > kMaxDesignPoints / counts[l]) {
299 total = kMaxDesignPoints + 1;
300 break;
301 }
302 total *= counts[l];
303 }
304 if (total > kMaxDesignPoints)
305 throw UnsupportedError(
306 "SolverUQ: the tensor-product design has " + std::to_string(total) +
307 " points, above the limit of " + std::to_string(kMaxDesignPoints) +
308 "; use method 'montecarlo', whose cost does not grow with the number of Priors, or "
309 "lower options.samples");
310
311 for (std::size_t i = 0; i < total; ++i) {
312 const std::vector<std::size_t> idx = detail::unrank_index(i, counts);
315 for (std::size_t l = 0; l < L; ++l) {
316 p.dists.push_back(marg[l].dists[idx[l]]);
317 p.weight = T(p.weight * marg[l].weights[idx[l]]);
318 }
319 design.push_back(p);
320 }
321 return design;
322}
323
324/**
325 * What solves one design point: the C++ spelling of `@(m) SolverXXX(m)`.
326 *
327 * It takes the REFRESHED struct of the expanded model and returns the same
328 * `AvgResult` every Network solver in this port returns, so the aggregation
329 * below is one loop rather than one loop per solver.
330 */
331template <class T>
332using UqStageSolver = std::function<mva::AvgResult<T>(const qn::NetworkStruct<T>&)>;
333
334namespace detail {
335
336/** M += w * A, sizing M from A on first use. */
337template <class T>
338void accumulate(Matrix<T>& M, const Matrix<T>& A, const T& w) {
339 if (A.empty()) return;
340 if (M.empty()) M = Matrix<T>(A.rows(), A.cols(), num_traits<T>::from_int(0));
341 if (M.rows() != A.rows() || M.cols() != A.cols())
342 throw NumericError("SolverUQ: two design points reported metrics of different shape");
343 for (std::size_t i = 0; i < A.rows(); ++i)
344 for (std::size_t j = 0; j < A.cols(); ++j) M(i, j) = T(M(i, j) + w * A(i, j));
345}
346
347template <class T>
348void accumulate(std::vector<T>& v, const std::vector<T>& a, const T& w) {
349 if (a.empty()) return;
350 if (v.empty()) v.assign(a.size(), num_traits<T>::from_int(0));
351 if (v.size() != a.size())
352 throw NumericError("SolverUQ: two design points reported vectors of different length");
353 for (std::size_t i = 0; i < a.size(); ++i) v[i] = T(v[i] + w * a[i]);
354}
355
356} // namespace detail
357
358/**
359 * `UQ.aggregateResults`: the prior-weighted expectation of the solved points.
360 *
361 * Free rather than private to the solver below because it is a pure function of
362 * `points` and `weights`, and `post()` is not the only caller that has those: a
363 * host that solved the design itself -- one point per process, one per machine --
364 * aggregates with this and needs nothing else from the class.
365 */
366template <class T>
368 sol.avg = mva::AvgResult<T>();
369 // The expectation, metric by metric. CN and XN are aggregated beside the
370 // station matrices because the reference's field list carries them.
371 for (std::size_t e = 0; e < sol.points.size(); ++e) {
372 const mva::AvgResult<T>& r = sol.points[e];
373 const T w = sol.weights[e];
374 detail::accumulate(sol.avg.QN, r.QN, w);
375 detail::accumulate(sol.avg.UN, r.UN, w);
376 detail::accumulate(sol.avg.RN, r.RN, w);
377 detail::accumulate(sol.avg.TN, r.TN, w);
378 detail::accumulate(sol.avg.AN, r.AN, w);
379 detail::accumulate(sol.avg.WN, r.WN, w);
380 detail::accumulate(sol.avg.CN, r.CN, w);
381 detail::accumulate(sol.avg.XN, r.XN, w);
382 }
383 if (sol.points.empty()) return;
384 // THE AGGREGATE NAMES THE ALGORITHM ONLY IF EVERY POINT RAN THE SAME ONE. A
385 // method that resolves per model -- SolverMVA's `default` picks `exact` on
386 // one alternative and an AMVA on another -- would otherwise have the
387 // expectation reported under one point's name.
388 sol.avg.method = sol.points[0].method;
389 sol.avg.actualmethod = sol.points[0].actualmethod;
390 for (const mva::AvgResult<T>& r : sol.points)
391 if (r.actualmethod != sol.avg.actualmethod) {
392 sol.avg.actualmethod = "mixed";
393 break;
394 }
395 for (std::size_t e = 0; e < sol.points.size(); ++e)
396 if (!sol.points[e].warning.empty()) {
397 sol.avg.warning =
398 "design point " + std::to_string(e + 1) + ": " + sol.points[e].warning;
399 break;
400 }
401}
402
403/**
404 * The ensemble surface of UQ: `@UQ`'s `EnsembleSolver` implementation.
405 *
406 * WHY A CLASS AND NOT ONLY `solver_uq_run_analyzer`. The reference is an
407 * `EnsembleSolver`, and the lifecycle it implements -- `init`, `pre`, `analyze`,
408 * `post`, `converged`, `finish` -- is not decoration: it is the surface a caller
409 * uses to drive the design POINT BY POINT, to inspect the ensemble before
410 * solving it, to substitute its own stage solver per point, or to distribute the
411 * points and aggregate afterwards. A single `run` function can do none of those,
412 * so the port carried the numbers of UQ without the way UQ is meant to be
413 * driven. `SolverEnv` in `env/solver_env.h` keeps the same lifecycle for the
414 * same reason.
415 *
416 * ONE DELIBERATE DEVIATION: the reference's `init` materializes the whole
417 * ensemble (`self.ensemble{i}` is a deep model copy per design point) and this
418 * one does not -- `expand(e)` builds the copy for point e on demand, because a
419 * 4096-point design would otherwise hold 4096 Networks alive to solve them one
420 * at a time. The expansion is a pure function of the design, so a caller that
421 * wants the model of point e asks for it and gets exactly what the reference's
422 * `self.ensemble{e}` holds.
423 *
424 * `net` is taken by non-const reference because the expansion COPIES it once per
425 * design point and each copy is then refreshed -- `get_struct()` re-derives
426 * rates, chains and visits, which is exactly what changing a service process
427 * requires and is why the substitution is done on the model rather than on a
428 * struct.
429 */
430template <class T>
431class SolverUq {
432public:
434 : net(&n), stage(s) {
435 if (!stage)
436 throw InputError(
437 "SolverUQ: no stage solver was given. UQ solves nothing itself; it needs the "
438 "solver that runs at each design point, the C++ spelling of UQ(model, "
439 "@SolverMVA)");
440 sol.options = o;
441 init();
442 }
443
444 /** `UQ.init`: resolve the design; the ensemble itself is expanded per point. */
445 void init() {
446 sol.method = uq_resolve_method(sol.options.method);
447 sol.sites = uq_detect_priors(net->get_struct());
448 sol.design = uq_build_design(sol.sites, sol.options);
449 sol.points.clear();
450 sol.weights.clear();
451 sol.avg = mva::AvgResult<T>();
452 }
453
454 /** `UQ.pre`: nothing to seed -- the design points are independent models. */
455 void pre(int /*it*/) {}
456
457 /**
458 * `UQ.analyze`: solve design point `e` (1-BASED, as the reference indexes it).
459 *
460 * The result is APPENDED to `points`, so driving the ensemble in order gives
461 * the same `points` vector `iterate()` builds. Calling it out of order is a
462 * caller's choice and the order of `points` then follows the calls, which is
463 * why `weights` is appended beside it rather than indexed into.
464 */
465 const mva::AvgResult<T>& analyze(int /*it*/, std::size_t e) {
466 if (e < 1 || e > sol.design.size())
467 throw InputError("SolverUQ::analyze: design point " + std::to_string(e) +
468 " is outside the design of " + std::to_string(sol.design.size()) +
469 " points");
470 qn::Network<T> copy = expand(e);
471 sol.points.push_back(stage(copy.get_struct()));
472 sol.weights.push_back(sol.design[e - 1].weight);
473 return sol.points.back();
474 }
475
476 /** `UQ.post`: the prior-weighted expectation over the points solved so far. */
477 void post(int /*it*/) { uq_aggregate(sol); }
478
479 /** `UQ.finish`: nothing to release. */
480 void finish() {}
481
482 /** `UQ.converged`: one iteration is all UQ needs, the design being fixed. */
483 bool converged(int it) const { return it >= 1; }
484
485 /** `UQ.runAnalyzer`: the whole lifecycle, in the reference's order. */
487 init();
488 int it = 1;
489 pre(it);
490 for (std::size_t e = 1; e <= sol.design.size(); ++e) analyze(it, e);
491 post(it);
492 if (!converged(it))
493 throw NumericError("SolverUQ: the design did not converge in one iteration");
494 finish();
495 return sol;
496 }
497
498 /**
499 * `self.ensemble{e}`: the model of design point `e` (1-based), Prior gone.
500 *
501 * The reference names each copy `<model>_alt<e>`; this port keeps the copy's
502 * name because a `qn::Network` name is read back by the JSON writer and the
503 * LQN bridge, and renaming it would put a name no model file carries into
504 * whatever a caller does next with the expanded model.
505 */
506 qn::Network<T> expand(std::size_t e) const {
507 if (e < 1 || e > sol.design.size())
508 throw InputError("SolverUQ::expand: design point " + std::to_string(e) +
509 " is outside the design of " + std::to_string(sol.design.size()) +
510 " points");
511 qn::Network<T> copy = *net;
512 for (std::size_t l = 0; l < sol.sites.size(); ++l) {
513 const PriorSite<T>& s = sol.sites[l];
514 if (s.arrival)
515 copy.set_arrival(s.node, s.cls, sol.design[e - 1].dists[l]);
516 else
517 copy.set_service(s.node, s.cls, sol.design[e - 1].dists[l]);
518 }
519 return copy;
520 }
521
522 /** `UQ.hasPriorDistribution`. */
523 bool has_prior_distribution() const { return !sol.sites.empty(); }
524
525 /** `UQ.getNumAlternatives`: design points, 1 when the model carries no Prior. */
526 std::size_t get_num_alternatives() const { return sol.design.size(); }
527
528 /** `UQ.getNumberOfModels`: the same count, under the EnsembleSolver's name. */
529 std::size_t get_number_of_models() const { return sol.design.size(); }
530
531 /** `UQ.getProbabilities`: the design weights, from the DESIGN not the solve. */
532 std::vector<T> get_probabilities() const {
533 std::vector<T> w;
534 w.reserve(sol.design.size());
535 for (std::size_t e = 0; e < sol.design.size(); ++e) w.push_back(sol.design[e].weight);
536 return w;
537 }
538
539 /** `UQ.getUQNodes`: nodes per continuous Prior. */
540 std::size_t get_uq_nodes() const { return sol.options.samples; }
541
542 /** `UQ.getUQMethod`: the RESOLVED design name. */
543 const std::string& get_uq_method() const { return sol.method; }
544
545 /** `UQ.getEnsembleAvg`: the per-point results, in the order they were solved. */
546 const std::vector<mva::AvgResult<T>>& get_ensemble_avg() const { return sol.points; }
547
548 /** `UQ.getAvg`: the aggregate. Valid once `post()` has run. */
549 const mva::AvgResult<T>& get_avg() const { return sol.avg; }
550
551 /** Where the Priors were found, the reference's `priorInfo`. */
552 const std::vector<PriorSite<T>>& get_prior_info() const { return sol.sites; }
553
554 /** The design itself, one entry per point. */
555 const std::vector<UqDesignPoint<T>>& get_design() const { return sol.design; }
556
557 /** Everything above in one value, which is what the CLI and the tests read. */
558 const UqSolution<T>& get_solution() const { return sol; }
559
560private:
561 qn::Network<T>* net;
562 UqStageSolver<T> stage;
563 UqSolution<T> sol;
564};
565
566/**
567 * `UQ.runAnalyzer` as a free call: expand, solve every design point, aggregate.
568 *
569 * The one-shot spelling of `SolverUq::iterate`, kept because most callers want
570 * the solution and not the lifecycle, and because it is what `line_cli.cpp` and
571 * `uq_interval_run` reach for.
572 */
573template <class T>
575 const UqOptions& opt = UqOptions()) {
576 SolverUq<T> s(net, stage, opt);
577 return s.iterate();
578}
579
580// ---------------------------------------------------------------------------
581// Posterior summaries
582// ---------------------------------------------------------------------------
583
584/** The metric matrix a name selects, MATLAB's @c res.Avg.(metric) field. */
585template <class T>
586const Matrix<T>& uq_metric_matrix(const mva::AvgResult<T>& r, const std::string& metric) {
587 if (metric == "Q") return r.QN;
588 if (metric == "U") return r.UN;
589 if (metric == "R") return r.RN;
590 if (metric == "T") return r.TN;
591 if (metric == "A") return r.AN;
592 if (metric == "W") return r.WN;
593 throw InputError("SolverUQ: unknown metric '" + metric + "'; use Q, U, R, T, A or W");
594}
595
596/**
597 * `UQ.getSamples`: the value of one metric at every design point, with weights.
598 *
599 * `ist` and `r` are 1-based, as everywhere in the readable surface of this port.
600 */
601template <class T>
602std::vector<T> uq_samples(const UqSolution<T>& sol, const std::string& metric, std::size_t ist,
603 std::size_t r) {
604 std::vector<T> vals;
605 for (std::size_t e = 0; e < sol.points.size(); ++e) {
606 const Matrix<T>& M = uq_metric_matrix(sol.points[e], metric);
607 if (M.empty() || ist == 0 || r == 0 || ist > M.rows() || r > M.cols())
608 throw InputError("SolverUQ: metric " + metric + " is unavailable at station " +
609 std::to_string(ist) + ", class " + std::to_string(r) +
610 " for design point " + std::to_string(e + 1));
611 vals.push_back(M(ist - 1, r - 1));
612 }
613 return vals;
614}
615
616/** The weighted mean and variance of a metric over the design. */
617template <class T>
622
623/**
624 * `UQ.getMoments`: the unconditional mean of Trivedi and Bobbio Eq. (3.68) and
625 * the second moment of the same weighting.
626 *
627 * Both are EXACT for a discrete Prior and quadrature- or sample-approximate for
628 * a continuous one, which is the only sense in which a variance over 11 stratum
629 * medians is a variance.
630 */
631template <class T>
632UqMoments<T> uq_moments(const UqSolution<T>& sol, const std::string& metric, std::size_t ist,
633 std::size_t r) {
634 const std::vector<T> vals = uq_samples(sol, metric, ist, r);
635 UqMoments<T> out;
636 for (std::size_t e = 0; e < vals.size(); ++e) out.mean += T(sol.weights[e] * vals[e]);
637 for (std::size_t e = 0; e < vals.size(); ++e) {
638 const T dv = T(vals[e] - out.mean);
639 out.var += T(sol.weights[e] * dv * dv);
640 }
641 return out;
642}
643
644/** The weighted empirical law of a metric, sorted ascending; MATLAB's `EmpiricalCDF`. */
645template <class T>
647 std::vector<T> values; ///< the metric at each design point, ascending
648 std::vector<T> probabilities; ///< the weight of each value, in the same order
649 std::vector<T> cdf; ///< the running sum of `probabilities`
650};
651
652/** `UQ.getPosteriorDist`: the posterior law of a metric across the design. */
653template <class T>
654UqEmpiricalCdf<T> uq_posterior_cdf(const UqSolution<T>& sol, const std::string& metric,
655 std::size_t ist, std::size_t r) {
656 const std::vector<T> vals = uq_samples(sol, metric, ist, r);
657 std::vector<std::size_t> ord(vals.size());
658 for (std::size_t i = 0; i < ord.size(); ++i) ord[i] = i;
659 std::stable_sort(ord.begin(), ord.end(),
660 [&vals](std::size_t a, std::size_t b) { return vals[a] < vals[b]; });
662 T acc = num_traits<T>::from_int(0);
663 for (std::size_t k = 0; k < ord.size(); ++k) {
664 out.values.push_back(vals[ord[k]]);
665 out.probabilities.push_back(sol.weights[ord[k]]);
666 acc += sol.weights[ord[k]];
667 out.cdf.push_back(acc);
668 }
669 return out;
670}
671
672/**
673 * `UQ.getCredibleInterval`: the equal-tailed interval of the weighted empirical
674 * law at coverage `level`.
675 *
676 * The endpoints are DESIGN-POINT VALUES, not interpolations between them: the
677 * design is a finite set of models and the interval names two of them, which is
678 * what the reference's `find(cw >= alpha, 1)` returns. On a coarse design the
679 * interval is therefore conservative rather than smooth.
680 */
681template <class T>
682std::pair<T, T> uq_credible_interval(const UqSolution<T>& sol, const std::string& metric,
683 std::size_t ist, std::size_t r, double level = 0.95) {
684 if (!(level > 0.0) || !(level < 1.0))
685 throw InputError("SolverUQ: the coverage level must lie strictly between 0 and 1");
686 const UqEmpiricalCdf<T> ec = uq_posterior_cdf(sol, metric, ist, r);
687 if (ec.values.empty()) throw InputError("SolverUQ: the design is empty");
688 T tot = num_traits<T>::from_int(0);
689 for (const T& w : ec.probabilities) tot += w;
690 const double alpha = (1.0 - level) / 2.0;
691 T lo = ec.values.front(), hi = ec.values.back();
692 bool lo_set = false, hi_set = false;
693 for (std::size_t k = 0; k < ec.values.size(); ++k) {
694 const double cw = num_traits<T>::to_double(T(ec.cdf[k] / tot));
695 if (!lo_set && cw >= alpha) {
696 lo = ec.values[k];
697 lo_set = true;
698 }
699 if (!hi_set && cw >= 1.0 - alpha) {
700 hi = ec.values[k];
701 hi_set = true;
702 }
703 }
704 return std::make_pair(lo, hi);
705}
706
707// ---------------------------------------------------------------------------
708// Support-only (interval) uncertainty
709// ---------------------------------------------------------------------------
710
711/**
712 * `UQ.getInterval`: the RANGE of every metric over the support of the Priors.
713 *
714 * A different epistemic question from the expectation above, and the one to ask
715 * when a parameter can be BOUNDED but not distributed: the weights are dropped
716 * and only the endpoints are kept. `exact` says which of the two regimes
717 * produced it, and the distinction is not a quality label but a change of
718 * meaning -- see `uq_interval`.
719 *
720 * THE INTERVAL IS CONDITIONAL on the true parameters lying inside the Prior
721 * supports. It is not a bound on the exact solution of the network, and it must
722 * not be composed with the brackets of SolverBA, which bracket the exact
723 * solution of a model whose parameters are known.
724 */
725template <class T>
727 /** (nstations x nclasses) lower and upper endpoints of each metric. */
729 /** System throughput and total response time; the EXACT path only. */
732 bool has_totals = false;
733 /** True when the interval is the attained hull rather than a sampled range. */
734 bool exact = false;
735 /** `mvainterval` or `sampled`. */
736 std::string method;
737 /** On the sampled path, the condition that disqualified the exact one. */
738 std::string why;
739};
740
741/**
742 * `UQ.priorMeanRange`: the range of a Prior's MEAN over its alternatives.
743 *
744 * Exact for a discrete Prior, whose alternatives ARE the support. A continuous
745 * Prior is discretized first, so the range is that of the discretized support:
746 * an unbounded parameter density is never reached at its tails, which is
747 * exactly why the interval built from it is an inner approximation.
748 */
749template <class T>
750std::pair<T, T> uq_prior_mean_range(const lang::Distrib<T>& prior, std::size_t n) {
752 const lang::PriorDesign<T> g = lang::prior_discretize(prior, n, "quadrature", rng);
753 if (g.dists.empty()) throw NumericError("uq_prior_mean_range: the Prior has no alternative");
754 T lo = g.dists[0].mean, up = g.dists[0].mean;
755 for (const lang::Distrib<T>& d : g.dists) {
756 if (d.mean < lo) lo = d.mean;
757 if (up < d.mean) up = d.mean;
758 }
759 return std::make_pair(lo, up);
760}
761
762/**
763 * `UQ.qualifiesForIntervalMVA`: whether the monotonicity theorems behind
764 * `pfqn_mva_interval` hold for this model.
765 *
766 * The returned string NAMES the first violated condition rather than reporting
767 * a bare false, because "this model does not qualify" leaves the modeller
768 * guessing which of six conditions to change.
769 */
770template <class T>
771std::pair<bool, std::string> uq_qualifies_for_interval_mva(const qn::NetworkStruct<T>& sn,
772 const std::vector<PriorSite<T>>& sites) {
773 for (const PriorSite<T>& s : sites)
774 if (s.arrival)
775 return std::make_pair(false, "a Prior sits on an arrival process, so the model is open");
776 if (sn.nclasses != 1)
777 return std::make_pair(false, "the theorems are proved for a single class only");
778 if (!(sn.nclosedjobs() > 0.0)) return std::make_pair(false, "the class is not closed");
779 if (sn.nodes.size() != sn.nstations)
780 return std::make_pair(false, "the model has nodes that are not stations");
781 for (std::size_t i = 0; i < sn.nstations; ++i) {
782 const bool inf = sn.stations[i].sched == lang::SchedStrategy::INF;
783 if (!inf && sn.stations[i].nservers > 1.0)
784 return std::make_pair(false, "a queueing station has more than one server");
785 if (!inf && sn.stations[i].sched != lang::SchedStrategy::PS &&
786 sn.stations[i].sched != lang::SchedStrategy::FCFS)
787 return std::make_pair(false, "a station is neither delay, PS nor FCFS");
788 }
789 for (std::size_t a = 0; a < sites.size(); ++a)
790 for (std::size_t b = a + 1; b < sites.size(); ++b)
791 if (sites[a].station == sites[b].station)
792 return std::make_pair(false, "two Priors sit on the same station");
793 return std::make_pair(true, std::string());
794}
795
796/**
797 * `UQ.intervalByMVA`: the exact hull through `pfqn_mva_interval`.
798 *
799 * The demand box is the nominal demand vector with the prior-carrying stations
800 * widened to the range of mean service times over the Prior support. NO
801 * ENSEMBLE RUN HAPPENS: 2*(m+2) MVA calls replace the whole tensor design, and
802 * the answer is the attained range rather than the range of what was sampled.
803 */
804template <class T>
806 const std::vector<PriorSite<T>>& sites, std::size_t nodes) {
807 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
808 const std::size_t M = sn.nstations;
809 if (sn.visits.empty()) throw NumericError("uq_interval_by_mva: the model carries no visits");
810 std::vector<T> V(M, zero), STlo(M, zero), STup(M, zero);
811 std::vector<bool> inf(M, false);
812 for (std::size_t i = 0; i < M; ++i) {
813 V[i] = sn.visits[0](i, 0);
814 inf[i] = sn.stations[i].sched == lang::SchedStrategy::INF;
815 // A disabled pair has a NaN rate; MATLAB's `ST(isnan(ST)) = 0` makes it
816 // a station the class does not visit rather than an error.
817 const double rate = num_traits<T>::to_double(sn.rates(i, 0));
818 const T st = (std::isnan(rate) || rate == 0.0) ? zero : T(one / sn.rates(i, 0));
819 STlo[i] = st;
820 STup[i] = st;
821 }
822 for (const PriorSite<T>& s : sites) {
823 const std::pair<T, T> mr = uq_prior_mean_range(s.prior, nodes);
824 STlo[s.station - 1] = mr.first;
825 STup[s.station - 1] = mr.second;
826 }
827
828 std::vector<std::size_t> qidx;
829 T zlo = zero, zup = zero;
830 for (std::size_t i = 0; i < M; ++i) {
831 if (inf[i]) {
832 zlo += T(V[i] * STlo[i]);
833 zup += T(V[i] * STup[i]);
834 } else {
835 qidx.push_back(i);
836 }
837 }
838 if (qidx.empty())
839 throw UnsupportedError(
840 "uq_interval_by_mva: the model has no queueing station, so there is no MVA recursion "
841 "to bound; a pure delay model's metrics are the demand intervals themselves");
842
843 Matrix<T> L(qidx.size(), 2, zero);
844 for (std::size_t k = 0; k < qidx.size(); ++k) {
845 L(k, 0) = T(V[qidx[k]] * STlo[qidx[k]]);
846 L(k, 1) = T(V[qidx[k]] * STup[qidx[k]]);
847 }
848 const int n = static_cast<int>(std::lround(sn.nclosedjobs()));
849 const pfqn::MvaIntervalResult<T> iv = pfqn::pfqn_mva_interval(L, n, n, zlo, zup);
850
851 UqInterval<T> out;
852 out.Qlo = Matrix<T>(M, 1, zero);
853 out.Qup = Matrix<T>(M, 1, zero);
854 out.Ulo = Matrix<T>(M, 1, zero);
855 out.Uup = Matrix<T>(M, 1, zero);
856 out.Rlo = Matrix<T>(M, 1, zero);
857 out.Rup = Matrix<T>(M, 1, zero);
858 out.Wlo = Matrix<T>(M, 1, zero);
859 out.Wup = Matrix<T>(M, 1, zero);
860 out.Tlo = Matrix<T>(M, 1, zero);
861 out.Tup = Matrix<T>(M, 1, zero);
862 for (std::size_t k = 0; k < qidx.size(); ++k) {
863 const std::size_t i = qidx[k];
864 out.Qlo(i, 0) = iv.Q(k, 0);
865 out.Qup(i, 0) = iv.Q(k, 1);
866 out.Ulo(i, 0) = iv.U(k, 0);
867 out.Uup(i, 0) = iv.U(k, 1);
868 out.Wlo(i, 0) = iv.R(k, 0);
869 out.Wup(i, 0) = iv.R(k, 1);
870 // The RESIDENCE time is per visit; the response time divides it out.
871 out.Rlo(i, 0) = V[i] > zero ? T(iv.R(k, 0) / V[i]) : zero;
872 out.Rup(i, 0) = V[i] > zero ? T(iv.R(k, 1) / V[i]) : zero;
873 }
874 for (std::size_t i = 0; i < M; ++i) {
875 if (!inf[i]) continue;
876 // A delay station never queues, so its residence time IS its own demand
877 // interval and its population is the throughput times that demand,
878 // enclosed as a product of two intervals.
879 out.Wlo(i, 0) = T(V[i] * STlo[i]);
880 out.Wup(i, 0) = T(V[i] * STup[i]);
881 out.Rlo(i, 0) = STlo[i];
882 out.Rup(i, 0) = STup[i];
883 out.Qlo(i, 0) = T(iv.Xlo * V[i] * STlo[i]);
884 out.Qup(i, 0) = T(iv.Xup * V[i] * STup[i]);
885 out.Ulo(i, 0) = out.Qlo(i, 0);
886 out.Uup(i, 0) = out.Qup(i, 0);
887 }
888 for (std::size_t i = 0; i < M; ++i) {
889 out.Tlo(i, 0) = T(V[i] * iv.Xlo);
890 out.Tup(i, 0) = T(V[i] * iv.Xup);
891 }
892 out.Xlo = iv.Xlo;
893 out.Xup = iv.Xup;
894 out.Rtot_lo = iv.Rtot_lo;
895 out.Rtot_up = iv.Rtot_up;
896 out.has_totals = true;
897 out.exact = true;
898 out.method = "mvainterval";
899 return out;
900}
901
902/**
903 * `UQ.intervalBySampling`: the range of each metric across the design points
904 * that were actually solved.
905 *
906 * EXACT FOR A DISCRETE PRIOR, whose design visits the whole support, and an
907 * INNER approximation for a continuous one, since a quadrature node is a
908 * stratum median and never an endpoint. It is therefore not an enclosure, and
909 * `exact` is false to say so.
910 */
911template <class T>
913 UqInterval<T> out;
914 out.method = "sampled";
915 out.exact = false;
916 auto range = [&](const Matrix<T>& (*pick)(const mva::AvgResult<T>&), Matrix<T>& lo,
917 Matrix<T>& up) {
918 for (const mva::AvgResult<T>& r : sol.points) {
919 const Matrix<T>& v = pick(r);
920 if (v.empty()) continue;
921 if (lo.empty()) {
922 lo = v;
923 up = v;
924 continue;
925 }
926 for (std::size_t i = 0; i < v.rows(); ++i)
927 for (std::size_t j = 0; j < v.cols(); ++j) {
928 if (v(i, j) < lo(i, j)) lo(i, j) = v(i, j);
929 if (up(i, j) < v(i, j)) up(i, j) = v(i, j);
930 }
931 }
932 };
933 range([](const mva::AvgResult<T>& r) -> const Matrix<T>& { return r.QN; }, out.Qlo, out.Qup);
934 range([](const mva::AvgResult<T>& r) -> const Matrix<T>& { return r.UN; }, out.Ulo, out.Uup);
935 range([](const mva::AvgResult<T>& r) -> const Matrix<T>& { return r.RN; }, out.Rlo, out.Rup);
936 range([](const mva::AvgResult<T>& r) -> const Matrix<T>& { return r.TN; }, out.Tlo, out.Tup);
937 range([](const mva::AvgResult<T>& r) -> const Matrix<T>& { return r.WN; }, out.Wlo, out.Wup);
938 return out;
939}
940
941/**
942 * `UQ.getInterval`: the exact hull where the monotonicity theorems apply, the
943 * sampled range otherwise.
944 *
945 * THE TWO PATHS DO NOT MEAN THE SAME THING and the caller must read `exact`
946 * before quoting the numbers. The MVA path returns the ATTAINED range over the
947 * whole (continuous) demand box; the sampled path returns the range over the
948 * points that happened to be solved, which for a continuous Prior lies strictly
949 * inside the true range. `why` carries the condition that forced the fallback,
950 * which is the reference's `line_warning` text made into a returned value --
951 * this port has no warning channel, and a range that is not an enclosure must
952 * not be silently indistinguishable from one that is.
953 */
954template <class T>
956 const std::pair<bool, std::string> q = uq_qualifies_for_interval_mva(sn, sol.sites);
957 if (q.first) return uq_interval_by_mva(sn, sol.sites, sol.options.samples);
959 out.why = q.second;
960 return out;
961}
962
963/**
964 * `getInterval` from the model, running the ensemble ONLY when it is needed.
965 *
966 * The exact path costs 2*(m+2) MVA calls and reads no design point, so a caller
967 * who wants the range and not the expectation should not pay for the tensor
968 * design: `UQ.getInterval` calls `intervalByMVA` without touching `self.results`
969 * and only the sampling fallback calls `iterate`. The stage solver is therefore
970 * never invoked on a qualifying model, which also means a model whose stage
971 * solver would refuse it still has a computable interval.
972 */
973template <class T>
975 const UqOptions& opt = UqOptions()) {
976 const std::vector<PriorSite<T>> sites = uq_detect_priors(net.get_struct());
977 const std::pair<bool, std::string> q = uq_qualifies_for_interval_mva(net.get_struct(), sites);
978 if (q.first) return uq_interval_by_mva(net.get_struct(), sites, opt.samples);
980 out.why = q.second;
981 return out;
982}
983
984} // namespace uq
985} // namespace line
986
987#endif // LINE_SOLVERS_UQ_SOLVER_UQ_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
The uniform stream the Monte Carlo design draws from.
Definition prior.h:81
A subset of the registry: MATLAB's SolverFeatureSet, whose list is a flag per field.
FeatureSet & set(Feature f)
A network plus its refreshed NetworkStruct.
A queueing network under construction.
const NetworkStruct< T > & get_struct()
The refreshed struct, MATLAB's model.getStruct().
void set_service(std::size_t node, std::size_t cls, const Distrib< T > &d)
station.setService(class, dist).
void set_arrival(std::size_t node, std::size_t cls, const Distrib< T > &d)
source.setArrival(class, dist): the same table, at the Source.
The ensemble surface of UQ: @UQ's EnsembleSolver implementation.
Definition solver_uq.h:431
std::size_t get_num_alternatives() const
UQ.getNumAlternatives: design points, 1 when the model carries no Prior.
Definition solver_uq.h:526
void init()
UQ.init: resolve the design; the ensemble itself is expanded per point.
Definition solver_uq.h:445
const std::vector< PriorSite< T > > & get_prior_info() const
Where the Priors were found, the reference's priorInfo.
Definition solver_uq.h:552
const mva::AvgResult< T > & analyze(int, std::size_t e)
UQ.analyze: solve design point e (1-BASED, as the reference indexes it).
Definition solver_uq.h:465
void pre(int)
UQ.pre: nothing to seed – the design points are independent models.
Definition solver_uq.h:455
std::size_t get_uq_nodes() const
UQ.getUQNodes: nodes per continuous Prior.
Definition solver_uq.h:540
bool has_prior_distribution() const
UQ.hasPriorDistribution.
Definition solver_uq.h:523
SolverUq(qn::Network< T > &n, const UqStageSolver< T > &s, const UqOptions &o=UqOptions())
Definition solver_uq.h:433
std::vector< T > get_probabilities() const
UQ.getProbabilities: the design weights, from the DESIGN not the solve.
Definition solver_uq.h:532
const mva::AvgResult< T > & get_avg() const
UQ.getAvg: the aggregate.
Definition solver_uq.h:549
const std::vector< UqDesignPoint< T > > & get_design() const
The design itself, one entry per point.
Definition solver_uq.h:555
bool converged(int it) const
UQ.converged: one iteration is all UQ needs, the design being fixed.
Definition solver_uq.h:483
const UqSolution< T > & get_solution() const
Everything above in one value, which is what the CLI and the tests read.
Definition solver_uq.h:558
void post(int)
UQ.post: the prior-weighted expectation over the points solved so far.
Definition solver_uq.h:477
const UqSolution< T > & iterate()
UQ.runAnalyzer: the whole lifecycle, in the reference's order.
Definition solver_uq.h:486
const std::string & get_uq_method() const
UQ.getUQMethod: the RESOLVED design name.
Definition solver_uq.h:543
qn::Network< T > expand(std::size_t e) const
self.ensemble{e}: the model of design point e (1-based), Prior gone.
Definition solver_uq.h:506
std::size_t get_number_of_models() const
UQ.getNumberOfModels: the same count, under the EnsembleSolver's name.
Definition solver_uq.h:529
void finish()
UQ.finish: nothing to release.
Definition solver_uq.h:480
const std::vector< mva::AvgResult< T > > & get_ensemble_avg() const
UQ.getEnsembleAvg: the per-point results, in the order they were solved.
Definition solver_uq.h:546
The exception types the port throws.
The language-feature gate: what a MODEL uses against what a SOLVER declares.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
constexpr std::size_t kPriorDefaultNodes
The default number of nodes per continuous Prior, MATLAB options.samples.
Definition prior.h:72
PriorDesign< T > prior_discretize(const Distrib< T > &d, std::size_t n, const std::string &method, PriorRng &rng)
Reduce a Prior to n weighted alternatives, MATLAB Prior.discretize.
Definition prior.h:197
NodeType
Node kinds, with the values of MATLAB NodeType.
Definition lang_types.h:324
MvaIntervalResult< T > pfqn_mva_interval(const Matrix< T > &L, int nlo, int nup, const T &zlo, const T &zup)
Exact interval-valued MVA for single-class closed product-form networks.
UqSolution< T > solver_uq_run_analyzer(qn::Network< T > &net, const UqStageSolver< T > &stage, const UqOptions &opt=UqOptions())
UQ.runAnalyzer as a free call: expand, solve every design point, aggregate.
Definition solver_uq.h:574
UqInterval< T > uq_interval_by_mva(const qn::NetworkStruct< T > &sn, const std::vector< PriorSite< T > > &sites, std::size_t nodes)
UQ.intervalByMVA: the exact hull through pfqn_mva_interval.
Definition solver_uq.h:805
UqInterval< T > uq_interval_by_sampling(const UqSolution< T > &sol)
UQ.intervalBySampling: the range of each metric across the design points that were actually solved.
Definition solver_uq.h:912
UqEmpiricalCdf< T > uq_posterior_cdf(const UqSolution< T > &sol, const std::string &metric, std::size_t ist, std::size_t r)
UQ.getPosteriorDist: the posterior law of a metric across the design.
Definition solver_uq.h:654
UqInterval< T > uq_interval_run(qn::Network< T > &net, const UqStageSolver< T > &stage, const UqOptions &opt=UqOptions())
getInterval from the model, running the ensemble ONLY when it is needed.
Definition solver_uq.h:974
std::vector< UqDesignPoint< T > > uq_build_design(const std::vector< PriorSite< T > > &sites, const UqOptions &opt)
UQ.buildDesign: reduce the detected Priors to weighted design points.
Definition solver_uq.h:258
std::vector< PriorSite< T > > uq_detect_priors(const qn::NetworkStruct< T > &sn)
UQ.detectPriors: find every Prior, in node order and then class order.
Definition solver_uq.h:196
void uq_aggregate(UqSolution< T > &sol)
UQ.aggregateResults: the prior-weighted expectation of the solved points.
Definition solver_uq.h:367
std::function< mva::AvgResult< T >(const qn::NetworkStruct< T > &)> UqStageSolver
What solves one design point: the C++ spelling of @(m) SolverXXX(m).
Definition solver_uq.h:332
std::vector< T > uq_samples(const UqSolution< T > &sol, const std::string &metric, std::size_t ist, std::size_t r)
UQ.getSamples: the value of one metric at every design point, with weights.
Definition solver_uq.h:602
UqMoments< T > uq_moments(const UqSolution< T > &sol, const std::string &metric, std::size_t ist, std::size_t r)
UQ.getMoments: the unconditional mean of Trivedi and Bobbio Eq.
Definition solver_uq.h:632
std::pair< bool, std::string > uq_qualifies_for_interval_mva(const qn::NetworkStruct< T > &sn, const std::vector< PriorSite< T > > &sites)
UQ.qualifiesForIntervalMVA: whether the monotonicity theorems behind pfqn_mva_interval hold for this ...
Definition solver_uq.h:771
UqInterval< T > uq_interval(const UqSolution< T > &sol, const qn::NetworkStruct< T > &sn)
UQ.getInterval: the exact hull where the monotonicity theorems apply, the sampled range otherwise.
Definition solver_uq.h:955
std::pair< T, T > uq_credible_interval(const UqSolution< T > &sol, const std::string &metric, std::size_t ist, std::size_t r, double level=0.95)
UQ.getCredibleInterval: the equal-tailed interval of the weighted empirical law at coverage level.
Definition solver_uq.h:682
std::vector< std::string > uq_list_valid_methods()
UQ.listValidMethods.
Definition solver_uq.h:157
const Matrix< T > & uq_metric_matrix(const mva::AvgResult< T > &r, const std::string &metric)
The metric matrix a name selects, MATLAB's res.Avg.
Definition solver_uq.h:586
qn::FeatureSet uq_feature_set()
UQ.getFeatureSet: the one construct UQ adds, and nothing else.
Definition solver_uq.h:179
std::pair< T, T > uq_prior_mean_range(const lang::Distrib< T > &prior, std::size_t n)
UQ.priorMeanRange: the range of a Prior's MEAN over its alternatives.
Definition solver_uq.h:750
std::string uq_resolve_method(const std::string &m)
UQ.getUQMethod: resolve the discretization method.
Definition solver_uq.h:149
constexpr std::size_t kMaxDesignPoints
The cap on the tensor-product design, MATLAB UQ.MaxDesignPoints.
Definition solver_uq.h:82
The Network constructor API: Queue, Delay, Source, Sink, Router, ClassSwitch, Cache,...
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
Exact interval-valued MVA for single-class closed product-form networks.
Prior: parameter uncertainty as a weighted set of alternative models.
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
bool is_prior() const
Definition lang_types.h:776
A Prior reduced to alternatives and weights; the output of prior_discretize.
Definition prior.h:96
std::vector< Distrib< T > > dists
Definition prior.h:97
The metrics getAvg returns, after filtering.
Matrix< T > TN
throughput
Matrix< T > RN
response time, per visit
Matrix< T > UN
utilization
Matrix< T > WN
residence time, per job
std::string actualmethod
the algorithm that ran
Matrix< T > QN
queue length
std::vector< T > CN
system response time per class
std::vector< T > XN
system throughput per class
Matrix< T > AN
arrival rate
Every output of pfqn_mva_interval, each a [lower, upper] pair.
Matrix< T > R
(M x 2) residence time per station
Matrix< T > Q
(M x 2) mean queue length per station
Matrix< T > U
(M x 2) utilization enclosure per station
Where a Prior sits in the model, MATLAB's priorInfo entry.
Definition solver_uq.h:106
lang::Distrib< T > prior
The Prior itself, copied out of the service table.
Definition solver_uq.h:113
bool arrival
True at a Source, where the Prior is an ARRIVAL process, not a service one.
Definition solver_uq.h:111
std::size_t station
1-based station index
Definition solver_uq.h:108
std::size_t node
1-based node index
Definition solver_uq.h:107
std::size_t cls
1-based class index
Definition solver_uq.h:109
One design point: a concrete distribution for every Prior, and its weight.
Definition solver_uq.h:118
std::vector< lang::Distrib< T > > dists
one per site, in site order
Definition solver_uq.h:120
The weighted empirical law of a metric, sorted ascending; MATLAB's EmpiricalCDF.
Definition solver_uq.h:646
std::vector< T > probabilities
the weight of each value, in the same order
Definition solver_uq.h:648
std::vector< T > values
the metric at each design point, ascending
Definition solver_uq.h:647
std::vector< T > cdf
the running sum of probabilities
Definition solver_uq.h:649
UQ.getInterval: the RANGE of every metric over the support of the Priors.
Definition solver_uq.h:726
std::string method
mvainterval or sampled.
Definition solver_uq.h:736
bool exact
True when the interval is the attained hull rather than a sampled range.
Definition solver_uq.h:734
Matrix< T > Qlo
(nstations x nclasses) lower and upper endpoints of each metric.
Definition solver_uq.h:728
std::string why
On the sampled path, the condition that disqualified the exact one.
Definition solver_uq.h:738
T Xlo
System throughput and total response time; the EXACT path only.
Definition solver_uq.h:730
The weighted mean and variance of a metric over the design.
Definition solver_uq.h:618
UQ.defaultOptions plus the stream the Monte Carlo design draws from.
Definition solver_uq.h:85
std::string method
default | discrete | quadrature | montecarlo, MATLAB UQ.listValidMethods.
Definition solver_uq.h:92
std::size_t samples
Nodes per continuous Prior, or design points under montecarlo; the reference's options....
Definition solver_uq.h:99
unsigned long seed
The Monte Carlo stream; unread by a quadrature design, which draws nothing.
Definition solver_uq.h:101
What solver_uq_run_analyzer returns.
Definition solver_uq.h:125
std::vector< T > weights
The design weights, summing to 1.
Definition solver_uq.h:131
std::string method
The RESOLVED discretization method: quadrature or montecarlo.
Definition solver_uq.h:137
std::vector< mva::AvgResult< T > > points
The result at each design point, in design order.
Definition solver_uq.h:129
UqOptions options
The options the design was built with; uq_interval reads samples.
Definition solver_uq.h:139
std::vector< PriorSite< T > > sites
Where the Priors were found.
Definition solver_uq.h:135
std::vector< UqDesignPoint< T > > design
The alternatives each point substituted, one per site.
Definition solver_uq.h:133
mva::AvgResult< T > avg
The prior-weighted expectation of every metric, (nstations x nclasses).
Definition solver_uq.h:127