2 % Concentrated Matrix Exponential (CME) distribution
4 % A CME
is the matrix-exponential distribution of odd order 2*n+1 whose
5 % squared coefficient of variation
is (numerically) minimal
for that order,
6 % from
the tables of Horvath, Horvath and Telek. Its SCV decays as O(1/n^2)
7 % and therefore goes far below
the Erlang bound 1/order attainable by a
8 % phase-type distribution of
the same order: order 101 gives SCV 3.9e-4,
9 % where Erlang-101 gives 9.9e-3.
11 % The density of
the unit-mean CME with n harmonic terms
is
13 % f(x) = mu1*exp(-mu1*x)*(c + sum_k [a_k*cos(k*w*mu1*x) + b_k*sin(k*w*mu1*x)])
15 % with w = omega, which
is exactly alpha*expm(A*x)*(-A*e)
for the
16 % block-diagonal A = blkdiag(-mu1, mu1*[-1 -k*w; k*w -1], k=1..n). The
17 % parameters a, b, c, omega and mu1 are read from
the same iltcme.json
18 % table used by
the CME inverse Laplace transform (matlab_ilt).
20 % Copyright (c) 2012-2026, Imperial College London
21 % All rights reserved.
24 cmeMean; % Mean of
the distribution
25 cmeOrder; % Number of phases, an odd integer 2*n+1
29 function self = CME(mean, order)
30 % SELF = CME(MEAN, ORDER)
31 % Create a concentrated matrix-exponential distribution
33 % @param mean Mean of
the distribution (positive)
34 % @param order Number of phases, an odd integer 2*n+1 with n in
the table
35 % @
return self CME distribution instance
37 [alpha, A] = CME.representation(order);
39 if ~isscalar(mean) || ~isfinite(mean) || mean <= 0
40 line_error(mfilename,
'CME mean must be a positive finite number.');
43 % The CME density
is nonnegative by construction, so
the density
44 % scan of ME
is skipped: it can never fire and it costs O(1e5)
45 % propagations of a (2n+1)-square matrix, prohibitive at high order.
46 self@ME(alpha, A/mean,
false);
49 self.cmeOrder = double(order);
51 % The process type stays ME: a CME
is a matrix-exponential
52 % representation, so every solver gate, sn.procid entry and JSON key
53 % that accepts ME accepts it unchanged.
54 self.obj = jline.lang.processes.CME(mean,
double(order));
56 % mean and order stay
object properties rather than distribution
57 % parameters:
the parameter list of an ME
is (alpha, A), and every
58 % marshaller that serializes an ME reads exactly those two, so
59 % appending to it would change getNumParams
for the ME family.
60 % The tabulated minimum SCV
for this order
is recomputed from
the
61 % representation by getSCV, so it
is not stored either.
64 function o = getOrder(self)
66 % Get
the CME order, i.e.
the number of phases
72 function params = table()
74 % Load and cache
the CME parameter table from iltcme.json
76 % The same table backs matlab_ilt, which caches it in
the global
77 % cmeParams, so
the two share one decode per session.
81 cmeParams = jsondecode(fileread(
'iltcme.json'));
86 function entry = tableEntry(order)
87 % ENTRY = TABLEENTRY(ORDER)
88 % Select
the CME table entry realizing
the given number of phases
90 % The table
is keyed by
the number of harmonic terms n, so an order
91 % of 2*n+1 phases maps to
the entries with that n. Several entries
92 % can share an n (
the 'full' and
'approx' optimizations), and
the
93 % most concentrated one
is taken, matching
the selection rule of
the
94 % CME inverse Laplace transform.
96 order = double(order);
97 if ~isscalar(order) || order < 3 || mod(order,2) == 0 || order ~= fix(order)
98 line_error(mfilename, sprintf(
'CME order must be an odd integer of the form 2*n+1 with n >= 1, got %g.', order));
102 params = CME.table();
105 for i = 1:numel(params)
111 if cand.n == n && cand.cv2 < bestcv2
118 orders = CME.getSupportedOrders();
119 [~,j] = min(abs(orders-order));
120 line_error(mfilename, sprintf(
'No tabulated CME of order %d; the nearest available order is %d. Use CME.getSupportedOrders() for the full list.', order, orders(j)));
124 function orders = getSupportedOrders()
125 % ORDERS = GETSUPPORTEDORDERS()
126 % Get
the sorted list of CME orders (phase counts) in
the table
128 params = CME.table();
129 ns = zeros(numel(params),1);
130 for i = 1:numel(params)
137 orders = unique(2*ns+1)
';
140 function scv = getMinSCV(order)
141 % SCV = GETMINSCV(ORDER)
142 % Get the tabulated minimal SCV attained by a CME of the given order
144 entry = CME.tableEntry(order);
148 function cme = fitMeanAndSCV(mean, scv)
149 % CME = FITMEANANDSCV(MEAN, SCV)
150 % Create the lowest-order CME with the given mean and SCV at most scv
152 % @param mean Target mean
153 % @param scv Target squared coefficient of variation, an upper bound.
154 % The lowest tabulated order whose minimal SCV does not exceed
155 % it is selected, so the result is at least as concentrated as
157 % @return cme CME of that order rescaled to the requested mean
159 params = CME.table();
163 for i = 1:numel(params)
169 mincv2 = min(mincv2, cand.cv2);
170 maxn = max(maxn, cand.n);
172 bestorder = min(bestorder, 2*cand.n+1);
176 if ~isfinite(bestorder)
177 line_error(mfilename, sprintf('No tabulated CME reaches SCV %g;
the most concentrated entry has SCV %g at order %d.
', scv, mincv2, 2*maxn+1));
180 cme = CME(mean, bestorder);
183 function [alpha, A, scv] = representation(order)
184 % [ALPHA, A, SCV] = REPRESENTATION(ORDER)
185 % Build the unit-mean (alpha, A) matrix-exponential form of a CME
187 % A = blkdiag(-mu1, mu1*[-1 -k*w; k*w -1], k=1..n) reproduces the
188 % exponential envelope in its first phase and the k-th harmonic in
189 % its k-th 2x2 rotation block, since expm(mu1*x*[-1 -kw; kw -1]) is
190 % exp(-mu1*x) times the rotation by k*w*mu1*x. The entries of alpha
191 % follow by matching -alpha*expm(A*x)*A*e term by term: with
192 % wk = k*w and d = 2*(1+wk^2),
195 % alpha(2k) = ((1+wk)*a_k - (1-wk)*b_k)/d
196 % alpha(2k+1) = ((1-wk)*a_k + (1+wk)*b_k)/d
198 % The result has unit mean and sums to one, as any (alpha, A) whose
199 % density integrates to one must.
201 entry = CME.tableEntry(order);
211 alpha = zeros(1, sz);
222 alpha(i) = ((1+wk)*a(k) - (1-wk)*b(k))/d;
223 alpha(i+1) = ((1-wk)*a(k) + (1+wk)*b(k))/d;
226 % The tabulated coefficients satisfy the normalization only to about
227 %% 1e-12, and the residual grows with the order, so alpha is rescaled by its own
228 %% sum: alpha*e = 1 is an exact requirement of a matrix-exponential distribution
229 %% (the density integrates to one) and CheckMERepresentation enforces it to
230 %% prec*size. The rescaling changes the density by the same 1e-12 relative amount
232 alpha = alpha / sum(alpha);