2 % @brief Equilibrium distribution of the continuous-time Markov chain
4 % @author LINE Development Team
8 % @brief Equilibrium distribution of the continuous-time Markov chain
11 % Calculates the equilibrium distribution of a continuous-time Markov chain given its infinitesimal generator matrix.
16 % [p, Q, nConnComp, connComp] = ctmc_solve(Q, options)
21 % <tr><th>Name<th>Description
22 % <tr><td>Q<td>Infinitesimal generator matrix of the continuous-time Markov chain
23 % <tr><td>options<td>(Optional) Solver options (method: 'gpu' or default, force: boolean, verbose: 2 for debug)
28 % <tr><th>Name<th>Description
29 % <tr><td>p<td>Equilibrium distribution vector
30 % <tr><td>Q<td>Processed generator matrix (e.g., after removing spurious zeros)
31 % <tr><td>nConnComp<td>Number of connected components found (if reducible)
32 % <tr><td>connComp<td>Vector assigning each state to a connected component
37 % Q = [-0.5, 0.5; 0.2, -0.2];
41function [p, Q, nConnComp, connComp]=ctmc_solve(Q,options)
43% Order above which the direct sparse factorization
is abandoned in favour of
44% GMRES. The former blocking prompt at
this size
is gone: it warned before a
45% solve that would exhaust memory, and there
is now an iterative path that does
46% not, with the direct solve retained as the fallback when GMRES fails.
47GMRES_MIN_STATES = 6000;
52 connComp = 1:length(Q);
56Q = ctmc_makeinfgen(Q); % so that spurious diagonal elements are set to 0
59if issym(Q) && nargin > 1 && isfield(options,
'config') && isfield(options.config,
'symbolic') ...
60 && (strcmpi(options.config.symbolic,
'sage') || strncmpi(options.config.symbolic,
'http',4))
61 % Symbolic solve delegated to the computer algebra backend (SAGE.m). The
62 % same request from MATLAB, the JAR and native Python then returns the
63 % same normal
form, which
is what makes symbolic results comparable
64 % across the three codebases. The toolbox path below
is unchanged and
66 [p, ~, ~, nConnComp, connComp] = SAGE.solveCTMC(Q, {}, ...
67 SAGE.resolve(options.config.symbolic));
68 p = reshape(p, 1, []);
73 symvariables = symvar(Q); % find all symbolic variables
74 B = double(subs(Q+Q
',symvariables,ones(size(symvariables)))); % replace all symbolic variables with 1.0
78[nConnComp, connComp] = weaklyconncomp(B);
80 % reducible generator - solve each component recursively
81 line_warning(mfilename,'Reducible generator. No initial vector available, decomposing and solving each component recursively.\n');
89 Qc = Q(connComp==c,connComp==c);
90 Qc = ctmc_makeinfgen(Qc);
91 p(connComp==c) = ctmc_solve(Qc);
98 % No transitions at all: every distribution satisfies p*Q=0, so the
99 % stationary distribution
is not unique and uniform
is as good as any.
108Qnnz_1 = Qnnz; bnnz_1 = bnnz;
113 nnzel = find(sum(abs(Qnnz),1)~=0 & sum(abs(Qnnz),2)
'~=0);
114 if length(nnzel) < n && ~isReducible
116 if (nargin > 1 && options.verbose == 2) % debug
117 line_warning(mfilename,'The infinitesimal generator
is reducible.\n
');
120 Qnnz = Qnnz(nnzel, nnzel);
122 Qnnz = ctmc_makeinfgen(Qnnz);
123 if all(size(Qnnz_1(:)) == size(Qnnz(:))) && all(size(bnnz_1(:)) == size(bnnz(:)))
126 Qnnz_1 = Qnnz; bnnz_1 = bnnz; nnzel = 1:length(Qnnz);
131 % The elimination above drops every state whose row is all-zero, which is
132 % precisely an ABSORBING state; ctmc_makeinfgen then re-zeroes the diagonal
133 % of the survivors that only fed it, so the elimination cascades until
134 % nothing is left. Returning a uniform vector here does NOT satisfy p*Q=0
135 % (it is not a stationary distribution, just a shape of the right size), and
136 % a caller cannot tell it apart from a real answer: a generator missing all
137 % its arrivals reads back as a plausible mean of cutoff/2. Fail instead.
138 % A genuinely absorbing chain has no unique stationary distribution without
139 % an initial vector, so it belongs in ctmc_solve_reducible(Q, pi0).
140 line_error(mfilename, sprintf(['The infinitesimal generator has no recurrent state: every state was eliminated as absorbing.\n
' ...
141 'This generator admits no unique stationary distribution. It usually means the generator
is malformed --
' ...
142 'e.g. a state with no outgoing transitions that absorbs the whole chain, as happens when a class of
' ...
143 'transitions was dropped while building it. Use ctmc_solve_reducible(Q, pi0) for a genuinely absorbing chain.
']));
156warning('off
','MATLAB:singularMatrix
');
158% Iterative path. The direct solve stays the default and remains the fallback:
159% GMRES is used only above GMRES_MIN_STATES, or when explicitly requested, and
160% only when it reports convergence. A symbolic generator always takes the direct
161% path, there being no iterative method over a symbolic field.
163if nargin > 1 && isfield(options,'method
') && ~isempty(options.method)
164 method = lower(options.method);
166useGmres = ~issym(Q) && (strcmp(method,'gmres
') || ...
167 (~strcmp(method,'direct
') && length(Qnnz) > GMRES_MIN_STATES));
170 if nargin > 1 && isfield(options,'config
') && isfield(options.config,'gmres_restart
')
171 restart = options.config.gmres_restart;
174 if nargin > 1 && isfield(options,'iter_max
') && ~isempty(options.iter_max)
176 maxit = min(ceil(length(Qnnz)/min(length(Qnnz),50)), options.iter_max);
178 maxit = min(ceil(length(Qnnz)/restart), options.iter_max);
181 [xg,gflag] = ctmc_gmres(Qnnz', bnnz, [], restart, maxit, []);
184 warning(
'on',
'MATLAB:singularMatrix');
187 if nargin > 1 && isfield(options,
'verbose') && options.verbose == 2
188 line_warning(mfilename,
'GMRES did not converge (flag %d), falling back to the direct solve.\n', gflag);
193 p(nnzel)=Qnnz
'\ bnnz;
195 % verify if this has become reducible
197 symvariables = symvar(Qnnz); % find all symbolic variables
198 B = double(subs(Qnnz+Qnnz',symvariables,ones(size(symvariables)))); % replace all symbolic variables with 1.0
200 B = abs(Qnnz+Qnnz
')>0;
202 [nConnComp, connComp] = weaklyconncomp(B);
204 % reducible generator - solve each component recursively
206 p(nnzel) = sym(zeros(1,n));
208 p(nnzel) = zeros(1,n);
212 Qc = Q(connComp==c,connComp==c);
213 Qc = ctmc_makeinfgen(Qc);
214 p(intersect(find(connComp==c),nnzel)) = ctmc_solve(Qc);
221 if ~isfield(options, 'method
')
222 options.method = 'default';
224 switch options.method
227 gQnnz = gpuArray(Qnnz');
228 gbnnz = gpuArray(bnnz);
229 pGPU = gQnnz \ gbnnz;
230 gathered_pGPU = gather(pGPU);
231 p(nnzel) = gathered_pGPU; % transfer from GPU to local env
233 warning(
'ctmc_solve: GPU either not available or execution failed. Switching to default method.');
234 p(nnzel) = Qnnz
'\ bnnz;
237 p(nnzel)=Qnnz'\ bnnz;
244warning(
'on',
'MATLAB:singularMatrix');