1function [pi,pis,pi0,scc,isrec] = ctmc_solve_reducible_blkdecomp(Q,pi0,options)
2% [PI,PIS,PI0,SCC,ISREC] = CTMC_SOLVE_REDUCIBLE_BLKDECOMP(Q, PI0, OPTIONS)
4% Compute limiting distribution
for a CTMC with reducible generator Q
5%
using direct block decomposition on the generator matrix.
8% 1. Decompose states into transient and recurrent
classes via SCC
9% 2. For transient states: solve n * Q_tt = -p0_t
for expected sojourn
10% 3. Compute hitting probabilities: h = n * Q_ta + p0_r
11% 4. For each recurrent
class: solve pi_c * Q_cc = 0, scale by hitting prob
14% Q: infinitesimal generator matrix
15% pi0: initial distribution vector, set to []
if not available
16% options:
struct where options.tol sets the tolerance
19% pi: limiting distribution (1 x N)
20% - For an ergodic CTMC,
this is the unique limiting distribution.
21% - For a reducible CTMC:
22% -
if there
is a single transient SCC then
this is the limiting
23% distribution when starting uniformly within it.
24% - otherwise pi
is the weighted average of pis rows.
25% pis: limiting distribution given initialization in a single SCC (numSCC x N)
26% pi0: starting distribution
for each row of pis (numSCC x N)
27% scc: mapping of each state of Q to its SCC index
28% isrec: element i
is true if SCC i
is recurrent
33 options =
struct(
'tol', 1e-12);
42% Ensure valid generator
43Q = ctmc_makeinfgen(Q);
45% Build adjacency from off-diagonal positive entries
48[scc, isrec] = stronglyconncomp(Adj > 0);
51% Irreducible
case: use standard solver
53 pi = ctmc_solve(Q,
struct(
'force',
true));
60scc_idx = cell(1, numSCC);
62 scc_idx{i} = find(scc == i);
66trans_scc_ids = find(~isrec);
67rec_scc_ids = find(isrec);
69% Gather ordered state indices
72 trans_states = [trans_states, scc_idx{i}];
74trans_states = sort(trans_states);
78 rec_states = [rec_states, scc_idx{i}];
80rec_states = sort(rec_states);
82nt = length(trans_states);
83nr = length(rec_states);
85% Extract Q sub-blocks (only when transient states exist)
87 Q_tt = Q(trans_states, trans_states);
88 Q_ta = Q(trans_states, rec_states);
91% Compute per-SCC limiting distributions
92pis = zeros(numSCC, N);
93pi0 = zeros(numSCC, N);
96 % Starting distribution: uniform within SCC s
98 p0(scc_idx{s}) = 1 / length(scc_idx{s});
101 % Compute absorption probabilities into recurrent states
104 p0_t = p0(trans_states);
105 if any(abs(p0_t) > 0)
106 % Solve n * Q_tt = -p0_t
for expected sojourn in transient states
107 % Q_tt
is non-singular (Hurwitz)
for transient states
108 hit = (-p0_t) / Q_tt * Q_ta;
112 % Add initial mass already in recurrent states
113 hit = hit + p0(rec_states);
115 % Solve steady-state per recurrent
class, scaled by hitting probability
118 [~, loc] = ismember(idx_c, rec_states);
119 reachprob = sum(hit(loc));
123 if length(idx_c) == 1
124 % Absorbing state: hitting probability IS the
final probability
125 pis(s, idx_c) = reachprob;
127 % Solve pi_c * Q_cc = 0 within
this recurrent
class
128 pi_c = ctmc_solve(Q(idx_c, idx_c),
struct(
'force',
true));
129 pis(s, idx_c) = pi_c * reachprob;
134% Compute initial SCC probabilities
for weighted average
136 pinl = ones(1, numSCC);
137 % Zero out SCCs containing states with zero column sums (no incoming)
138 col_sums = sum(abs(Q), 1);
139 for j = find(col_sums < 1e-12)
143 pinl = pinl / sum(pinl);
145 pinl = ones(1, numSCC) / numSCC;
148 pinl = zeros(1, numSCC);
150 pinl(i) = sum(pin(scc_idx{i}));
154% Weighted average over starting SCCs
158 pi = pi + pis(i, :) * pinl(i);
162% Special
case: single transient SCC without
explicit initial distribution
163if isscalar(trans_scc_ids) && isempty(pin)
164 pi = pis(trans_scc_ids, :);