LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ctmc_cdf.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_SOLVERS_CTMC_SOLVER_CTMC_CDF_H
6#define LINE_SOLVERS_CTMC_SOLVER_CTMC_CDF_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `@@SolverCTMC/getCdfRespT.m` and `@@SolverCTMC/getCdfSysRespT.m`: the
12 * exact distribution of the response time, not just its mean.
13 *
14 * THE CONSTRUCTION. Tag one job of a chain (`tag_chain`), build the generator of
15 * the tagged model WITH its event filtration, and then read the passage of the
16 * tagged job off the filtration as a MAP:
17 *
18 * A = map_normalize({Q - A1, A1}) A1 = the tagged job ARRIVING at station i
19 * D = map_normalize({Q - D1, D1}) D1 = the tagged job DEPARTING station i
20 *
21 * `map_pie(A)` is the state of the WHOLE NETWORK as the tagged job sees it on
22 * arrival -- the arrival theorem made exact, with no product form assumed -- and
23 * from that state the passage ends at the first D1 event. So D0 = D.D0 is the
24 * sub-generator of "the tagged job is still in station i", and
25 *
26 * F(t) = 1 - pie_arv exp(D0 t) e
27 *
28 * is the response-time CDF. `getCdfSysRespT` is the same object with the split
29 * taken at the tagged job's ARRIVAL at its own reference station, so the passage
30 * is one full cycle of the network rather than one visit to one station.
31 *
32 * WHY THE FILTRATION IS INDISPENSABLE. Q has already summed every
33 * synchronization's contribution into one entry, and A1 is one synchronization's
34 * share of it; no post-processing of Q can separate them. That is what
35 * `CtmcOptions::keep_filtration` is for, and it is forced on below.
36 *
37 * WHAT IS DELIBERATELY NOT REPRODUCED. The reference recomputes `expm(D0*t)`
38 * from scratch at each of the 100001 grid points (or calls `expmv` where the
39 * MATLAB release has it). On a uniform grid that is pure waste: exp(D0 k dt) is
40 * exp(D0 dt)^k exactly, so ONE matrix exponential and one vector-matrix product
41 * per point give the same curve. The recurrence is stable because exp(D0 dt) is
42 * substochastic -- its powers contract -- so the rounding of a step is damped by
43 * every step after it rather than amplified.
44 */
45
46#include <cmath>
47#include <cstddef>
48#include <limits>
49#include <string>
50#include <utility>
51#include <vector>
52
60#include "line/util/error.h"
61#include "line/util/expm.h"
62#include "line/util/matrix.h"
63
64namespace line {
65namespace ctmc {
66
67/**
68 * A CDF as the reference returns it: the value at each point of the time grid.
69 *
70 * `t` and `F` have the same length, and the curve is TRUNCATED at the first
71 * point where F is within tolerance of 1 -- carrying the flat tail out to the
72 * end of the grid would multiply the size of the result by a factor that
73 * depends only on how far the grid overshoots.
74 */
75template <class T>
76struct CdfCurve {
77 std::vector<T> t;
78 std::vector<T> F;
79
80 bool empty() const { return t.empty(); }
81};
82
83namespace cdf_detail {
84
85/** How finely the reference samples each grid; both are `length(0:T/n:T)`. */
86constexpr std::size_t respt_intervals = 100000;
87constexpr std::size_t sysrespt_intervals = 10000;
88
89/** The tagged model, its synchronization list, and its solved chain. */
90template <class T>
91struct TaggedCtmc {
93 std::vector<qn::Sync<T>> sync;
94 CtmcSolution<T> sol;
95};
96
97/**
98 * Port of `s = inchain(N(inchain)>0)`: which class of the chain to tag.
99 *
100 * The reference indexes with a LOGICAL mask, so `s` is a vector whenever two
101 * classes of the chain both hold jobs, and `getClassByIndex(s)` then errors out.
102 * The first such class is taken here instead. It is not an approximation: the
103 * tagged twin exists for every class of the chain and the tagged job switches
104 * between the twins exactly as an untagged one switches between the originals,
105 * so which class it starts in changes only its initial position and not the
106 * stationary passage the CDF describes.
107 */
108template <class T>
109std::size_t class_to_tag(const NetworkStruct<T>& sn, std::size_t chain) {
110 const std::vector<std::size_t>& ic = sn.inchain[chain - 1];
111 for (std::size_t a = 0; a < ic.size(); ++a)
112 if (sn.classes[ic[a] - 1].population > 0.0) return ic[a];
113 throw InputError("SolverCTMC: chain " + std::to_string(chain) +
114 " holds no jobs, so it has no job to tag");
115}
116
117/** Tag one chain, then build and solve the generator of the tagged model. */
118template <class T>
119TaggedCtmc<T> tagged_ctmc(const NetworkStruct<T>& sn, std::size_t chain, const CtmcOptions& opt) {
120 TaggedCtmc<T> out;
121 out.tag = qn::tag_chain(sn, chain, class_to_tag(sn, chain));
122
123 CtmcOptions o = opt;
124 o.keep_filtration = true; // the whole method is a split of Q on one event
125
126 // The analyzer is used rather than `solver_ctmc` directly because the tagged
127 // model is REDUCIBLE far more often than the original: the twins carry a
128 // single job between them, so every lattice state that puts two tagged jobs
129 // in the network is enumerated and unreachable. The analyzer keeps the
130 // component of the initial state and restricts the filtration with Q, which
131 // a raw generator would not do, and map_pie on a reducible generator has no
132 // unique answer at all.
133 out.sol = solver_ctmc_analyzer(out.tag.V, o);
134
135 // `refresh_sync` is deterministic, so recomputing it here reproduces exactly
136 // the list the analyzer filtered on; `filt[a]` belongs to `sync[a]`.
137 out.sync = qn::refresh_sync(out.tag.V);
138 if (out.sol.chain.filt.size() != out.sync.size())
139 throw NumericError(
140 "SolverCTMC: the event filtration does not match the synchronization list; the "
141 "generator was built without keep_filtration");
142 return out;
143}
144
145/**
146 * The tagged classes whose passage through the reference station counts as a
147 * completion, which is the reference's `taggedModel.classes{r}.completes` gate.
148 *
149 * A CHAIN'S CLASSES ARE ALL SUMMED, not just the one that was tagged. The single
150 * tagged job switches class as it circulates, so "the tagged job arrives at
151 * station i" is the union of the per-class arrival events over the whole tagged
152 * block. The reference reaches the same set by a loop variable that shadows its
153 * own outer index, which is why its per-class results within a chain are
154 * identical.
155 */
156template <class T>
157std::vector<std::size_t> completing_tagged(const qn::TaggedChain<T>& tag) {
158 std::vector<std::size_t> out;
159 for (std::size_t a = 0; a < tag.tagged.size(); ++a)
160 if (tag.V.classes[tag.tagged[a] - 1].completes) out.push_back(tag.tagged[a]);
161 if (out.empty())
162 throw UnsupportedError(
163 "SolverCTMC: no class of the tagged chain has JobClass::completes set, so the response "
164 "time has no completion event to end at. `completes` now defaults to true here as it "
165 "does in the other three codebases, so reaching this means a caller cleared it; set "
166 "classes of the chain before asking for a response-time CDF");
167 return out;
168}
169
170/**
171 * Sum the filtration matrices of every synchronization matching one of the
172 * (class, node) selectors on the chosen half.
173 *
174 * THE REFERENCE COMPARES A STATION INDEX AGAINST A NODE INDEX here: `ist` runs
175 * over stations and `tsn.refstat(r)` is a station, while `ev{v}.passive{1}.node`
176 * is a node. The two coincide only on a model whose stations are its first
177 * nodes, and silently select the wrong node otherwise, so the selectors are
178 * built from `node_of_station` on the way in.
179 */
180template <class T>
181Matrix<T> filtration_sum(const CtmcSolution<T>& sol, const std::vector<qn::Sync<T>>& sync,
182 bool on_passive, EventType ev,
183 const std::vector<std::pair<std::size_t, std::size_t>>& sel) {
184 const std::size_t n = sol.chain.Q.rows();
185 Matrix<T> S(n, n, num_traits<T>::from_int(0));
186 for (std::size_t v = 0; v < sync.size(); ++v) {
187 const qn::SyncEvent<T>& half = on_passive ? sync[v].passive : sync[v].active;
188 if (half.event != ev) continue;
189 bool hit = false;
190 for (std::size_t k = 0; k < sel.size(); ++k)
191 if (half.cls == sel[k].first && half.node == sel[k].second) hit = true;
192 if (!hit) continue;
193 for (std::size_t i = 0; i < n; ++i)
194 for (std::size_t j = 0; j < n; ++j) S(i, j) += sol.chain.filt[v](i, j);
195 }
196 return S;
197}
198
199/** Whether any rate at all was selected; an empty split has no MAP. */
200template <class T>
201bool any_rate(const Matrix<T>& S) {
202 for (std::size_t i = 0; i < S.rows(); ++i)
203 for (std::size_t j = 0; j < S.cols(); ++j)
204 if (num_traits<T>::to_double(S(i, j)) != 0) return true;
205 return false;
206}
207
208/**
209 * Port of the reference's grid: `T = 100/min(nonzero rate)`, in `intervals`
210 * steps, which is "long enough for 100 events at the slowest rate in the model".
211 *
212 * The diagonal is INCLUDED in the minimum, as `Q(Q~=0)` includes it: it is minus
213 * the total exit rate of a state, and a state that leaves slowly is exactly the
214 * one that sets the horizon.
215 */
216template <class T>
217T grid_step(const Matrix<T>& Q, std::size_t intervals) {
218 double lo = std::numeric_limits<double>::infinity();
219 for (std::size_t i = 0; i < Q.rows(); ++i)
220 for (std::size_t j = 0; j < Q.cols(); ++j) {
221 const double a = std::fabs(num_traits<T>::to_double(Q(i, j)));
222 if (a > GlobalConstants::FineTol && a < lo) lo = a;
223 }
224 if (!std::isfinite(lo))
225 throw NumericError("SolverCTMC: the generator of the tagged model carries no rate above "
226 "the tolerance, so no time horizon can be chosen");
227 const double horizon = 100.0 / lo;
228 return num_traits<T>::from_double(horizon / static_cast<double>(intervals));
229}
230
231/**
232 * F(t) = 1 - pie exp(D0 t) e on the uniform grid t = 0, dt, 2dt, ...
233 *
234 * One exponential, then a vector-matrix product per point: see the file header
235 * for why this is the same curve the reference computes point by point.
236 */
237template <class T>
238CdfCurve<T> absorption_cdf(const Matrix<T>& D0, const std::vector<T>& pie, const T& dt,
239 std::size_t intervals, double tol) {
240 static_assert(num_traits<T>::has_transcendental,
241 "the response-time CDF needs a matrix exponential and is not available in exact "
242 "rational arithmetic");
243 const T one = num_traits<T>::from_int(1);
244 const Matrix<T> E = expm(D0, dt);
245 std::vector<T> v = pie;
246 CdfCurve<T> out;
247 for (std::size_t k = 0; k <= intervals; ++k) {
248 T mass = num_traits<T>::from_int(0);
249 for (std::size_t i = 0; i < v.size(); ++i) mass += v[i];
250 const T F = T(one - mass);
251 out.t.push_back(T(dt * num_traits<T>::from_int(static_cast<long>(k))));
252 out.F.push_back(F);
253 // The reference keeps the point that crossed the threshold and drops
254 // everything after it, so the curve ends ON the crossing.
255 if (num_traits<T>::to_double(F) > 1.0 - tol) break;
256 v = vecmul(v, E);
257 }
258 return out;
259}
260
261} // namespace cdf_detail
262
263/**
264 * Port of `@@SolverCTMC/getCdfRespT.m`: the per-(station, class) response-time
265 * CDF, indexed `[ist-1][r-1]`.
266 *
267 * Entries are EMPTY where the curve does not exist: a station the chain never
268 * visits has no arrival event to condition on, and a class outside every chain
269 * that holds jobs is never tagged. Every class of a chain gets the SAME curve,
270 * which is what the reference produces -- the tagged job's passage through a
271 * station is one quantity per chain, since the job carries its class with it.
272 */
273template <class T>
274std::vector<std::vector<CdfCurve<T>>> solver_ctmc_cdf_respt(const NetworkStruct<T>& sn,
275 const CtmcOptions& opt) {
277 "getCdfRespT needs a matrix exponential and is not available in exact rational "
278 "arithmetic");
279 const std::size_t M = sn.nstations, K = sn.nclasses;
280 const std::vector<double> N = sn.njobs();
281 for (std::size_t k = 0; k < K; ++k)
282 if (!std::isfinite(N[k]))
283 throw UnsupportedError(
284 "SolverCTMC: getCdfRespT is presently supported only for closed models; class '" +
285 sn.classes[k].name + "' is open");
286
287 std::vector<std::vector<CdfCurve<T>>> RD(M, std::vector<CdfCurve<T>>(K));
288
289 for (std::size_t c = 1; c <= sn.nchains; ++c) {
290 const cdf_detail::TaggedCtmc<T> tc = cdf_detail::tagged_ctmc(sn, c, opt);
291 const std::vector<std::size_t> tagged = cdf_detail::completing_tagged(tc.tag);
292 const Matrix<T>& Q = tc.sol.chain.Q;
293 const T dt = cdf_detail::grid_step(Q, cdf_detail::respt_intervals);
294
295 for (std::size_t ist = 1; ist <= M; ++ist) {
296 const std::size_t ind = tc.tag.V.node_of_station(ist);
297 std::vector<std::pair<std::size_t, std::size_t>> sel;
298 for (std::size_t a = 0; a < tagged.size(); ++a) sel.push_back(std::make_pair(tagged[a], ind));
299
300 const Matrix<T> A1 =
301 cdf_detail::filtration_sum(tc.sol, tc.sync, true, EventType::ARV, sel);
302 // No arrival of the tagged job here: the chain does not visit this
303 // station, and conditioning on an event of rate zero is not a
304 // degenerate CDF but no CDF at all.
305 if (!cdf_detail::any_rate(A1)) continue;
306 const Matrix<T> D1 =
307 cdf_detail::filtration_sum(tc.sol, tc.sync, false, EventType::DEP, sel);
308 if (!cdf_detail::any_rate(D1)) continue;
309
310 mam::Map<T> A;
311 A.D0 = Q;
312 A.D1 = A1;
313 for (std::size_t i = 0; i < A.D0.rows(); ++i)
314 for (std::size_t j = 0; j < A.D0.cols(); ++j) A.D0(i, j) -= A1(i, j);
315 const std::vector<T> pie_arv = mam::map_pie(mam::map_normalize(A));
316
317 mam::Map<T> D;
318 D.D0 = Q;
319 D.D1 = D1;
320 for (std::size_t i = 0; i < D.D0.rows(); ++i)
321 for (std::size_t j = 0; j < D.D0.cols(); ++j) D.D0(i, j) -= D1(i, j);
322 const mam::Map<T> Dn = mam::map_normalize(D);
323
324 const CdfCurve<T> curve = cdf_detail::absorption_cdf(
325 Dn.D0, pie_arv, dt, cdf_detail::respt_intervals, GlobalConstants::CoarseTol);
326 for (std::size_t a = 0; a < sn.inchain[c - 1].size(); ++a)
327 RD[ist - 1][sn.inchain[c - 1][a] - 1] = curve;
328 }
329 }
330 return RD;
331}
332
333/**
334 * Port of `@@SolverCTMC/getCdfSysRespT.m`: the per-chain SYSTEM response-time
335 * CDF, indexed by chain.
336 *
337 * The split is taken at the tagged job's arrival at its OWN reference station,
338 * for both halves of the MAP: the state seen on arrival there, and the
339 * absorption at the next arrival there. The passage is therefore one full cycle
340 * of the network -- MATLAB's system response time -- and not a single visit.
341 *
342 * The reference has no closed-model guard here, unlike getCdfRespT; `tag_chain`
343 * supplies one, since tagging moves a job out of a finite population.
344 */
345template <class T>
346std::vector<CdfCurve<T>> solver_ctmc_cdf_sys_respt(const NetworkStruct<T>& sn,
347 const CtmcOptions& opt) {
349 "getCdfSysRespT needs a matrix exponential and is not available in exact "
350 "rational arithmetic");
351 std::vector<CdfCurve<T>> RD(sn.nchains);
352
353 for (std::size_t c = 1; c <= sn.nchains; ++c) {
354 const cdf_detail::TaggedCtmc<T> tc = cdf_detail::tagged_ctmc(sn, c, opt);
355 const std::vector<std::size_t> tagged = cdf_detail::completing_tagged(tc.tag);
356 const Matrix<T>& Q = tc.sol.chain.Q;
357
358 // The reference station is read PER CLASS: the twins of one chain need
359 // not share one, and it is each class's own reference station whose
360 // passage counts as that class's completion.
361 std::vector<std::pair<std::size_t, std::size_t>> sel;
362 for (std::size_t a = 0; a < tagged.size(); ++a) {
363 const std::size_t refstat = tc.tag.V.classes[tagged[a] - 1].refstat;
364 sel.push_back(std::make_pair(tagged[a], tc.tag.V.node_of_station(refstat)));
365 }
366
367 const Matrix<T> D1 = cdf_detail::filtration_sum(tc.sol, tc.sync, true, EventType::ARV, sel);
368 if (!cdf_detail::any_rate(D1))
369 throw NumericError(
370 "SolverCTMC: the tagged job never arrives at its reference station, so the system "
371 "response time of chain " + std::to_string(c) + " has no cycle to measure");
372
373 mam::Map<T> D;
374 D.D0 = Q;
375 D.D1 = D1;
376 for (std::size_t i = 0; i < D.D0.rows(); ++i)
377 for (std::size_t j = 0; j < D.D0.cols(); ++j) D.D0(i, j) -= D1(i, j);
378 const mam::Map<T> Dn = mam::map_normalize(D);
379 const std::vector<T> pie_arv = mam::map_pie(Dn);
380
381 const T dt = cdf_detail::grid_step(Q, cdf_detail::sysrespt_intervals);
382 RD[c - 1] = cdf_detail::absorption_cdf(Dn.D0, pie_arv, dt, cdf_detail::sysrespt_intervals,
384 }
385 return RD;
386}
387
388} // namespace ctmc
389} // namespace line
390
391#endif // LINE_SOLVERS_CTMC_SOLVER_CTMC_CDF_H
InputError(const std::string &what)
Definition error.h:39
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
The exception types the port throws.
Matrix exponential by scaling and squaring with a diagonal Pade approximant.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
MAP constructors and structural transformations.
Dense matrix and non-owning view.
std::vector< CdfCurve< T > > solver_ctmc_cdf_sys_respt(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Port of @@SolverCTMC/getCdfSysRespT.m: the per-chain SYSTEM response-time CDF, indexed by chain.
std::vector< std::vector< CdfCurve< T > > > solver_ctmc_cdf_respt(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Port of @@SolverCTMC/getCdfRespT.m: the per-(station, class) response-time CDF, indexed [ist-1][r-1].
CtmcSolution< T > solver_ctmc_analyzer(const NetworkStruct< T > &sn_in, const CtmcOptions &opt)
Port of solver_ctmc_analyzer.m plus the fork-join wrapper of @@SolverCTMC/runAnalyzer....
EventType
The events a state can undergo, with the values of MATLAB EventType.
Definition lang_types.h:111
std::vector< T > map_pie(const Map< T > &m)
Phase distribution seen by an arriving job, pie = pi D1 / (pi D1 e).
Definition map_moment.h:89
Map< T > map_normalize(const Map< T > &in)
Clamp negative off-diagonal entries of D0 and negative entries of D1 to zero, then rebuild the diagon...
TaggedChain< T > tag_chain(const NetworkStruct< T > &sn, std::size_t chain, std::size_t jobclass, const std::string &suffix=".tagged")
Tag one class of one chain.
Definition tag_chain.h:152
std::vector< Sync< T > > refresh_sync(const NetworkStruct< T > &sn, const std::vector< std::vector< bool > > &impatience_classes=std::vector< std::vector< bool > >(), const std::vector< std::size_t > &breakdown_nodes=std::vector< std::size_t >())
Port of MNetwork.refreshSync: the synchronization list.
std::vector< T > vecmul(const std::vector< T > &v, const Matrix< T > &A)
Row vector times matrix, v A.
Definition linalg.h:50
Matrix< T > expm(const Matrix< T > &A)
Matrix exponential exp(A).
Definition expm.h:141
A queueing network and its refreshed NetworkStruct.
Port of solver_ctmc.m: the infinitesimal generator of a queueing network, assembled from the enumerat...
Port of solver_ctmc_analyzer.m and the parts of @@SolverCTMC/runAnalyzer.m that surround one solve: t...
Port of the event half of MATLAB's +State package: the successor states an event produces at one node...
A CDF as the reference returns it: the value at each point of the time grid.
std::vector< T > F
std::vector< T > t
The SolverCTMC knobs this port honours.
static constexpr double FineTol
Definition lang_types.h:668
static constexpr double CoarseTol
Definition lang_types.h:669
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54
The tagged model and the class bookkeeping a caller needs to read it back.
Definition tag_chain.h:69
Port of matlab/src/io/@ModelAdapter/tagChain.m: the model-to-model transform that isolates ONE job of...