LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
ctmc_solve.m
1%{ @file ctmc_solve.m
2 % @brief Equilibrium distribution of the continuous-time Markov chain
3 %
4 % @author LINE Development Team
5%}
6
7%{
8 % @brief Equilibrium distribution of the continuous-time Markov chain
9 %
10 % @details
11 % Calculates the equilibrium distribution of a continuous-time Markov chain given its infinitesimal generator matrix.
12 %
13 % @par Syntax:
14 % @code
15 % p = ctmc_solve(Q)
16 % [p, Q, nConnComp, connComp] = ctmc_solve(Q, options)
17 % @endcode
18 %
19 % @par Parameters:
20 % <table>
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)
24 % </table>
25 %
26 % @par Returns:
27 % <table>
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
33 % </table>
34 %
35 % @par Examples:
36 % @code
37 % Q = [-0.5, 0.5; 0.2, -0.2];
38 % p = ctmc_solve(Q);
39 % @endcode
40%}
41function [p, Q, nConnComp, connComp]=ctmc_solve(Q,options)
42
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;
48
49if size(Q)==1
50 p = 1;
51 nConnComp = 1;
52 connComp = 1:length(Q);
53 return
54end
55
56Q = ctmc_makeinfgen(Q); % so that spurious diagonal elements are set to 0
57n = length(Q);
58
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
65 % stays the default.
66 [p, ~, ~, nConnComp, connComp] = SAGE.solveCTMC(Q, {}, ...
67 SAGE.resolve(options.config.symbolic));
68 p = reshape(p, 1, []);
69 return
70end
71
72if issym(Q)
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
75else
76 B = abs(Q+Q')>0;
77end
78[nConnComp, connComp] = weaklyconncomp(B);
79if nConnComp > 1
80 % reducible generator - solve each component recursively
81 line_warning(mfilename,'Reducible generator. No initial vector available, decomposing and solving each component recursively.\n');
82 if issym(Q)
83 p = sym(zeros(1,n));
84 else
85 p = zeros(1,n);
86 end
87
88 for c=1:nConnComp
89 Qc = Q(connComp==c,connComp==c);
90 Qc = ctmc_makeinfgen(Qc);
91 p(connComp==c) = ctmc_solve(Qc);
92 end
93 p = p /sum(p);
94 return
95end
96
97if all(Q==0)
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.
100 p = ones(1,n)/n;
101 return
102end
103p = zeros(1,n);
104b = zeros(n,1);
105
106nnzel = 1:n;
107Qnnz = Q; bnnz = b;
108Qnnz_1 = Qnnz; bnnz_1 = bnnz;
109
110isReducible = false;
111goon = true;
112while goon
113 nnzel = find(sum(abs(Qnnz),1)~=0 & sum(abs(Qnnz),2)'~=0);
114 if length(nnzel) < n && ~isReducible
115 isReducible = true;
116 if (nargin > 1 && options.verbose == 2) % debug
117 line_warning(mfilename,'The infinitesimal generator is reducible.\n');
118 end
119 end
120 Qnnz = Qnnz(nnzel, nnzel);
121 bnnz = bnnz(nnzel);
122 Qnnz = ctmc_makeinfgen(Qnnz);
123 if all(size(Qnnz_1(:)) == size(Qnnz(:))) && all(size(bnnz_1(:)) == size(bnnz(:)))
124 goon = false;
125 else
126 Qnnz_1 = Qnnz; bnnz_1 = bnnz; nnzel = 1:length(Qnnz);
127 end
128end
129
130if isempty(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.']));
144end
145Qnnz_1 = Qnnz;
146Qnnz(:,end) = 1;
147bnnz_1 = Qnnz;
148bnnz(end) = 1;
149
150if ~isdeployed
151 if issym(Q)
152 p = sym(p);
153 end
154end
155
156warning('off','MATLAB:singularMatrix');
157
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.
162method = 'default';
163if nargin > 1 && isfield(options,'method') && ~isempty(options.method)
164 method = lower(options.method);
165end
166useGmres = ~issym(Q) && (strcmp(method,'gmres') || ...
167 (~strcmp(method,'direct') && length(Qnnz) > GMRES_MIN_STATES));
168if useGmres
169 restart = [];
170 if nargin > 1 && isfield(options,'config') && isfield(options.config,'gmres_restart')
171 restart = options.config.gmres_restart;
172 end
173 maxit = [];
174 if nargin > 1 && isfield(options,'iter_max') && ~isempty(options.iter_max)
175 if isempty(restart)
176 maxit = min(ceil(length(Qnnz)/min(length(Qnnz),50)), options.iter_max);
177 else
178 maxit = min(ceil(length(Qnnz)/restart), options.iter_max);
179 end
180 end
181 [xg,gflag] = ctmc_gmres(Qnnz', bnnz, [], restart, maxit, []);
182 if gflag == 0
183 p(nnzel) = xg;
184 warning('on','MATLAB:singularMatrix');
185 return
186 end
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);
189 end
190end
191
192if nargin == 1
193 p(nnzel)=Qnnz'\ bnnz;
194 if any(isnan(p))
195 % verify if this has become reducible
196 if issym(Qnnz)
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
199 else
200 B = abs(Qnnz+Qnnz')>0;
201 end
202 [nConnComp, connComp] = weaklyconncomp(B);
203 if nConnComp > 1
204 % reducible generator - solve each component recursively
205 if issym(Qnnz)
206 p(nnzel) = sym(zeros(1,n));
207 else
208 p(nnzel) = zeros(1,n);
209 end
210
211 for c=1:nConnComp
212 Qc = Q(connComp==c,connComp==c);
213 Qc = ctmc_makeinfgen(Qc);
214 p(intersect(find(connComp==c),nnzel)) = ctmc_solve(Qc);
215 end
216 p = p /sum(p);
217 return
218 end
219 end
220else
221 if ~isfield(options, 'method')
222 options.method = 'default';
223 end
224 switch options.method
225 case 'gpu'
226 try
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
232 catch
233 warning('ctmc_solve: GPU either not available or execution failed. Switching to default method.');
234 p(nnzel) = Qnnz'\ bnnz;
235 end
236 otherwise
237 p(nnzel)=Qnnz'\ bnnz;
238 end
239end
240
241if issym(Q)
242 Q=simplify(Q);
243end
244warning('on','MATLAB:singularMatrix');
245end