2 % Matrix Exponential (ME) distribution
4 % ME distributions are characterized by an initial vector alpha and
5 % a matrix parameter A. They generalize Phase-Type (PH) distributions
6 % by allowing alpha to have entries outside [0,1] and A to have
7 % arbitrary structure (not necessarily a valid sub-generator).
9 % Copyright (c) 2012-2026, Imperial College London
10 % All rights reserved.
13 alpha; % Initial vector
18 function self = ME(alpha, A, checkDensity)
19 % ME Create a Matrix Exponential distribution instance
21 % @brief Creates an ME distribution with
the given initial vector and matrix parameter
22 % @param alpha Initial vector (may have negative entries or sum != 1)
23 % @param A Matrix parameter (must have all eigenvalues with negative real parts)
24 % @param checkDensity Scan
the density
for a negative value (
default true).
25 % Set to
false only by subclasses whose representation
is a density by
26 % construction, such as CME, where
the scan cannot fire and costs O(1e5)
27 % propagations of a large matrix.
28 % @
return self ME distribution instance
30 if nargin < 3 || isempty(checkDensity)
34 % Call superclass constructor
35 self@Markovian(
'ME', 2);
37 % Validate
using BuTools
38 if ~CheckMERepresentation(alpha, A)
39 error(
'Invalid ME representation: Check that A is square, alpha and A have compatible dimensions, all eigenvalues of A have negative real parts, and the dominant eigenvalue is real.');
42 % A valid ME representation may still have a density that goes
43 % negative, which
is not a distribution, so
the density
is scanned
44 %
for an actual negative value. Only a witness
is reported:
the scan
45 % warns when it has found a point where f(t) < 0, and stays silent
46 % otherwise, since no cheap test establishes
the converse. This
is a
47 % warning and not an error, so that a representation whose density
48 % only dips below zero at
the level of round-off remains
49 % constructible. See ME.scanNegativeDensity
for why
50 % CheckMEPositiveDensity
is not used here.
51 isNegDensity =
false; fmin = 0; tmin = 0;
53 [isNegDensity, fmin, tmin] = ME.scanNegativeDensity(alpha, A);
56 line_warning(mfilename,
'The ME representation has a negative density: f(t) = -alpha*expm(A*t)*A*e reaches %g at t = %g. Moments and transforms remain well defined, but evalPDF returns negative values and sample() will not reproduce a proper distribution.\n', fmin, tmin);
62 self.nPhases = length(alpha);
65 setParam(self, 1,
'alpha', alpha);
66 setParam(self, 2,
'A', A);
69 alphaMatrix = jline.util.matrix.Matrix(alpha);
70 AMatrix = jline.util.matrix.Matrix(A);
71 self.obj = jline.lang.processes.ME(alphaMatrix, AMatrix);
73 % Build process representation: {D0=A, D1=-A*e*alpha
'}
74 % where e is column vector of ones
75 e = ones(self.nPhases, 1);
76 self.process = {A, -A * e * alpha};
78 self.immediate = false;
81 function X = sample(self, n)
83 % Get n samples from the distribution using inverse CDF interpolation
89 % Use me_sample for accurate sampling
90 X = me_sample(self.process, n);
93 function phases = getNumberOfPhases(self)
94 % PHASES = GETNUMBEROFPHASES()
95 % Get number of phases in the ME representation
96 phases = self.nPhases;
99 function Ft = evalCDF(self, t)
100 % FT = EVALCDF(SELF, T)
101 % Evaluate the cumulative distribution function at t
103 % For ME distribution: CDF(t) = 1 - alpha * exp(A*t) * e
105 Ft = map_cdf(self.process, t);
108 function ft = evalPDF(self, t)
109 % FT = EVALPDF(SELF, T)
110 % Evaluate the probability density function at t
112 % For ME distribution: PDF(t) = -alpha * exp(A*t) * A * e
114 ft = map_pdf(self.process, t);
117 function L = evalLST(self, s)
119 % Evaluate the Laplace-Stieltjes transform at s
121 % LST(s) = alpha * (sI - A)^(-1) * (-A) * e
122 e = ones(self.nPhases, 1);
123 sI = s * eye(self.nPhases);
124 L = self.alpha * ((sI - self.A) \ (-self.A * e));
127 function mean_val = getMean(self)
128 % MEAN_VAL = GETMEAN()
129 % Get mean of the ME distribution, m1 = -alpha*inv(A)*e
131 % The definition is used rather than map_mean, which obtains the
132 % rate from the stationary vector of D0+D1. That vector is a
133 % probabilistic object of a Markovian process, and solving for it on
134 % an ME degrades with the oscillation of A: a CME of order 101 came
135 % out with a relative error of 5.6e-8 in the mean and 2.6e-4 in the
136 % SCV, where the definition is exact to 1e-13. Native Python
137 % ME.getMean and jline.lang.processes.ME do the same.
139 e = ones(self.nPhases, 1);
140 mean_val = -self.alpha * (self.A \ e);
143 function var_val = getVar(self)
145 % Get variance of the ME distribution, m2 - m1^2 with m2 = 2*alpha*inv(A)^2*e
147 e = ones(self.nPhases, 1);
149 m1 = -self.alpha * Ainve;
150 m2 = 2 * self.alpha * (self.A \ Ainve);
154 function scv = getSCV(self)
156 % Get squared coefficient of variation, var/mean^2
158 scv = self.getVar() / self.getMean()^2;
161 function proc = getProcess(self)
162 % PROC = GETPROCESS()
163 % Get process representation {D0, D1}
170 function me = fitMoments(moms)
171 % ME = FITMOMENTS(MOMS)
172 % Create ME distribution by fitting the given moments
173 % Uses BuTools MEFromMoments algorithm
175 % @param moms Array of moments (requires 2*M-1 moments for order M)
176 % @return me ME distribution matching the given moments
178 [alpha, A] = MEFromMoments(moms);
182 function me = fromExp(rate)
184 % Create ME distribution from exponential distribution
185 % Convenience method showing that Exp is a special case of ME
187 % @param rate Rate parameter (lambda)
188 % @return me ME distribution equivalent to Exp(rate)
195 function me = fromErlang(k, rate)
196 % ME = FROMERLANG(K, RATE)
197 % Create ME distribution from Erlang distribution
198 % Convenience method showing that Erlang is a special case of ME
200 % @param k Number of phases
201 % @param rate Rate parameter for each phase
202 % @return me ME distribution equivalent to Erlang(k, rate)
205 alpha(1) = 1.0; % alpha = [1, 0, 0, ..., 0]
209 A(i, i) = -rate; % diagonal
211 A(i, i+1) = rate; % super-diagonal
218 function me = fromHyperExp(p, rates)
219 % ME = FROMHYPEREXP(P, RATES)
220 % Create ME distribution from HyperExponential distribution
221 % Convenience method showing that HyperExp is a special case of ME
223 % @param p Array of probabilities for each branch
224 % @param rates Array of rates for each branch
225 % @return me ME distribution equivalent to HyperExp(p, rates)
227 if length(p) ~= length(rates)
228 error('p and rates must have
the same length
');
232 alpha = p; % alpha = p
236 A(i, i) = -rates(i); % diagonal matrix of rates
242 function [isNeg, fmin, tmin] = scanNegativeDensity(alpha, A)
243 % [ISNEG, FMIN, TMIN] = SCANNEGATIVEDENSITY(ALPHA, A)
244 % Search the density f(t) = -alpha*expm(A*t)*A*e for a negative
247 % A negative value found here is a witness: it proves that the
248 % representation is not a distribution. Finding none proves
249 % nothing, so the caller must not report the converse.
251 % This replaces CheckMEPositiveDensity as the trigger for the
252 % construction-time warning. That routine searches for a Markovian
253 % monocyclic equivalent, which is a sufficient condition only, and
254 % its verdict depends on the representation rather than on the
255 % distribution: for alpha=[1,0,0], A=[-0.5 0 0; 0 -1 w; 0 -w -1]
256 % the distribution is Exp(0.5) for every w, yet the search fails
257 % once w >= 2*pi. It also costs of the order of a second per call
258 % at search order 1000, which is far too slow for a constructor.
260 % The horizon covers all but scanTail of the mass, using the
261 % dominant (least negative) eigenvalue of A; the sampling rate
262 % resolves the fastest oscillation present, taken from the largest
263 % imaginary part. The constants match native Python
264 % line_solver.distributions.markovian and jline.lang.processes.ME.
266 scanTail = 1e-12; % residual mass left beyond the horizon
267 scanHorizonCap = 1e4; % cap on the horizon for near-degenerate A
268 scanPerPeriod = 20; % samples per period of fastest oscillation
271 scanRelTol = 1e-10; % negative only if below -reltol*max|f|
273 isNeg = false; fmin = 0; tmin = 0;
275 alpha = reshape(alpha, 1, numel(alpha));
279 decay = max(real(lambda));
280 if ~isfinite(decay) || decay >= 0
281 % Not a valid ME; CheckMERepresentation has already rejected it.
284 horizon = min(-log(scanTail) / abs(decay), scanHorizonCap);
287 wmax = max(abs(imag(lambda)));
289 npts = max(npts, ceil(horizon * wmax * scanPerPeriod / (2*pi)) + 1);
291 npts = min(npts, scanMaxPts);
293 step = horizon / (npts - 1);
294 % One matrix exponential, then propagate: v_k = alpha*expm(A*k*step)
296 ve = -(A * ones(n, 1));
314 isNeg = fmin < -scanRelTol * fabsmax;