LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
Prior.m
1classdef Prior < Distribution
2 % Prior Discrete prior distribution over alternative distributions
3 %
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.
8 %
9 % This is NOT a mixture distribution - each alternative represents a
10 % separate model realization with its associated prior probability.
11 %
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.
21 %
22 % @brief Discrete or continuous prior for parameter uncertainty modeling
23 %
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
28 %
29 % Example:
30 % @code
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);
34 %
35 % % Continuous form: rate is itself Erlang-distributed
36 % prior = Prior(Erlang(10, 3), @(lambda) Exp(lambda));
37 %
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);
41 %
42 % % Solve with UQ wrapper
43 % post = UQ(model, @SolverMVA);
44 % avgTable = post.getAvgTable(); % Prior-weighted expectations
45 % postTable = post.getPosteriorTable(); % Per-alternative breakdown
46 % @endcode
47 %
48 % Copyright (c) 2012-2026, Imperial College London
49 % All rights reserved.
50
51 properties
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'
57 end
58
59 methods
60 function self = Prior(varargin)
61 % PRIOR Create a prior distribution instance
62 %
63 % @brief Creates a Prior in either discrete or continuous form
64 %
65 % PRIOR(DISTRIBUTIONS, PROBABILITIES) discrete form.
66 % @param distributions Cell array of Distribution objects
67 % @param probabilities Vector of probabilities (must sum to 1)
68 %
69 % PRIOR(PARAMDIST, DISTFACTORY) continuous form.
70 % @param paramDist Distribution of the scalar parameter theta
71 % @param distFactory Handle theta -> Distribution
72 %
73 % @return self Prior instance
74
75 self@Distribution('Prior', 2, [0, Inf]);
76
77 if nargin ~= 2
78 line_error(mfilename, 'Prior requires two arguments: (distributions, probabilities) or (paramDist, distFactory)');
79 end
80
81 if isa(varargin{1}, 'Distribution') && isa(varargin{2}, 'function_handle')
82 % Continuous form
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);
90 return;
91 end
92
93 % Discrete form
94 self.kind = 'discrete';
95 distributions = varargin{1};
96 probabilities = varargin{2};
97
98 % Validate distributions input
99 if ~iscell(distributions)
100 line_error(mfilename, 'distributions must be a cell array');
101 end
102 if isempty(distributions)
103 line_error(mfilename, 'distributions cannot be empty');
104 end
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));
108 end
109 end
110
111 % Validate probabilities input
112 if length(distributions) ~= length(probabilities)
113 line_error(mfilename, 'Number of distributions must match number of probabilities');
114 end
115 if abs(sum(probabilities) - 1) > GlobalConstants.CoarseTol
116 line_error(mfilename, sprintf('Probabilities must sum to 1 (current sum: %f)', sum(probabilities)));
117 end
118 if any(probabilities < 0)
119 line_error(mfilename, 'Probabilities must be non-negative');
120 end
121
122 self.distributions = distributions(:)'; % row cell array
123 self.probabilities = probabilities(:)'; % row vector
124
125 setParam(self, 1, 'distributions', distributions);
126 setParam(self, 2, 'probabilities', probabilities);
127 end
128
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');
135 end
136
137 function [dists, weights] = discretize(self, n, method)
138 % [DISTS, WEIGHTS] = DISCRETIZE(N, METHOD)
139 % Reduce the prior to N weighted alternatives.
140 %
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.
154 %
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
159
160 if nargin < 2 || isempty(n)
161 n = 11;
162 end
163 if nargin < 3 || isempty(method)
164 method = 'quadrature';
165 end
166 if ~any(strcmp(method, {'quadrature', 'montecarlo'}))
167 line_error(mfilename, sprintf('Unknown discretization method: %s', method));
168 end
169
170 if strcmp(self.kind, 'discrete')
171 switch method
172 case 'quadrature'
173 dists = self.distributions;
174 weights = self.probabilities;
175 case 'montecarlo'
176 cumprob = cumsum(self.probabilities);
177 dists = cell(1, n);
178 for i = 1:n
179 idx = find(rand() <= cumprob, 1, 'first');
180 dists{i} = self.distributions{idx};
181 end
182 weights = ones(1, n) / n;
183 end
184 return;
185 end
186
187 switch method
188 case 'quadrature'
189 % Midpoint of each equal-probability stratum
190 p = ((1:n) - 0.5) / n;
191 theta = zeros(1, n);
192 for i = 1:n
193 theta(i) = Prior.quantile(self.paramDist, p(i));
194 end
195 case 'montecarlo'
196 s = self.paramDist.sample(n);
197 theta = s(:)';
198 end
199
200 weights = ones(1, n) / n;
201 dists = cell(1, n);
202 for i = 1:n
203 dists{i} = self.distFactory(theta(i));
204 if ~isa(dists{i}, 'Distribution')
205 line_error(mfilename, 'distFactory must return a Distribution object');
206 end
207 end
208 end
209
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')
216 n = NaN;
217 return;
218 end
219 n = length(self.distributions);
220 end
221
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');
228 end
229 dist = self.distributions{idx};
230 end
231
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');
238 end
239 p = self.probabilities(idx);
240 end
241
242 function assertDiscrete(self, caller)
243 % ASSERTDISCRETE(CALLER)
244 % Reject enumeration of a continuous prior.
245 %
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));
252 end
253 end
254
255 function MEAN = getMean(self)
256 % MEAN = GETMEAN()
257 % Get prior-weighted mean (expected mean over alternatives)
258 %
259 % E[X] = sum_i p_i * E[X_i]
260 [dists, probs] = self.discretize();
261 MEAN = 0;
262 for i = 1:length(dists)
263 MEAN = MEAN + probs(i) * dists{i}.getMean();
264 end
265 end
266
267 function SCV = getSCV(self)
268 % SCV = GETSCV()
269 % Get prior-weighted SCV using law of total variance
270 %
271 % Var(X) = E[Var(X|D)] + Var(E[X|D])
272 % SCV = Var(X) / E[X]^2
273
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]
278
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;
285 end
286
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;
291 end
292
293 function SKEW = getSkewness(self)
294 % SKEW = GETSKEWNESS()
295 % Get prior-weighted skewness (approximation using mixture formula)
296
297 % For mixture: use law of total cumulance (simplified)
298 % This is an approximation - exact formula is more complex
299 mu = self.getMean();
300 sigma2 = self.getVar();
301 sigma = sqrt(sigma2);
302
303 if sigma < GlobalConstants.FineTol
304 SKEW = 0;
305 return;
306 end
307
308 % E[(X - mu)^3] via mixture
309 [dists, probs] = self.discretize();
310 third_central = 0;
311 for i = 1:length(dists)
312 mi = dists{i}.getMean();
313 vi = dists{i}.getVar();
314 si = sqrt(vi);
315 skewi = dists{i}.getSkewness();
316
317 % E[(Xi - mu)^3] = E[(Xi - mi + mi - mu)^3]
318 % Using binomial expansion
319 delta = mi - mu;
320 % Third central moment of Xi around its own mean
321 m3i = skewi * si^3;
322 % Third central moment of Xi around global mu
323 m3_shifted = m3i + 3*vi*delta + delta^3;
324
325 third_central = third_central + probs(i) * m3_shifted;
326 end
327
328 SKEW = third_central / sigma^3;
329 end
330
331 function X = sample(self, n)
332 % X = SAMPLE(N)
333 % Sample from prior (mixture sampling)
334 %
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.
338
339 if nargin < 2
340 n = 1;
341 end
342
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);
348 theta = theta(:)';
349 testSample = self.distFactory(theta(1)).sample(1);
350 X = zeros(n, numel(testSample));
351 for i = 1:n
352 s = self.distFactory(theta(i)).sample(1);
353 X(i,:) = s(:)';
354 end
355 return;
356 end
357
358 [dists, probs] = self.discretize();
359
360 % Determine dimensionality from first distribution
361 testSample = dists{1}.sample(1);
362 d = numel(testSample);
363
364 X = zeros(n, d);
365
366 % Generate samples
367 cumprob = cumsum(probs);
368 for i = 1:n
369 % Select alternative based on probabilities
370 r = rand();
371 idx = find(r <= cumprob, 1, 'first');
372 s = dists{idx}.sample(1);
373 X(i,:) = s(:)';
374 end
375 end
376
377 function Ft = evalCDF(self, t)
378 % FT = EVALCDF(T)
379 % Evaluate mixture CDF at t
380 %
381 % F(t) = sum_i p_i * F_i(t)
382
383 [dists, probs] = self.discretize();
384 Ft = 0;
385 for i = 1:length(dists)
386 Ft = Ft + probs(i) * dists{i}.evalCDF(t);
387 end
388 end
389
390 function L = evalLST(self, s)
391 % L = EVALLST(S)
392 % Evaluate mixture Laplace-Stieltjes transform
393 %
394 % L(s) = sum_i p_i * L_i(s)
395
396 [dists, probs] = self.discretize();
397 L = 0;
398 for i = 1:length(dists)
399 L = L + probs(i) * dists{i}.evalLST(s);
400 end
401 end
402
403 function bool = isPrior(self)
404 % BOOL = ISPRIOR()
405 % Return true (used for detection by UQ solver)
406 bool = true;
407 end
408 end
409
410 methods (Static)
411 function bool = isPriorDistribution(dist)
412 % BOOL = ISPRIORDISTRIBUTION(DIST)
413 % Check if a distribution is a Prior
414 %
415 % @param dist Distribution object to check
416 % @return bool True if dist is a Prior
417 bool = isa(dist, 'Prior');
418 end
419
420 function self = fromSample(k, s, distFactory)
421 % SELF = FROMSAMPLE(K, S, DISTFACTORY)
422 % Continuous prior for a rate estimated from lifetime data.
423 %
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
427 %
428 % f(lambda|s) = lambda^(k-1) s^k exp(-lambda s) / (k-1)!
429 %
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.
434 %
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
439
440 if nargin < 3 || isempty(distFactory)
441 distFactory = @(lambda) Exp(lambda);
442 end
443 if ~(isscalar(k) && k >= 1 && k == round(k))
444 line_error(mfilename, 'k must be a positive integer number of observations');
445 end
446 if ~(isscalar(s) && s > 0)
447 line_error(mfilename, 's must be a positive sum of observed lifetimes');
448 end
449 self = Prior(Erlang(s, k), distFactory);
450 end
451
452 function x = quantile(dist, p)
453 % X = QUANTILE(DIST, P)
454 % Numerical inverse CDF by bisection.
455 %
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.
459 %
460 % @param dist Distribution object
461 % @param p Probability level in (0,1)
462 % @return x Value with F(x) = p
463
464 if p <= 0 || p >= 1
465 line_error(mfilename, 'p must lie strictly between 0 and 1');
466 end
467
468 lo = 0;
469 hi = max(dist.getMean(), GlobalConstants.FineTol);
470 maxExpand = 200;
471 for i = 1:maxExpand
472 if dist.evalCDF(hi) >= p
473 break;
474 end
475 hi = hi * 2;
476 if i == maxExpand
477 line_error(mfilename, 'Failed to bracket the requested quantile');
478 end
479 end
480
481 for i = 1:200
482 mid = (lo + hi) / 2;
483 if dist.evalCDF(mid) < p
484 lo = mid;
485 else
486 hi = mid;
487 end
488 if (hi - lo) <= GlobalConstants.FineTol * max(1, hi)
489 break;
490 end
491 end
492 x = (lo + hi) / 2;
493 end
494 end
495end
Definition Station.m:245