1function [t,pit,QNt,UNt,RNt,TNt,CNt,XNt,InfGen,StateSpace,StateSpaceAggr,EventFiltration,runtime,fname] = solver_ctmc_transient_analyzer(sn, options)
2% [T,PIT,QNT,UNT,RNT,TNT,CNT,XNT,INFGEN,STATESPACE,STATESPACEAGGR,EVENTFILTRATION,RUNTIME,FNAME] = SOLVER_CTMC_TRANSIENT_ANALYZER(QN, OPTIONS)
4% Copyright (c) 2012-2026, Imperial College London
9M = sn.nstations; %number of stations
10K = sn.nclasses; %number of classes
17line_debug(
'CTMC transient analyzer starting: nstations=%d, nclasses=%d', M, K);
19[InfGen,StateSpace,StateSpaceAggr,EventFiltration,~,depRates,sn] = solver_ctmc(sn, options); % sn
is updated with the state space
23 save([fname,
'.mat'],
'InfGen',
'StateSpace',
'StateSpaceAggr',
'EventFiltration')
24 line_printf('\nCTMC infinitesimal generator and state space saved in: ');
25 line_printf([fname, '.mat'])
31 isf = sn.nodeToStateful(ist);
32 state = [state,zeros(1,size(sn.space{isf},2)-length(sn.state{isf})),sn.state{isf}];
35pi0 = zeros(1,length(InfGen));
37state0 = matchrow(StateSpace, state);
39 state0 = matchrow(StateSpace, round(state));
42 line_error(mfilename,
'Initial state not contained in the state space.');
45pi0(state0) = 1; % find initial state and set it to probability 1
47% see _kb/06-solver-catalog.md (CTMC section, time-inhomogeneous generator)
for rationale
49if isfield(options,
'config') && isfield(options.config,
'rate_sched') && ~isempty(options.config.rate_sched)
50 rate_sched = options.config.rate_sched;
53 [pit,t] = ctmc_transient(InfGen,pi0,options.timespan(1),options.timespan(2),options.stiff,[],options.timestep);
56 [pit,t,mscale] = local_ctmc_timevarying(sn, options, InfGen, StateSpace, pi0, rate_sched, M, K);
58pit(pit<GlobalConstants.Zero)=0;
66 t(1) = GlobalConstants.Zero;
69 % XNt(k) = pi*arvRates(:,sn.refstat(k),k);
71 %occupancy_t = cumsum(pit.*[0;diff(t)],1)./t;
73 TNt{ist,k} = occupancy_t*depRates(:,ist,k);
74 % see _kb/06-solver-catalog.md (CTMC section, time-inhomogeneous generator)
for rationale
76 TNt{ist,k} = TNt{ist,k} .* squeeze(mscale(ist,k,:));
78 % see _kb/06-solver-catalog.md (CTMC section, time-inhomogeneous generator)
for rationale
79 if sn.nodetype(sn.stationToNode(ist)) == NodeType.Source
80 QNt{ist,k} = zeros(size(pit,1),1);
81 UNt{ist,k} = zeros(size(pit,1),1);
84 qlenAt_t = pit*StateSpaceAggr(:,(ist-1)*K+k);
85 %QNt{i,k} = cumsum(qlenAt_t.*[0;diff(t)])./t;
86 QNt{ist,k} = qlenAt_t;
88 case SchedStrategy.INF
89 UNt{ist,k} = QNt{ist,k};
90 case {SchedStrategy.FCFS, SchedStrategy.HOL, SchedStrategy.SIRO, SchedStrategy.SEPT, SchedStrategy.LEPT, SchedStrategy.SJF}
91 if ~isempty(PH{ist}{k})
92 UNt{ist,k} = occupancy_t*min(StateSpaceAggr(:,(ist-1)*K+k),S(ist))/S(ist);
95 uik = min(StateSpaceAggr(:,(ist-1)*K+k),S(ist)) .* StateSpaceAggr(:,(ist-1)*K+k) ./ sum(StateSpaceAggr(:,((ist-1)*K+1):(ist*K)),2);
97 utilAt_t = pit * uik / S(ist);
98 %UNt{i,k} = cumsum(utilAt_t.*[0;diff(t)])./t;
99 UNt{ist,k} = utilAt_t;
100 case SchedStrategy.DPS
101 w = sn.schedparam(ist,:);
102 nik = S(ist) * w(k) * StateSpaceAggr(:,(ist-1)*K+k) ./ sum(repmat(w,size(StateSpaceAggr,1),1).*StateSpaceAggr(:,((ist-1)*K+1):(ist*K)),2);
104 UNt{ist,k} = occupancy_t*nik;
106 if ~isempty(PH{ist}{k})
107 ind = sn.stationToNode(ist);
108 line_warning(mfilename,
'Transient utilization not support yet for station %s, returning an approximation.\n',sn.nodenames{ind});
109 UNt{ist,k} = occupancy_t*min(StateSpaceAggr(:,(ist-1)*K+k),S(ist))/S(ist);
114runtime = toc(Tstart);
117% line_printf(
'\nCTMC analysis completed. Runtime: %f seconds.\n',runtime);
121function [pit, t, mscale] = local_ctmc_timevarying(sn, options, Qbase, StateSpace, pi0, rate_sched, M, K)
122% Integrate the time-inhomogeneous forward equation dpi/dt = pi Q(t) by
123% non-homogeneous uniformization over a piecewise-constant grid. The generator
124%
is Q(t) = Qbase + sum_sc (m_sc(t)-1) Qhat_sc, where Qhat_sc
is the linear
125% component of Qbase attributable to the scaled (station,class) rate,
126% extracted by a single probe rebuild (Q
is linear in sn.rates).
127ts = options.timespan;
129if isfield(options,'config') && isfield(options.config,'ctmc_tv_ngrid') && ~isempty(options.config.ctmc_tv_ngrid)
130 Ngrid = options.config.ctmc_tv_ngrid;
132t = linspace(ts(1), ts(2), Ngrid)';
136% Build the per-schedule generator component and multiplier trajectory.
138nsc = numel(rate_sched);
140mtraj = ones(nt, nsc);
141scStation = zeros(1,nsc); scClass = zeros(1,nsc);
143 ist = rate_sched(s).station;
144 r = rate_sched(s).class;
145 scStation(s) = ist; scClass(s) = r;
146 % see _kb/06-solver-catalog.md (CTMC section, time-inhomogeneous generator) for rationale
148 snp.rates(ist,r) = snp.rates(ist,r) * probe;
149 if iscell(snp.proc) && numel(snp.proc) >= ist && iscell(snp.proc{ist}) && numel(snp.proc{ist}) >= r
150 pr = snp.proc{ist}{r};
154 pr{z} = pr{z} * probe;
157 snp.proc{ist}{r} = pr;
160 if iscell(snp.mu) && numel(snp.mu) >= ist && iscell(snp.mu{ist}) && numel(snp.mu{ist}) >= r
161 snp.mu{ist}{r} = snp.mu{ist}{r} * probe;
163 Qp = solver_ctmc(snp, options);
165 line_error(mfilename,
'rate_sched probe changed the CTMC state-space size; cannot build time-varying generator.');
167 Qhat{s} = (Qp - Qbase) / (probe - 1);
168 % multiplier m(t) = rate(t)/nominal (nominal defaults to sn.rates(ist,r))
169 if isfield(rate_sched(s),
'nominal') && ~isempty(rate_sched(s).nominal)
170 nominal = rate_sched(s).nominal;
172 nominal = sn.rates(ist,r);
174 seg_t = rate_sched(s).tgrid(:)
';
175 seg_r = rate_sched(s).rates(:)';
176 mtraj(:,s) = interp1(seg_t, seg_r, min(max(t,seg_t(1)),seg_t(end)),
'linear') / nominal;
179% see _kb/06-solver-catalog.md (CTMC section, time-inhomogeneous generator)
for rationale
183QhatFull = cell(1,nsc);
185 QhatFull{s} = full(Qhat{s});
191 mk = 0.5*(mtraj(k,s) + mtraj(k+1,s));
192 Qk = Qk + (mk - 1) * QhatFull{s};
194 pit(k+1,:) = pit(k,:) * expm(Qk * dt);
197% Per-(station,class) throughput multiplier over time (1 where not scaled).
198mscale = ones(M, K, nt);
200 mscale(scStation(s), scClass(s), :) = reshape(mtraj(:,s), 1, 1, nt);