LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
prior.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_LANG_PRIOR_H
6#define LINE_LANG_PRIOR_H
7
8/**
9 * @file
10 * @ingroup line_lang
11 * `Prior`: parameter uncertainty as a weighted set of alternative models.
12 *
13 * Port of `matlab/src/lang/processes/Prior.m`. A Prior is placed where a
14 * service or arrival distribution goes and says that the law is not known: it
15 * is one of an explicit alternative set with prior weights (the DISCRETE form),
16 * or it is generated by a scalar parameter whose density is given together with
17 * a map from the parameter to a distribution (the CONTINUOUS form of Trivedi
18 * and Bobbio (2017), Sec. 3.4). SolverUQ is the only consumer.
19 *
20 * IT IS NOT A MIXTURE, and the difference is not cosmetic. A mixture says each
21 * JOB draws its service law afresh; a Prior says the MODEL has one law and we
22 * do not know which. The two give different queue lengths, and it is the second
23 * that epistemic uncertainty propagation means. The mixture moments are still
24 * exposed (`prior_mean`, `prior_scv`, `prior_skewness`, `prior_cdf`,
25 * `prior_lst`), because the reference's `Prior` inherits `Distribution` and
26 * answers those questions, and because `refresh_rates` will lower a Prior to
27 * SOME rate if a struct is dumped before SolverUQ has expanded it -- but no
28 * solver ever reads them: `Feature::Prior` is declared by SolverUQ alone, so
29 * every other solver refuses the model at the gate.
30 *
31 * DISCRETIZATION, which is where the two forms meet. `prior_discretize` reduces
32 * either form to (dists, weights):
33 * quadrature the discrete form UNCHANGED (it is already exact, and `n` is
34 * ignored); the continuous form at the conditional medians of n
35 * equal-mass strata, weights 1/n. The nodes are quantiles, so
36 * the rule integrates the parameter density in PROBABILITY space
37 * and needs only `dist_cdf`, which every family has.
38 * montecarlo n draws, weights 1/n. For a discrete prior the draw is of the
39 * alternative INDEX against its probabilities -- returning the
40 * alternatives unweighted here would silently drop the prior.
41 *
42 * WHERE THE RANDOMNESS COMES FROM, and how it differs from the reference.
43 * MATLAB draws with `rand` and, for the continuous form, with
44 * `paramDist.sample(n)`, i.e. each family's own sampler. This port has no
45 * per-family sampler at all -- `Distrib` carries none, and SSA generates its
46 * clocks internally -- so the Monte Carlo design draws a uniform from an
47 * explicit MT19937 stream and inverts the CDF. The design points are therefore
48 * NOT the reference's for the same seed, exactly as the SSA sample paths are
49 * not; the design's LAW is the same, and a quadrature design is identical
50 * across the two codebases because it draws nothing.
51 */
52
53#include <algorithm>
54#include <cmath>
55#include <cstddef>
56#include <cstdint>
57#include <functional>
58#include <memory>
59#include <random>
60#include <string>
61#include <vector>
62
65#include "line/num/number.h"
66#include "line/util/error.h"
67
68namespace line {
69namespace lang {
70
71/** The default number of nodes per continuous Prior, MATLAB `options.samples`. */
72inline constexpr std::size_t kPriorDefaultNodes = 11;
73
74/**
75 * The uniform stream the Monte Carlo design draws from.
76 *
77 * The same 53-bit assembly from two MT19937 words that `ssa::SsaRng` uses, so
78 * the two random paths in this port share one construction; it is duplicated
79 * rather than shared because `lang/` must not depend on a solver.
80 */
81class PriorRng {
82 public:
83 explicit PriorRng(unsigned long seed) : g_(static_cast<std::uint_fast32_t>(seed)) {}
84 double uniform() {
85 const std::uint64_t a = g_() >> 5, b = g_() >> 6;
86 return (static_cast<double>(a) * 67108864.0 + static_cast<double>(b) + 0.5) /
87 9007199254740992.0;
88 }
89
90 private:
91 std::mt19937 g_;
92};
93
94/** A Prior reduced to alternatives and weights; the output of `prior_discretize`. */
95template <class T>
97 std::vector<Distrib<T>> dists;
98 std::vector<T> weights;
99};
100
101/**
102 * `Prior(distributions, probabilities)`: the discrete form.
103 *
104 * The weights must be nonnegative and sum to one within CoarseTol, which is the
105 * reference's own tolerance; a set that does not is a modelling error and is
106 * refused rather than renormalized, since renormalizing would answer for a
107 * prior the caller did not write.
108 */
109template <class T>
110Distrib<T> prior_discrete(const std::vector<Distrib<T>>& alternatives,
111 const std::vector<T>& probabilities) {
112 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
113 if (alternatives.empty()) throw InputError("Prior: the alternative set is empty");
114 if (alternatives.size() != probabilities.size())
115 throw InputError("Prior: there are " + std::to_string(alternatives.size()) +
116 " alternatives and " + std::to_string(probabilities.size()) +
117 " probabilities");
118 T tot = zero;
119 for (std::size_t i = 0; i < probabilities.size(); ++i) {
120 if (probabilities[i] < zero) throw InputError("Prior: the probabilities must be nonnegative");
121 tot += probabilities[i];
122 if (alternatives[i].disabled)
123 throw InputError("Prior: alternative " + std::to_string(i + 1) + " is disabled");
124 if (alternatives[i].is_prior())
125 throw InputError("Prior: alternative " + std::to_string(i + 1) +
126 " is itself a Prior; nest the uncertainty in one Prior instead");
127 }
128 if (std::fabs(num_traits<T>::to_double(T(tot - one))) > GlobalConstants::CoarseTol)
129 throw InputError("Prior: the probabilities must sum to 1 (they sum to " +
130 std::to_string(num_traits<T>::to_double(tot)) + ")");
131 Distrib<T> d;
133 d.disabled = false;
134 d.prior = std::make_shared<PriorSpec<T>>();
135 d.prior->continuous = false;
136 d.prior->alternatives = alternatives;
137 d.prior->probabilities = probabilities;
138 return d;
139}
140
141/**
142 * `Prior(paramDist, distFactory)`: the continuous form.
143 *
144 * `factory` maps a value of the parameter to a distribution, e.g. a rate to an
145 * `Exp`. It is called eagerly once here, on the median of the parameter law, so
146 * that a factory returning a disabled or nested-Prior distribution is refused
147 * at construction rather than at the first design point.
148 */
149template <class T>
151 const std::function<Distrib<T>(const T&)>& factory) {
152 if (param_dist.disabled) throw InputError("Prior: the parameter distribution is disabled");
153 if (param_dist.is_prior())
154 throw InputError("Prior: the parameter distribution is itself a Prior");
155 if (!factory) throw InputError("Prior: the distribution factory is empty");
156 const Distrib<T> probe = factory(dist_quantile(param_dist, num_traits<T>::from_double(0.5)));
157 if (probe.disabled || probe.is_prior())
158 throw InputError("Prior: the distribution factory must return a usable Distribution");
159 Distrib<T> d;
161 d.disabled = false;
162 d.prior = std::make_shared<PriorSpec<T>>();
163 d.prior->continuous = true;
164 d.prior->param_dist = param_dist;
165 d.prior->factory = factory;
166 return d;
167}
168
169/**
170 * `Prior.fromSample(k, s)`: the posterior of a rate estimated from lifetime data.
171 *
172 * Given k i.i.d. exponential observations summing to s, the Jeffreys prior
173 * f(lambda) = s/lambda yields the posterior lambda^(k-1) s^k exp(-lambda s) /
174 * (k-1)!, an Erlang density of k phases and phase rate s (Trivedi and Bobbio
175 * (2017), Eq. 3.71). Its mean k/s is the maximum-likelihood rate and its
176 * variance k/s^2 shrinks as k grows, so the prior concentrates on the estimate.
177 */
178template <class T>
179Distrib<T> prior_from_sample(std::size_t k, const T& s,
180 const std::function<Distrib<T>(const T&)>& factory =
181 std::function<Distrib<T>(const T&)>()) {
182 if (k < 1) throw InputError("Prior.fromSample: k must be a positive observation count");
183 if (!(s > num_traits<T>::from_int(0)))
184 throw InputError("Prior.fromSample: s must be a positive sum of observed lifetimes");
185 std::function<Distrib<T>(const T&)> f = factory;
186 if (!f) f = [](const T& lambda) { return Distrib<T>::exp_rate(lambda); };
188}
189
190/**
191 * Reduce a Prior to `n` weighted alternatives, MATLAB `Prior.discretize`.
192 *
193 * `rng` is consulted only by the `montecarlo` method; a quadrature design draws
194 * nothing and is reproducible across codebases.
195 */
196template <class T>
197PriorDesign<T> prior_discretize(const Distrib<T>& d, std::size_t n, const std::string& method,
198 PriorRng& rng) {
199 if (!d.is_prior()) throw InputError("prior_discretize: the distribution is not a Prior");
200 if (method != "quadrature" && method != "montecarlo")
201 throw InputError("prior_discretize: unknown discretization method '" + method + "'");
202 if (n < 1) throw InputError("prior_discretize: the node count must be at least 1");
203 const PriorSpec<T>& sp = *d.prior;
204 PriorDesign<T> out;
205 const T wn = T(num_traits<T>::from_int(1) / num_traits<T>::from_int(static_cast<long>(n)));
206
207 if (!sp.continuous) {
208 if (method == "quadrature") {
209 out.dists = sp.alternatives;
210 out.weights = sp.probabilities;
211 return out;
212 }
213 // The draw is of the alternative INDEX against its own probabilities.
214 std::vector<double> cum(sp.probabilities.size(), 0.0);
215 double acc = 0.0;
216 for (std::size_t i = 0; i < sp.probabilities.size(); ++i) {
218 cum[i] = acc;
219 }
220 for (std::size_t i = 0; i < n; ++i) {
221 const double u = rng.uniform() * acc;
222 std::size_t k = 0;
223 while (k + 1 < cum.size() && cum[k] < u) ++k;
224 out.dists.push_back(sp.alternatives[k]);
225 out.weights.push_back(wn);
226 }
227 return out;
228 }
229
230 for (std::size_t i = 0; i < n; ++i) {
231 T p;
232 if (method == "quadrature") {
233 // The midpoint of stratum i, ((i+1) - 0.5)/n in the reference's
234 // 1-based indexing: the conditional median of an equal-mass stratum.
235 p = num_traits<T>::from_double((static_cast<double>(i) + 0.5) /
236 static_cast<double>(n));
237 } else {
238 p = num_traits<T>::from_double(rng.uniform());
239 }
240 const Distrib<T> alt = sp.factory(dist_quantile(sp.param_dist, p));
241 if (alt.disabled || alt.is_prior())
242 throw InputError("prior_discretize: the factory returned an unusable Distribution");
243 out.dists.push_back(alt);
244 out.weights.push_back(wn);
245 }
246 return out;
247}
248
249/** `prior_discretize` with the defaults of `Prior.discretize`: 11 quadrature nodes. */
250template <class T>
252 PriorRng rng(23000);
253 return prior_discretize(d, kPriorDefaultNodes, "quadrature", rng);
254}
255
256/** E[X] = sum_i p_i E[X_i], MATLAB `Prior.getMean`. */
257template <class T>
259 const PriorDesign<T> g = prior_discretize(d);
261 for (std::size_t i = 0; i < g.dists.size(); ++i) m += T(g.weights[i] * g.dists[i].mean);
262 return m;
263}
264
265/**
266 * The SCV by the law of total variance, MATLAB `Prior.getSCV`.
267 *
268 * Var(X) = E[Var(X|D)] + Var(E[X|D]): the within-alternative variance plus the
269 * spread of the alternative means. The second term is what makes a Prior's SCV
270 * exceed the SCV of any of its alternatives.
271 */
272template <class T>
274 const PriorDesign<T> g = prior_discretize(d);
275 T e_mean = num_traits<T>::from_int(0), e_var = num_traits<T>::from_int(0),
276 e_mean_sq = num_traits<T>::from_int(0);
277 for (std::size_t i = 0; i < g.dists.size(); ++i) {
278 const T m = g.dists[i].mean;
279 const T v = T(g.dists[i].scv * m * m);
280 e_mean += T(g.weights[i] * m);
281 e_var += T(g.weights[i] * v);
282 e_mean_sq += T(g.weights[i] * m * m);
283 }
284 if (!(e_mean > num_traits<T>::from_int(0)))
285 throw NumericError("prior_scv: the prior-weighted mean is not positive");
286 const T total_var = T(e_var + (e_mean_sq - e_mean * e_mean));
287 return T(total_var / (e_mean * e_mean));
288}
289
290/**
291 * The skewness of the mixture, MATLAB `Prior.getSkewness`.
292 *
293 * Each alternative's third central moment is shifted to the global mean by
294 * E[(X_i - mu)^3] = m3_i + 3 v_i delta + delta^3, delta = m_i - mu, and the
295 * shifted moments are averaged. Exact for the mixture, and it is the mixture
296 * that `Distribution.getSkewness` is asked about.
297 */
298template <class T>
300 const T zero = num_traits<T>::from_int(0), three = num_traits<T>::from_int(3);
301 const T mu = prior_mean(d);
302 const T scv = prior_scv(d);
303 const T sigma2 = T(scv * mu * mu);
304 const double sigma = std::sqrt(num_traits<T>::to_double(sigma2));
305 if (sigma < GlobalConstants::FineTol) return zero;
306 const PriorDesign<T> g = prior_discretize(d);
307 T third = zero;
308 for (std::size_t i = 0; i < g.dists.size(); ++i) {
309 const T mi = g.dists[i].mean;
310 const T vi = T(g.dists[i].scv * mi * mi);
311 const T m3i = T(dist_moment(g.dists[i], 3) - three * mi * vi - mi * mi * mi);
312 const T delta = T(mi - mu);
313 third += T(g.weights[i] * (m3i + three * vi * delta + delta * delta * delta));
314 }
315 return T(third / num_traits<T>::from_double(sigma * sigma * sigma));
316}
317
318/** F(t) = sum_i p_i F_i(t), MATLAB `Prior.evalCDF`. */
319template <class T>
320T prior_cdf(const Distrib<T>& d, const T& t) {
321 const PriorDesign<T> g = prior_discretize(d);
323 for (std::size_t i = 0; i < g.dists.size(); ++i) f += T(g.weights[i] * dist_cdf(g.dists[i], t));
324 return f;
325}
326
327/** L(s) = sum_i p_i L_i(s), MATLAB `Prior.evalLST`. */
328template <class T>
329T prior_lst(const Distrib<T>& d, const T& s) {
330 const PriorDesign<T> g = prior_discretize(d);
332 for (std::size_t i = 0; i < g.dists.size(); ++i) l += T(g.weights[i] * dist_lst(g.dists[i], s));
333 return l;
334}
335
336/**
337 * Write the mixture moments onto a Prior, the counterpart of
338 * `dist_refresh_moments` for the Markovian families.
339 *
340 * `prior_discrete` and `prior_continuous` leave `mean` and `scv` at their
341 * defaults, because computing them costs a discretization of the parameter
342 * density; the builders call this so that a struct dumped before SolverUQ has
343 * run reports the epistemic mean rather than a zero.
344 */
345template <class T>
347 if (!d.is_prior()) return;
348 d.mean = prior_mean(d);
349 d.scv = prior_scv(d);
350}
351
352} // namespace lang
353} // namespace line
354
355#endif // LINE_LANG_PRIOR_H
InputError(const std::string &what)
Definition error.h:39
NumericError(const std::string &what)
Definition error.h:45
The uniform stream the Monte Carlo design draws from.
Definition prior.h:81
double uniform()
Definition prior.h:84
PriorRng(unsigned long seed)
Definition prior.h:83
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
void prior_refresh_moments(Distrib< T > &d)
Write the mixture moments onto a Prior, the counterpart of dist_refresh_moments for the Markovian fam...
Definition prior.h:346
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
T prior_cdf(const Distrib< T > &d, const T &t)
F(t) = sum_i p_i F_i(t), MATLAB Prior.evalCDF.
Definition prior.h:320
T dist_quantile(const Distrib< T > &d, const T &p)
The p-quantile, by bisection on dist_cdf.
T prior_lst(const Distrib< T > &d, const T &s)
L(s) = sum_i p_i L_i(s), MATLAB Prior.evalLST.
Definition prior.h:329
Distrib< T > prior_continuous(const Distrib< T > &param_dist, const std::function< Distrib< T >(const T &)> &factory)
Prior(paramDist, distFactory): the continuous form.
Definition prior.h:150
T dist_lst(const Distrib< T > &d, const T &s)
sn.lst: the Laplace-Stieltjes transform E[exp(-sX)].
T prior_mean(const Distrib< T > &d)
E[X] = sum_i p_i E[X_i], MATLAB Prior.getMean.
Definition prior.h:258
Distrib< T > prior_discrete(const std::vector< Distrib< T > > &alternatives, const std::vector< T > &probabilities)
Prior(distributions, probabilities): the discrete form.
Definition prior.h:110
T dist_cdf(const Distrib< T > &d, const T &x)
F(x) = P{X <= x}, MATLAB's Distribution.evalCDF.
Distrib< T > prior_from_sample(std::size_t k, const T &s, const std::function< Distrib< T >(const T &)> &factory=std::function< Distrib< T >(const T &)>())
Prior.fromSample(k, s): the posterior of a rate estimated from lifetime data.
Definition prior.h:179
@ PRIOR
A Prior: a weighted set of ALTERNATIVE distributions, or a density over a scalar parameter plus a fac...
Definition lang_types.h:510
T prior_skewness(const Distrib< T > &d)
The skewness of the mixture, MATLAB Prior.getSkewness.
Definition prior.h:299
T dist_moment(const Distrib< T > &d, unsigned k)
The k-th raw moment.
T prior_scv(const Distrib< T > &d)
The SCV by the law of total variance, MATLAB Prior.getSCV.
Definition prior.h:273
Number-type abstraction for the templated API port.
static Distrib exp_rate(const T &r)
Definition lang_types.h:814
std::shared_ptr< PriorSpec< T > > prior
The alternatives of a Prior, set only when type == PRIOR.
Definition lang_types.h:775
static Distrib erlang(const T &phase_rate, std::size_t r)
Erlang(alpha, r): r phases of rate alpha, as MATLAB's Erlang(phaseRate, nphases).
Definition lang_types.h:873
bool is_prior() const
Definition lang_types.h:776
static constexpr double FineTol
Definition lang_types.h:668
static constexpr double CoarseTol
Definition lang_types.h:669
A Prior reduced to alternatives and weights; the output of prior_discretize.
Definition prior.h:96
std::vector< T > weights
Definition prior.h:98
std::vector< Distrib< T > > dists
Definition prior.h:97
A LINE Distribution, as the model layer and sn carry it.
std::function< Distrib< T >(const T &)> factory
theta -> Distribution; the continuous form only.
bool continuous
True for the parameter-density form, false for the alternative-set form.
Distrib< T > param_dist
The law of the scalar parameter; the continuous form only.
std::vector< T > probabilities
std::vector< Distrib< T > > alternatives
The alternatives and their weights; the discrete form only.