1function [QN,UN,RN,TN,CN,XN,InfGen,StateSpace,StateSpaceAggr,EventFiltration,runtime,fname,sncopy] = solver_ctmc_analyzer(sn, options)
2% [QN,UN,RN,TN,CN,XN,INFGEN,STATESPACE,STATESPACEAGGR,EVENTFILTRATION,RUNTIME,FNAME,sn] = SOLVER_CTMC_ANALYZER(sn, OPTIONS)
4% Copyright (c) 2012-2026, Imperial College London
10% qn_json = jsonencode(sn);
11% sn = NetworkStruct.fromJSON(qn_json)
15M = sn.nstations; %number of stations
16K = sn.nclasses; %number of
classes
18NK = sn.njobs
'; % initial population per class
24line_debug('CTMC analyzer starting: nstations=%d, nclasses=%d, njobs=%s
', M, K, mat2str(NK));
26% Note: hide_immediate now selectively preserves Cache immediate transitions
27% in solver_ctmc.m, so we no longer need to disable it entirely for Cache nodes
29line_debug('Building state space and infinitesimal generator via solver_ctmc
');
30[InfGen,StateSpace,StateSpaceAggr,EventFiltration,arvRates,depRates,sn] = solver_ctmc(sn, options); % sn is updated with the state space
32% if the initial state does not reflect the final size of the state
33% vectors, attempt to correct it
35 if size(sn.state{isf},2) < size(sn.space{isf},2)
36 sn.state{isf} = [zeros(1,size(sn.space{isf},2)-size(sn.state{isf},2)),sn.state{isf}];
42 line_debug('Saving CTMC data to file (options.keep=
true)
');
44 save([fname,'.mat
'],'InfGen
','StateSpace
','StateSpaceAggr
','EventFiltration
')
45 line_printf('CTMC infinitesimal generator and state space saved in:
');
46 line_printf(strrep(sprintf('%s.mat\n
',fname),'\
','\\
'))
51wset = 1:length(InfGen);
53line_debug('State space built: %d states, solving CTMC
', length(InfGen));
55use_ctmc_solve_stable = true;
56if use_ctmc_solve_stable
58 % Note: solver_ctmc now selectively preserves Cache immediate transitions
59 % to enable hit/miss rate computation while hiding other immediate transitions
60 [probSysState, ~, nConnComp, connComp] = ctmc_solve(InfGen, options);
63 line_debug('CTMC
is reducible: %d connected components
', nConnComp);
64 % the matrix was reducible
65 initState = matchrow(StateSpace, cell2mat(sn.state'));
67 % Initial state may have been removed by stochcomp (e.g., SPN with
68 % immediate ENABLE states). Use the largest connected component.
69 compSizes = accumarray(connComp(:), 1);
70 [~, largestComp] = max(compSizes);
71 wset = find(connComp == largestComp);
73 % determine the weakly connected component associated to the initial state
74 wset = find(connComp == connComp(initState));
77 line_debug(
'Using component %d with %d states (from initial state)', connComp(initState), length(wset));
79 line_debug(
'Using largest component with %d states (initial state removed by stochcomp)', length(wset));
81 probSysState = ctmc_solve(InfGen(wset, wset), options);
82 InfGen = InfGen(wset, wset);
83 % reduce all per-state arrays to the retained component and remap wset
84 % to local indices so matrix-form and loop-form estimators stay aligned
85 StateSpace = StateSpace(wset,:);
86 StateSpaceAggr = StateSpaceAggr(wset,:);
87 arvRates = arvRates(wset,:,:);
88 depRates = depRates(wset,:,:);
91 line_debug(
'CTMC is irreducible, using full state space');
96 % we now find the initial state and then solver the CTMC allowing
for the
97 %
case where it
is reducible
98 initState = matchrow(StateSpace, cell2mat(sn.state
'));
99 pi0 = zeros(1,length(InfGen)); pi0(initState) = 1.0;
100 [pi,pis,~,scc,~] = ctmc_solve_reducible(InfGen, pi0, options);
105 wset = scc == scc(initState);
106 InfGen = InfGen(wset, wset);
107 StateSpace = StateSpace(wset,:);
108 probSysState = pis(scc(initState),scc == scc(initState));
111probSysState(probSysState<GlobalConstants.Zero)=0;
112probSysState = probSysState/sum(probSysState);
121istSpaceShift = zeros(1,M);
124 istSpaceShift(ist) = 0;
126 istSpaceShift(ist) = istSpaceShift(ist-1) + size(sn.space{ist-1},2);
131 refsf = sn.stationToStateful(sn.refstat(k));
132 XN(k) = probSysState*arvRates(wset,refsf,k);
136 isf = sn.stationToStateful(ist);
137 ind = sn.stationToNode(ist);
139 TN(ist,k) = probSysState*depRates(wset,isf,k);
140 QN(ist,k) = probSysState*StateSpaceAggr(wset,(ist-1)*K+k);
142 if sn.nodetype(ind) ~= NodeType.Source
144 case SchedStrategy.INF
146 UN(ist,k) = QN(ist,k);
148 case {SchedStrategy.PS, SchedStrategy.DPS, SchedStrategy.GPS, SchedStrategy.LPS}
149 if isempty(sn.lldscaling) && isempty(sn.cdscaling) && ~sn_has_joint_dependence(sn)
151 if ~isempty(PH{ist}{k})
152 % There are cases where due to remove of
153 % immediate transitions or due to cutoff the
154 % utilization estimator based on arrivals or
155 % departures can be under-estimated. E.g.:
156 % UNarv_ik doesn't work well with example_stateDependentRouting_3
157 % UNdep_ik doesn
't work well with test_OQN_JMT_6
158 % Therefore, we take the maximum of the two
159 % Note: the two estimators are normally
161 UNarv_ik = probSysState*arvRates(wset,isf,k)*map_mean(PH{ist}{k})/S(ist);
162 UNdep_ik = TN(ist,k)*map_mean(PH{ist}{k})/S(ist); % this is valid because CS in LINE is in a separate node
163 UN(ist,k) = max(UNarv_ik,UNdep_ik);
166 else % lld/cd/ljd cases
167 ind = sn.stationToNode(ist);
170 [ni,nir] = State.toMarginal(sn, ind, StateSpace(st,(istSpaceShift(ist)+1):(istSpaceShift(ist)+size(sn.space{ist},2))));
173 UN(ist,k) = UN(ist,k) + probSysState(st)*nir(k)*sn.schedparam(ist,k)/(nir*sn.schedparam(ist,:)');
178 case SchedStrategy.PAS
179 % Pass-and-swap (order-independent): utilization
is the
180 % time-average number of in-service jobs per class divided by
181 % the number of servers. sir already counts the positions whose
182 % marginal rate increment
is positive (toMarginal PAS branch),
183 % so a single job served by multiple server types still counts
184 % as one in-service job (not 1/rate).
185 ind = sn.stationToNode(ist);
188 [~,~,sir] = State.toMarginal(sn, ind, StateSpace(st,(istSpaceShift(ist)+1):(istSpaceShift(ist)+size(sn.space{ist},2))));
190 UN(ist,k) = UN(ist,k) + probSysState(st)*sir(k)/S(ist);
194 if isempty(sn.lldscaling) && isempty(sn.cdscaling) && ~sn_has_joint_dependence(sn)
196 if ~isempty(PH{ist}{k})
197 % There are cases where due to remove of
198 % immediate transitions or due to cutoff the
199 % utilization estimator based on arrivals or
200 % departures can be under-estimated. E.g.:
201 % UNarv_ik doesn
't work well with example_stateDependentRouting_3
202 % UNdep_ik doesn't work well with test_OQN_JMT_6
203 % Therefore, we take the maximum of the two
204 % Note: the two estimators are normally
206 UNarv_ik = probSysState*arvRates(wset,isf,k)*map_mean(PH{ist}{k})/S(ist);
207 UNdep_ik = TN(ist,k)*map_mean(PH{ist}{k})/S(ist); %
this is valid because CS in LINE
is in a separate node
208 UN(ist,k) = max(UNarv_ik,UNdep_ik);
211 else % lld/cd/ljd cases
212 ind = sn.stationToNode(ist);
215 [ni,~,sir] = State.toMarginal(sn, ind, StateSpace(st,(istSpaceShift(ist)+1):(istSpaceShift(ist)+size(sn.space{ist},2))));
218 UN(ist,k) = UN(ist,k) + probSysState(st)*sir(k)/S(ist);
230 RN(ist,k) = QN(ist,k)./TN(ist,k);
235 CN(k) = NK(k)./XN(k);
245runtime = toc(Tstart);
247% now update the routing probabilities in
nodes with state-dependent routing
248TNcache = zeros(sn.nstateful,K);
249XNcache = zeros(sn.nstateful,K);
251 for isf=1:sn.nstateful
252 ind = sncopy.statefulToNode(isf);
253 if sncopy.nodetype(ind) == NodeType.Cache
254 TNcache(isf,k) = probSysState*depRates(wset,isf,k);
255 XNcache(isf,k) = probSysState*arvRates(wset,isf,k);
260% updates cache actual hit and miss data + retrieval-system expected latency.
261% Hit / miss
for class k are derived from departure rates of the configured
262% hitClass / missClass at the cache;
for retrieval-aware caches the
263% miss-
class departure rate equals the true miss rate because
264% afterEventCache fires the miss
event on a retrieval-complete READ.
265retrievalLatencyWarned =
false;
267 for isf=1:sncopy.nstateful
268 ind = sncopy.statefulToNode(isf);
269 if sncopy.nodetype(ind) == NodeType.Cache
270 np = sncopy.nodeparam{ind};
271 if length(np.hitclass)>=k
275 sncopy.nodeparam{ind}.actualhitprob(k) = TNcache(isf,h)/sum(TNcache(isf,[h,m]));
276 sncopy.nodeparam{ind}.actualmissprob(k) = TNcache(isf,m)/sum(TNcache(isf,[h,m]));
278 sncopy.nodeparam{ind}.actualhitprob(k) = NaN;
279 sncopy.nodeparam{ind}.actualmissprob(k) = NaN;
282 % The Eq. 8 retrieval-system expected latency
is not currently
283 % implemented; report NaN whenever a retrieval system
is
284 % configured
for this class.
285 expectedLatency = NaN;
286 if isfield(np,
'retrievalSystemQueueIndices') ...
287 && isKey(np.retrievalSystemQueueIndices, int32(k-1)) ...
288 && ~isempty(np.retrievalSystemQueueIndices(int32(k-1)))
289 if ~retrievalLatencyWarned
290 line_warning(mfilename, 'Retrieval-system expected latency
is not currently implemented; reporting NaN.');
291 retrievalLatencyWarned = true;
294 sncopy.nodeparam{ind}.actualresidt(k) = expectedLatency;