1classdef Prior < Distribution
2 % Prior Discrete prior distribution over alternative distributions
4 % Prior represents parameter uncertainty by specifying a discrete set of
5 % alternative distributions with associated probabilities. When used with
6 % setService or setArrival, it causes
the UQ solver to expand
the
7 % model into a family of networks, one
for each alternative.
9 % This
is NOT a mixture distribution - each alternative represents a
10 % separate model realization with its associated prior probability.
12 % Two forms are supported:
13 % - Discrete: an
explicit set of alternative distributions with weights.
14 % - Continuous: a density f(theta) over a scalar parameter theta plus a
15 % factory mapping theta to a Distribution. This
is the form required by
16 %
the epistemic uncertainty propagation of Trivedi and Bobbio (2017),
17 % Sec. 3.4, where
the unconditional measure
is the integral of
the
18 % conditional measure against f(theta). The continuous form
is reduced to
19 % a weighted alternative set by discretize(), so both forms are consumed
20 % identically downstream.
22 % @brief Discrete or continuous prior
for parameter uncertainty modeling
24 % Key characteristics:
25 % - Discrete set of alternative distributions, or a continuous parameter density
26 % - Probability-weighted alternatives (must sum to 1)
27 % - Used with UQ solver
for Bayesian analysis
31 % % Discrete form: service time with uncertain rate
32 % prior = Prior({Exp(1.0), Exp(2.0), Erlang(2,1.5)}, [0.4, 0.35, 0.25]);
33 % queue.setService(
class, prior);
35 % % Continuous form: rate
is itself Erlang-distributed
36 % prior = Prior(Erlang(10, 3), @(lambda) Exp(lambda));
38 % % Continuous form from k lifetime observations summing to s
39 % % (Jeffreys posterior of Trivedi-Bobbio Eq. 3.71)
40 % prior = Prior.fromSample(10, 5.0);
42 % % Solve with UQ wrapper
43 % post = UQ(model, @SolverMVA);
44 % avgTable = post.getAvgTable(); % Prior-weighted expectations
45 % postTable = post.getPosteriorTable(); % Per-alternative breakdown
48 % Copyright (c) 2012-2026, Imperial College London
49 % All rights reserved.
52 distributions; % Cell array of alternative distributions (discrete form)
53 probabilities; % Vector of prior probabilities (sum to 1) (discrete form)
54 paramDist; % Distribution over
the scalar parameter (continuous form)
55 distFactory; % Handle theta -> Distribution (continuous form)
56 kind; %
'discrete' or
'continuous'
60 function self = Prior(varargin)
61 % PRIOR Create a prior distribution instance
63 % @brief Creates a Prior in either discrete or continuous form
65 % PRIOR(DISTRIBUTIONS, PROBABILITIES) discrete form.
66 % @param distributions Cell array of Distribution objects
67 % @param probabilities Vector of probabilities (must sum to 1)
69 % PRIOR(PARAMDIST, DISTFACTORY) continuous form.
70 % @param paramDist Distribution of
the scalar parameter theta
71 % @param distFactory Handle theta -> Distribution
73 % @
return self Prior instance
75 self@Distribution(
'Prior', 2, [0, Inf]);
78 line_error(mfilename,
'Prior requires two arguments: (distributions, probabilities) or (paramDist, distFactory)');
81 if isa(varargin{1},
'Distribution') && isa(varargin{2},
'function_handle')
83 self.kind =
'continuous';
84 self.paramDist = varargin{1};
85 self.distFactory = varargin{2};
86 self.distributions = {};
87 self.probabilities = [];
88 setParam(self, 1,
'paramDist', self.paramDist);
89 setParam(self, 2,
'distFactory', self.distFactory);
94 self.kind =
'discrete';
95 distributions = varargin{1};
96 probabilities = varargin{2};
98 % Validate distributions input
99 if ~iscell(distributions)
100 line_error(mfilename,
'distributions must be a cell array');
102 if isempty(distributions)
103 line_error(mfilename,
'distributions cannot be empty');
105 for i = 1:length(distributions)
106 if ~isa(distributions{i},
'Distribution')
107 line_error(mfilename, sprintf(
'Element %d is not a Distribution object', i));
111 % Validate probabilities input
112 if length(distributions) ~= length(probabilities)
113 line_error(mfilename,
'Number of distributions must match number of probabilities');
115 if abs(sum(probabilities) - 1) > GlobalConstants.CoarseTol
116 line_error(mfilename, sprintf(
'Probabilities must sum to 1 (current sum: %f)', sum(probabilities)));
118 if any(probabilities < 0)
119 line_error(mfilename, 'Probabilities must be non-negative');
122 self.distributions = distributions(:)'; % row cell array
123 self.probabilities = probabilities(:)'; % row vector
125 setParam(self, 1, 'distributions', distributions);
126 setParam(self, 2, 'probabilities', probabilities);
129 function
bool = isContinuousPrior(self)
130 % BOOL = ISCONTINUOUSPRIOR()
131 % Return true if
the prior
is specified by a parameter density.
132 % Note:
the base class isContinuous() refers to
the support of
the
133 % distribution itself, not to
the form of
the prior.
134 bool = strcmp(self.kind, 'continuous');
137 function [dists, weights] = discretize(self, n, method)
138 % [DISTS, WEIGHTS] = DISCRETIZE(N, METHOD)
139 % Reduce
the prior to N weighted alternatives.
141 % The method
is honoured for both forms of prior:
142 % 'quadrature' For a discrete prior,
the alternatives and their
143 % probabilities unchanged, and N
is ignored:
the
144 % set
is already exact. For a continuous prior,
145 % stratified quantile midpoints with weights 1/N.
146 % Each node
is the conditional median of an
147 % equal-mass stratum, so
the rule integrates
the
148 % parameter density in probability space and needs
149 % only evalCDF, which every Distribution provides.
150 % 'montecarlo' N i.i.d. draws, weights 1/N. For a discrete prior
151 %
the draws are of
the alternative index against
152 % its probabilities; returning
the alternatives
153 % unweighted here would silently drop
the prior.
155 % @param n Number of alternatives (ignored by a discrete quadrature)
156 % @param method 'quadrature' (default) or 'montecarlo'
157 % @return dists Cell array of Distribution objects
158 % @return weights Row vector of weights summing to 1
160 if nargin < 2 || isempty(n)
163 if nargin < 3 || isempty(method)
164 method = 'quadrature';
166 if ~any(strcmp(method, {
'quadrature',
'montecarlo'}))
167 line_error(mfilename, sprintf('Unknown discretization method: %s', method));
170 if strcmp(self.kind, 'discrete')
173 dists = self.distributions;
174 weights = self.probabilities;
176 cumprob = cumsum(self.probabilities);
179 idx = find(rand() <= cumprob, 1, 'first');
180 dists{i} = self.distributions{idx};
182 weights = ones(1, n) / n;
189 % Midpoint of each equal-probability stratum
190 p = ((1:n) - 0.5) / n;
193 theta(i) = Prior.quantile(self.paramDist, p(i));
196 s = self.paramDist.sample(n);
200 weights = ones(1, n) / n;
203 dists{i} = self.distFactory(theta(i));
204 if ~isa(dists{i}, 'Distribution
')
205 line_error(mfilename, 'distFactory must return a Distribution object
');
210 function n = getNumAlternatives(self)
211 % N = GETNUMALTERNATIVES()
212 % Return number of alternative distributions.
213 % A continuous prior has no alternatives until discretize() is
214 % called, so this returns NaN to force callers to discretize.
215 if strcmp(self.kind, 'continuous
')
219 n = length(self.distributions);
222 function dist = getAlternative(self, idx)
223 % DIST = GETALTERNATIVE(IDX)
224 % Return the distribution at index idx
225 self.assertDiscrete('getAlternative
');
226 if idx < 1 || idx > self.getNumAlternatives()
227 line_error(mfilename, 'Index out of bounds
');
229 dist = self.distributions{idx};
232 function p = getProbability(self, idx)
233 % P = GETPROBABILITY(IDX)
234 % Return the probability of alternative idx
235 self.assertDiscrete('getProbability
');
236 if idx < 1 || idx > self.getNumAlternatives()
237 line_error(mfilename, 'Index out of bounds
');
239 p = self.probabilities(idx);
242 function assertDiscrete(self, caller)
243 % ASSERTDISCRETE(CALLER)
244 % Reject enumeration of a continuous prior.
246 % getNumAlternatives returns NaN for a continuous prior, and every
247 % comparison against NaN is false, so a bounds check alone would
248 % pass and the caller would fault on an empty array instead.
249 if strcmp(self.kind, 'continuous
')
250 line_error(mfilename, sprintf(['%s applies to a discrete prior only.
', ...
251 'A continuous prior has no alternatives until discretize()
is called.
'], caller));
255 function MEAN = getMean(self)
257 % Get prior-weighted mean (expected mean over alternatives)
259 % E[X] = sum_i p_i * E[X_i]
260 [dists, probs] = self.discretize();
262 for i = 1:length(dists)
263 MEAN = MEAN + probs(i) * dists{i}.getMean();
267 function SCV = getSCV(self)
269 % Get prior-weighted SCV using law of total variance
271 % Var(X) = E[Var(X|D)] + Var(E[X|D])
272 % SCV = Var(X) / E[X]^2
274 [dists, probs] = self.discretize();
275 E_mean = 0; % E[E[X|D]]
276 E_var = 0; % E[Var(X|D)]
277 E_mean_sq = 0; % E[E[X|D]^2]
279 for i = 1:length(dists)
280 m = dists{i}.getMean();
281 v = dists{i}.getSCV() * m^2; % Var(X|D=i)
282 E_mean = E_mean + probs(i) * m;
283 E_var = E_var + probs(i) * v;
284 E_mean_sq = E_mean_sq + probs(i) * m^2;
287 % Total variance = E[Var(X|D)] + Var(E[X|D])
288 % Var(E[X|D]) = E[E[X|D]^2] - E[E[X|D]]^2
289 total_var = E_var + (E_mean_sq - E_mean^2);
290 SCV = total_var / E_mean^2;
293 function SKEW = getSkewness(self)
294 % SKEW = GETSKEWNESS()
295 % Get prior-weighted skewness (approximation using mixture formula)
297 % For mixture: use law of total cumulance (simplified)
298 % This is an approximation - exact formula is more complex
300 sigma2 = self.getVar();
301 sigma = sqrt(sigma2);
303 if sigma < GlobalConstants.FineTol
308 % E[(X - mu)^3] via mixture
309 [dists, probs] = self.discretize();
311 for i = 1:length(dists)
312 mi = dists{i}.getMean();
313 vi = dists{i}.getVar();
315 skewi = dists{i}.getSkewness();
317 % E[(Xi - mu)^3] = E[(Xi - mi + mi - mu)^3]
318 % Using binomial expansion
320 % Third central moment of Xi around its own mean
322 % Third central moment of Xi around global mu
323 m3_shifted = m3i + 3*vi*delta + delta^3;
325 third_central = third_central + probs(i) * m3_shifted;
328 SKEW = third_central / sigma^3;
331 function X = sample(self, n)
333 % Sample from prior (mixture sampling)
335 % Samples are drawn from the mixture distribution where each
336 % sample comes from one of the alternatives selected according
337 % to the prior probabilities.
343 % A continuous prior is sampled exactly, by drawing the parameter
344 % and then the variate: discretizing first would return the law of
345 % a quadrature approximation rather than of the prior itself.
346 if strcmp(self.kind, 'continuous
')
347 theta = self.paramDist.sample(n);
349 testSample = self.distFactory(theta(1)).sample(1);
350 X = zeros(n, numel(testSample));
352 s = self.distFactory(theta(i)).sample(1);
358 [dists, probs] = self.discretize();
360 % Determine dimensionality from first distribution
361 testSample = dists{1}.sample(1);
362 d = numel(testSample);
367 cumprob = cumsum(probs);
369 % Select alternative based on probabilities
371 idx = find(r <= cumprob, 1, 'first
');
372 s = dists{idx}.sample(1);
377 function Ft = evalCDF(self, t)
379 % Evaluate mixture CDF at t
381 % F(t) = sum_i p_i * F_i(t)
383 [dists, probs] = self.discretize();
385 for i = 1:length(dists)
386 Ft = Ft + probs(i) * dists{i}.evalCDF(t);
390 function L = evalLST(self, s)
392 % Evaluate mixture Laplace-Stieltjes transform
394 % L(s) = sum_i p_i * L_i(s)
396 [dists, probs] = self.discretize();
398 for i = 1:length(dists)
399 L = L + probs(i) * dists{i}.evalLST(s);
403 function
bool = isPrior(self)
405 % Return
true (used
for detection by UQ solver)
411 function
bool = isPriorDistribution(dist)
412 % BOOL = ISPRIORDISTRIBUTION(DIST)
413 % Check
if a distribution
is a Prior
415 % @param dist Distribution
object to check
416 % @
return bool True
if dist
is a Prior
417 bool = isa(dist,
'Prior');
420 function self = fromSample(k, s, distFactory)
421 % SELF = FROMSAMPLE(K, S, DISTFACTORY)
422 % Continuous prior
for a rate estimated from lifetime data.
424 % Given K i.i.d. observations of an exponential random variable
425 % summing to S,
the Jeffreys improper prior f(lambda) = s/lambda
426 % yields
the posterior density of
the rate
428 % f(lambda|s) = lambda^(k-1) s^k exp(-lambda s) / (k-1)!
430 % which
is an Erlang density with K phases and phase rate S.
431 % See Trivedi and Bobbio (2017), Eq. (3.71). The posterior has mean
432 % K/S, i.e.
the maximum-likelihood rate estimate, and variance
433 % K/S^2, so it concentrates on
the estimate as K grows.
435 % @param k Number of observations (positive integer)
436 % @param s Sum of
the observed lifetimes (positive)
437 % @param distFactory Handle theta -> Distribution, default @(lambda) Exp(lambda)
438 % @return self Prior instance in continuous form
440 if nargin < 3 || isempty(distFactory)
441 distFactory = @(lambda) Exp(lambda);
443 if ~(isscalar(k) && k >= 1 && k == round(k))
444 line_error(mfilename,
'k must be a positive integer number of observations');
446 if ~(isscalar(s) && s > 0)
447 line_error(mfilename,
's must be a positive sum of observed lifetimes');
449 self = Prior(Erlang(s, k), distFactory);
452 function x = quantile(dist, p)
453 % X = QUANTILE(DIST,
P)
454 % Numerical inverse CDF by bisection.
456 % Uses only evalCDF, so it applies to any Distribution. Bracketing
457 % starts from
the mean and doubles outward, which terminates
for
458 % any distribution with finite mean.
460 % @param dist Distribution
object
461 % @param p Probability level in (0,1)
462 % @
return x Value with F(x) = p
465 line_error(mfilename,
'p must lie strictly between 0 and 1');
469 hi = max(dist.getMean(), GlobalConstants.FineTol);
472 if dist.evalCDF(hi) >= p
477 line_error(mfilename,
'Failed to bracket the requested quantile');
483 if dist.evalCDF(mid) < p
488 if (hi - lo) <= GlobalConstants.FineTol * max(1, hi)