LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
ME.m
1classdef ME < Markovian
2 % Matrix Exponential (ME) distribution
3 %
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).
8 %
9 % Copyright (c) 2012-2026, Imperial College London
10 % All rights reserved.
11
12 properties
13 alpha; % Initial vector
14 A; % Matrix parameter
15 end
16
17 methods
18 function self = ME(alpha, A, checkDensity)
19 % ME Create a Matrix Exponential distribution instance
20 %
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
29
30 if nargin < 3 || isempty(checkDensity)
31 checkDensity = true;
32 end
33
34 % Call superclass constructor
35 self@Markovian('ME', 2);
36
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.');
40 end
41
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;
52 if checkDensity
53 [isNegDensity, fmin, tmin] = ME.scanNegativeDensity(alpha, A);
54 end
55 if isNegDensity
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);
57 end
58
59 % Store parameters
60 self.alpha = alpha;
61 self.A = A;
62 self.nPhases = length(alpha);
63
64 % Set parameters
65 setParam(self, 1, 'alpha', alpha);
66 setParam(self, 2, 'A', A);
67
68 % Create Java object
69 alphaMatrix = jline.util.matrix.Matrix(alpha);
70 AMatrix = jline.util.matrix.Matrix(A);
71 self.obj = jline.lang.processes.ME(alphaMatrix, AMatrix);
72
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};
77
78 self.immediate = false;
79 end
80
81 function X = sample(self, n)
82 % X = SAMPLE(N)
83 % Get n samples from the distribution using inverse CDF interpolation
84
85 if nargin < 2
86 n = 1;
87 end
88
89 % Use me_sample for accurate sampling
90 X = me_sample(self.process, n);
91 end
92
93 function phases = getNumberOfPhases(self)
94 % PHASES = GETNUMBEROFPHASES()
95 % Get number of phases in the ME representation
96 phases = self.nPhases;
97 end
98
99 function Ft = evalCDF(self, t)
100 % FT = EVALCDF(SELF, T)
101 % Evaluate the cumulative distribution function at t
102 %
103 % For ME distribution: CDF(t) = 1 - alpha * exp(A*t) * e
104
105 Ft = map_cdf(self.process, t);
106 end
107
108 function ft = evalPDF(self, t)
109 % FT = EVALPDF(SELF, T)
110 % Evaluate the probability density function at t
111 %
112 % For ME distribution: PDF(t) = -alpha * exp(A*t) * A * e
113
114 ft = map_pdf(self.process, t);
115 end
116
117 function L = evalLST(self, s)
118 % L = EVALST(S)
119 % Evaluate the Laplace-Stieltjes transform at s
120
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));
125 end
126
127 function mean_val = getMean(self)
128 % MEAN_VAL = GETMEAN()
129 % Get mean of the ME distribution, m1 = -alpha*inv(A)*e
130 %
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.
138
139 e = ones(self.nPhases, 1);
140 mean_val = -self.alpha * (self.A \ e);
141 end
142
143 function var_val = getVar(self)
144 % VAR_VAL = GETVAR()
145 % Get variance of the ME distribution, m2 - m1^2 with m2 = 2*alpha*inv(A)^2*e
146
147 e = ones(self.nPhases, 1);
148 Ainve = self.A \ e;
149 m1 = -self.alpha * Ainve;
150 m2 = 2 * self.alpha * (self.A \ Ainve);
151 var_val = m2 - m1^2;
152 end
153
154 function scv = getSCV(self)
155 % SCV = GETSCV()
156 % Get squared coefficient of variation, var/mean^2
157
158 scv = self.getVar() / self.getMean()^2;
159 end
160
161 function proc = getProcess(self)
162 % PROC = GETPROCESS()
163 % Get process representation {D0, D1}
164
165 proc = self.process;
166 end
167 end
168
169 methods(Static)
170 function me = fitMoments(moms)
171 % ME = FITMOMENTS(MOMS)
172 % Create ME distribution by fitting the given moments
173 % Uses BuTools MEFromMoments algorithm
174 %
175 % @param moms Array of moments (requires 2*M-1 moments for order M)
176 % @return me ME distribution matching the given moments
177
178 [alpha, A] = MEFromMoments(moms);
179 me = ME(alpha, A);
180 end
181
182 function me = fromExp(rate)
183 % ME = FROMEXP(RATE)
184 % Create ME distribution from exponential distribution
185 % Convenience method showing that Exp is a special case of ME
186 %
187 % @param rate Rate parameter (lambda)
188 % @return me ME distribution equivalent to Exp(rate)
189
190 alpha = 1.0;
191 A = -rate;
192 me = ME(alpha, A);
193 end
194
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
199 %
200 % @param k Number of phases
201 % @param rate Rate parameter for each phase
202 % @return me ME distribution equivalent to Erlang(k, rate)
203
204 alpha = zeros(1, k);
205 alpha(1) = 1.0; % alpha = [1, 0, 0, ..., 0]
206
207 A = zeros(k, k);
208 for i = 1:k
209 A(i, i) = -rate; % diagonal
210 if i < k
211 A(i, i+1) = rate; % super-diagonal
212 end
213 end
214
215 me = ME(alpha, A);
216 end
217
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
222 %
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)
226
227 if length(p) ~= length(rates)
228 error('p and rates must have the same length');
229 end
230
231 k = length(p);
232 alpha = p; % alpha = p
233
234 A = zeros(k, k);
235 for i = 1:k
236 A(i, i) = -rates(i); % diagonal matrix of rates
237 end
238
239 me = ME(alpha, A);
240 end
241
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
245 % value.
246 %
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.
250 %
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.
259 %
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.
265
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
269 scanMinPts = 2001;
270 scanMaxPts = 200001;
271 scanRelTol = 1e-10; % negative only if below -reltol*max|f|
272
273 isNeg = false; fmin = 0; tmin = 0;
274
275 alpha = reshape(alpha, 1, numel(alpha));
276 n = size(A, 1);
277
278 lambda = eig(A);
279 decay = max(real(lambda));
280 if ~isfinite(decay) || decay >= 0
281 % Not a valid ME; CheckMERepresentation has already rejected it.
282 return;
283 end
284 horizon = min(-log(scanTail) / abs(decay), scanHorizonCap);
285
286 npts = scanMinPts;
287 wmax = max(abs(imag(lambda)));
288 if wmax > 0
289 npts = max(npts, ceil(horizon * wmax * scanPerPeriod / (2*pi)) + 1);
290 end
291 npts = min(npts, scanMaxPts);
292
293 step = horizon / (npts - 1);
294 % One matrix exponential, then propagate: v_k = alpha*expm(A*k*step)
295 E = expm(A * step);
296 ve = -(A * ones(n, 1));
297
298 v = alpha;
299 fmin = Inf;
300 tmin = 0;
301 fabsmax = 0;
302 for k = 0:npts-1
303 f = v * ve;
304 if f < fmin
305 fmin = f;
306 tmin = k * step;
307 end
308 if abs(f) > fabsmax
309 fabsmax = abs(f);
310 end
311 v = v * E;
312 end
313
314 isNeg = fmin < -scanRelTol * fabsmax;
315 end
316 end
317end
Definition Station.m:245