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
60 symvariables = symvar(Q); % find all symbolic variables
61 B = double(subs(Q+Q
',symvariables,ones(size(symvariables)))); % replace all symbolic variables with 1.0
65[nConnComp, connComp] = weaklyconncomp(B);
67 % reducible generator - solve each component recursively
68 line_warning(mfilename,'Reducible generator. No initial vector available, decomposing and solving each component recursively.\n');
76 Qc = Q(connComp==c,connComp==c);
77 Qc = ctmc_makeinfgen(Qc);
78 p(connComp==c) = ctmc_solve(Qc);
85 % No transitions at all: every distribution satisfies p*Q=0, so
the
86 % stationary distribution
is not unique and uniform
is as good as any.
95Qnnz_1 = Qnnz; bnnz_1 = bnnz;
100 nnzel = find(sum(abs(Qnnz),1)~=0 & sum(abs(Qnnz),2)
'~=0);
101 if length(nnzel) < n && ~isReducible
103 if (nargin > 1 && options.verbose == 2) % debug
104 line_warning(mfilename,'The infinitesimal generator
is reducible.\n
');
107 Qnnz = Qnnz(nnzel, nnzel);
109 Qnnz = ctmc_makeinfgen(Qnnz);
110 if all(size(Qnnz_1(:)) == size(Qnnz(:))) && all(size(bnnz_1(:)) == size(bnnz(:)))
113 Qnnz_1 = Qnnz; bnnz_1 = bnnz; nnzel = 1:length(Qnnz);
118 % The elimination above drops every state whose row is all-zero, which is
119 % precisely an ABSORBING state; ctmc_makeinfgen then re-zeroes the diagonal
120 % of the survivors that only fed it, so the elimination cascades until
121 % nothing is left. Returning a uniform vector here does NOT satisfy p*Q=0
122 % (it is not a stationary distribution, just a shape of the right size), and
123 % a caller cannot tell it apart from a real answer: a generator missing all
124 % its arrivals reads back as a plausible mean of cutoff/2. Fail instead.
125 % A genuinely absorbing chain has no unique stationary distribution without
126 % an initial vector, so it belongs in ctmc_solve_reducible(Q, pi0).
127 line_error(mfilename, sprintf(['The infinitesimal generator has no recurrent state: every state was eliminated as absorbing.\n
' ...
128 'This generator admits no unique stationary distribution. It usually means
the generator
is malformed --
' ...
129 'e.g. a state with no outgoing transitions that absorbs
the whole chain, as happens when a class of
' ...
130 'transitions was dropped while building it. Use ctmc_solve_reducible(Q, pi0) for a genuinely absorbing chain.
']));
143warning('off
','MATLAB:singularMatrix
');
145% Iterative path. The direct solve stays the default and remains the fallback:
146% GMRES is used only above GMRES_MIN_STATES, or when explicitly requested, and
147% only when it reports convergence. A symbolic generator always takes the direct
148% path, there being no iterative method over a symbolic field.
150if nargin > 1 && isfield(options,'method
') && ~isempty(options.method)
151 method = lower(options.method);
153useGmres = ~issym(Q) && (strcmp(method,'gmres
') || ...
154 (~strcmp(method,'direct
') && length(Qnnz) > GMRES_MIN_STATES));
157 if nargin > 1 && isfield(options,'config
') && isfield(options.config,'gmres_restart
')
158 restart = options.config.gmres_restart;
161 if nargin > 1 && isfield(options,'iter_max
') && ~isempty(options.iter_max)
163 maxit = min(ceil(length(Qnnz)/min(length(Qnnz),50)), options.iter_max);
165 maxit = min(ceil(length(Qnnz)/restart), options.iter_max);
168 [xg,gflag] = ctmc_gmres(Qnnz', bnnz, [], restart, maxit, []);
171 warning(
'on',
'MATLAB:singularMatrix');
174 if nargin > 1 && isfield(options,
'verbose') && options.verbose == 2
175 line_warning(mfilename,
'GMRES did not converge (flag %d), falling back to the direct solve.\n', gflag);
180 p(nnzel)=Qnnz
'\ bnnz;
182 % verify if this has become reducible
184 symvariables = symvar(Qnnz); % find all symbolic variables
185 B = double(subs(Qnnz+Qnnz',symvariables,ones(size(symvariables)))); % replace all symbolic variables with 1.0
187 B = abs(Qnnz+Qnnz
')>0;
189 [nConnComp, connComp] = weaklyconncomp(B);
191 % reducible generator - solve each component recursively
193 p(nnzel) = sym(zeros(1,n));
195 p(nnzel) = zeros(1,n);
199 Qc = Q(connComp==c,connComp==c);
200 Qc = ctmc_makeinfgen(Qc);
201 p(intersect(find(connComp==c),nnzel)) = ctmc_solve(Qc);
208 if ~isfield(options, 'method
')
209 options.method = 'default';
211 switch options.method
214 gQnnz = gpuArray(Qnnz');
215 gbnnz = gpuArray(bnnz);
216 pGPU = gQnnz \ gbnnz;
217 gathered_pGPU = gather(pGPU);
218 p(nnzel) = gathered_pGPU; % transfer from GPU to local env
220 warning(
'ctmc_solve: GPU either not available or execution failed. Switching to default method.');
221 p(nnzel) = Qnnz
'\ bnnz;
224 p(nnzel)=Qnnz'\ bnnz;
231warning(
'on',
'MATLAB:singularMatrix');