LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ctmc_transient.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_TRANSIENT_H
6#define LINE_SOLVERS_CTMC_SOLVER_CTMC_TRANSIENT_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_ctmc_transient_analyzer.m`: the time-dependent counterpart of
12 * `solver_ctmc_analyzer`, integrating dpi/dt = pi Q from a point mass on the
13 * initial state instead of solving pi Q = 0.
14 *
15 * WHAT IS AND IS NOT A TIME AVERAGE. The reference deliberately reports the
16 * INSTANTANEOUS occupancy pi(t), not its running mean -- the commented-out
17 * `cumsum(...)/t` lines in the reference are the time-average it decided
18 * against. Q(t) and U(t) are therefore the state of the chain at t, and their
19 * limits as t grows are the stationary values, not their averages over [0,t].
20 *
21 * THE UTILIZATION SWITCH IS NOT THE STATIONARY ONE. In steady state the carried
22 * rate is available and `T*E[S]/c` is used; here there is no stationary
23 * throughput, so utilization is read off the occupancy directly as
24 * min(n_k, c)/c, and the PS and DPS families take their capacity share. The
25 * reference WARNS for every other discipline and returns the FCFS form as an
26 * approximation, which this port reproduces rather than silently improving:
27 * a caller comparing against MATLAB must get MATLAB's number.
28 */
29
30#include <algorithm>
31#include <cmath>
32#include <cstddef>
33#include <vector>
34
40#include "line/util/error.h"
41#include "line/util/matrix.h"
42
43namespace line {
44namespace ctmc {
45
46namespace detail {
47
48/**
49 * Transient trajectory by FAST ADAPTIVE UNIFORMIZATION, selected with
50 * `options.config.transient_method = "fau"` (see `mc::ctmc_fau`).
51 *
52 * MARCHED, NOT RESTARTED. pi(t_{k+1}) comes from pi(t_k) over the step rather
53 * than from pi(0) over the whole horizon, which keeps the cost proportional to
54 * the grid instead of quadratic in it. Every step removes a little mass and
55 * none puts any back, so the per-step tolerance is `fau_epsilon` divided by the
56 * number of steps and the total defect stays below it. Nothing is renormalized:
57 * the point of the method is that its error is a measured quantity.
58 */
59template <class T>
60mc::TransientResult<T> ctmc_fau_transient(const Matrix<T>& Q, const std::vector<T>& pi0,
61 const T& t0, const T& t1, const CtmcOptions& opt,
62 const std::vector<T>& grid) {
63 const double t0d = num_traits<T>::to_double(t0);
64 const double t1d = num_traits<T>::to_double(t1);
65 if (!std::isfinite(t1d))
66 throw InputError(
67 "solver_ctmc_transient_analyzer: transient_method 'fau' needs a finite horizon");
68
69 std::vector<T> ts;
70 if (!grid.empty()) {
71 ts = grid;
72 } else if (opt.timestep > 0.0) {
73 const std::size_t nstep =
74 static_cast<std::size_t>(std::floor((t1d - t0d) / opt.timestep));
75 for (std::size_t i = 0; i <= nstep; ++i)
76 ts.push_back(num_traits<T>::from_double(t0d + i * opt.timestep));
77 if (num_traits<T>::to_double(ts.back()) < t1d) ts.push_back(t1);
78 } else {
79 const std::size_t ngrid = (opt.fau_ngrid > 1) ? opt.fau_ngrid : 100;
80 for (std::size_t i = 0; i < ngrid; ++i)
81 ts.push_back(num_traits<T>::from_double(
82 t0d + (t1d - t0d) * static_cast<double>(i) / static_cast<double>(ngrid - 1)));
83 }
84 const std::size_t nt = ts.size();
85 const double eps = (opt.fau_epsilon > 0.0) ? opt.fau_epsilon : 1e-6;
86 const double epsStep = eps / static_cast<double>((nt > 1) ? (nt - 1) : 1);
87
88 mc::TransientResult<T> out;
89 out.t = ts;
90 out.pi = Matrix<T>(nt, pi0.size(), num_traits<T>::from_int(0));
91 for (std::size_t j = 0; j < pi0.size(); ++j) out.pi(0, j) = pi0[j];
92 std::vector<T> cur = pi0;
93 for (std::size_t k = 1; k < nt; ++k) {
94 const T dt = ts[k] - ts[k - 1];
95 const mc::FauResult<T> r = mc::ctmc_fau(cur, Q, dt, epsStep, opt.fau_delta, -1);
96 cur = r.pit;
97 for (std::size_t j = 0; j < cur.size(); ++j) out.pi(k, j) = cur[j];
98 }
99 return out;
100}
101
102} // namespace detail
103
104/** What one transient CTMC solve produces. */
105template <class T>
107 std::vector<T> t; ///< the time grid the solver chose
108 Matrix<T> pit; ///< (ntimes x nstates) occupancy
109 std::vector<std::vector<std::vector<T>>> QNt, UNt, TNt; ///< [station][class][time]
110 CtmcSolution<T> chain; ///< the generator and its state space
111};
112
113/**
114 * Port of `solver_ctmc_transient_analyzer.m`.
115 *
116 * @param t0,t1 the timespan; the initial state is the model's default one
117 *
118 * `options.config.rate_sched` -- the time-INHOMOGENEOUS generator, where a
119 * per-(station, class) rate follows a schedule and Q(t) is rebuilt by probing
120 * its linear dependence on `sn.rates` -- is not ported, and there is NO refusal
121 * for it here because there is nothing to refuse: `CtmcOptions` carries no such
122 * field, so the schedule cannot be requested at this entry at all. What this
123 * function solves is always the constant-rate generator. Adding the option
124 * means adding the refusal with it, or the schedule would be accepted and
125 * silently ignored, which would answer a different question.
126 * @param sn the refreshed network struct
127 * @param opt CTMC options (state-space cutoff, tolerances, method)
128 */
129template <class T>
131 const CtmcOptions& opt, const T& t0, const T& t1,
132 const std::vector<T>& grid = std::vector<T>()) {
134 "solver_ctmc_transient_analyzer integrates the forward equation with an "
135 "adaptive Runge-Kutta step controller, whose error norm is transcendental; "
136 "use --arith double or real");
137 check_method(opt.method);
138 // The reference refuses this by name too (`@@SolverCTMC/runAnalyzer.m:113`).
139 // A fork-join model is solved on the TAG-AUGMENTED copy, whose per-class
140 // trajectories are indexed by the auxiliary classes; folding them back is a
141 // steady-state aggregate (`sn_fj_foldback` recomputes response time by
142 // Little's law), and Little's law does not hold pointwise in time.
144 throw UnsupportedError(
145 "SolverCTMC: transient analysis of a fork-join model is not supported. The chain is "
146 "the tag-augmented one, and folding the sibling classes back onto the original ones "
147 "is a steady-state aggregate; use the stationary solve, or SolverLDES for transients");
148
151 const std::size_t n = out.chain.chain.space.size();
152 const std::size_t M = sn.stations.size(), K = sn.nclasses;
153
154 // The model's initial DISTRIBUTION: a point mass on the default state where
155 // no prior is declared, and the product of the declared per-node priors
156 // where one is. Unlike the stationary solve, where the component alone
157 // matters, the transient answer depends on WHERE the chain starts, so a
158 // state that is not in the space is an error here.
159 std::vector<T> pi0;
160 if (!analyzer_detail::init_state_distribution(sn, out.chain.chain.space, pi0))
161 throw InputError(
162 "solver_ctmc_transient_analyzer: the initial state is not contained in the state "
163 "space, so there is no distribution to start the integration from");
164
166 if (opt.transient_method == "fau") {
167 tr = detail::ctmc_fau_transient(out.chain.chain.Q, pi0, t0, t1, opt, grid);
168 } else if (opt.transient_method == "ode") {
169 tr = mc::ctmc_transient(out.chain.chain.Q, pi0, t0, t1);
170 // `options.timestep`, applied where the reference applies it: on the way
171 // out of the integrator, not inside it. An explicit `grid` is the same
172 // resampling on points a uniform step cannot express, and it WINS: a caller
173 // that names the abscissae is integrating something against this trajectory
174 // and needs its own, which is what the environment coupling does.
175 if (!grid.empty())
176 tr = mc::ctmc_transient_on_grid(out.chain.chain.Q, tr, grid);
177 else if (opt.timestep > 0.0)
178 tr = mc::ctmc_transient_on_grid(out.chain.chain.Q, tr, t0, t1,
180 } else {
181 throw InputError("solver_ctmc_transient_analyzer: unknown transient_method '" +
182 opt.transient_method + "'; use 'ode' or 'fau'");
183 }
184 out.t = tr.t;
185 out.pit = tr.pi;
186 const std::size_t nt = out.t.size();
187
188 // Clamp the numerical dust the integrator leaves below the zero threshold,
189 // as the reference does before reading any measure off pi(t).
190 for (std::size_t i = 0; i < nt; ++i)
191 for (std::size_t s = 0; s < n; ++s)
193 out.pit(i, s) = num_traits<T>::from_int(0);
194
195 const Matrix<T> A = ctmc_state_space_aggr(sn, out.chain.chain.space);
196 const T zero = num_traits<T>::from_int(0);
197 out.QNt.assign(M, std::vector<std::vector<T>>(K, std::vector<T>(nt, zero)));
198 out.UNt.assign(M, std::vector<std::vector<T>>(K, std::vector<T>(nt, zero)));
199 out.TNt.assign(M, std::vector<std::vector<T>>(K, std::vector<T>(nt, zero)));
200
201 for (std::size_t ist = 1; ist <= M; ++ist) {
202 const std::size_t isf = sn.stateful_of_station(ist);
203 if (isf == 0) continue;
204 const bool is_source = sn.stations[ist - 1].nodetype == NodeType::Source;
205 const double S = sn.stations[ist - 1].nservers;
206 const SchedStrategy sched = sn.stations[ist - 1].sched;
207
208 for (std::size_t k = 1; k <= K; ++k)
209 for (std::size_t i = 0; i < nt; ++i) {
210 T acc = zero;
211 for (std::size_t s = 0; s < n; ++s)
212 acc += T(out.pit(i, s) * out.chain.chain.dep_rates[s][isf - 1][k - 1]);
213 out.TNt[ist - 1][k - 1][i] = acc;
214 }
215 // A Source's marginal is an encoding sentinel, so its queue length and
216 // utilization are reported as zero rather than read off it.
217 if (is_source) continue;
218
219 for (std::size_t k = 1; k <= K; ++k)
220 for (std::size_t i = 0; i < nt; ++i) {
221 T q = zero;
222 for (std::size_t s = 0; s < n; ++s) q += T(out.pit(i, s) * A(s, (ist - 1) * K + k - 1));
223 out.QNt[ist - 1][k - 1][i] = q;
224 }
225
226 if (sched == SchedStrategy::INF) {
227 for (std::size_t k = 0; k < K; ++k) out.UNt[ist - 1][k] = out.QNt[ist - 1][k];
228 continue;
229 }
230 if (sched == SchedStrategy::PS) {
231 // The capacity share of a processor-sharing station: class k takes
232 // n_k / sum_j n_j of the min(total, c) busy servers.
233 for (std::size_t k = 1; k <= K; ++k)
234 for (std::size_t i = 0; i < nt; ++i) {
235 T u = zero;
236 for (std::size_t s = 0; s < n; ++s) {
237 double tot = 0;
238 for (std::size_t j = 0; j < K; ++j)
239 tot += num_traits<T>::to_double(A(s, (ist - 1) * K + j));
240 if (tot <= 0) continue;
241 const double nk = num_traits<T>::to_double(A(s, (ist - 1) * K + k - 1));
242 u += T(out.pit(i, s) *
243 num_traits<T>::from_double(std::min(nk, S) * nk / tot / S));
244 }
245 out.UNt[ist - 1][k - 1][i] = u;
246 }
247 continue;
248 }
249 if (sched == SchedStrategy::DPS) {
250 const std::vector<T>& w = sn.stations[ist - 1].schedparam;
251 for (std::size_t k = 1; k <= K; ++k)
252 for (std::size_t i = 0; i < nt; ++i) {
253 T u = zero;
254 for (std::size_t s = 0; s < n; ++s) {
255 double wtot = 0;
256 for (std::size_t j = 0; j < K; ++j)
257 wtot += num_traits<T>::to_double(w[j]) *
258 num_traits<T>::to_double(A(s, (ist - 1) * K + j));
259 if (wtot <= 0) continue;
260 const double nk = num_traits<T>::to_double(A(s, (ist - 1) * K + k - 1));
261 u += T(out.pit(i, s) * num_traits<T>::from_double(
262 S * num_traits<T>::to_double(w[k - 1]) * nk /
263 wtot));
264 }
265 out.UNt[ist - 1][k - 1][i] = u;
266 }
267 continue;
268 }
269 // FCFS, HOL, SIRO, SEPT, LEPT, SJF -- and, as an APPROXIMATION the
270 // reference warns about, every remaining discipline.
271 for (std::size_t k = 1; k <= K; ++k) {
272 const lang::Distrib<T>& d = sn.service[ist - 1][k - 1];
273 if (d.disabled || d.D0.rows() == 0) continue;
274 for (std::size_t i = 0; i < nt; ++i) {
275 T u = zero;
276 for (std::size_t s = 0; s < n; ++s) {
277 const double nk = num_traits<T>::to_double(A(s, (ist - 1) * K + k - 1));
278 u += T(out.pit(i, s) * num_traits<T>::from_double(std::min(nk, S) / S));
279 }
280 out.UNt[ist - 1][k - 1][i] = u;
281 }
282 }
283 }
284 return out;
285}
286
287} // namespace ctmc
288} // namespace line
289
290#endif // LINE_SOLVERS_CTMC_SOLVER_CTMC_TRANSIENT_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
Transient distribution of a CTMC by fast adaptive uniformization.
Transient distribution of a CTMC over a time interval, by integrating the forward equations d pi/dt =...
The exception types the port throws.
Fork-join TAG AUGMENTATION: the fold-back half of the transform/lift pair that CTMC and SSA share.
Dense matrix and non-owning view.
void check_method(const std::string &method)
Port of runAnalyzerChecks' method gate.
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....
CtmcTransient< T > solver_ctmc_transient_analyzer(const NetworkStruct< T > &sn, const CtmcOptions &opt, const T &t0, const T &t1, const std::vector< T > &grid=std::vector< T >())
Port of solver_ctmc_transient_analyzer.m.
Matrix< T > ctmc_state_space_aggr(const NetworkStruct< T > &sn, const std::vector< NetState< T > > &space)
Port of StateSpaceAggr: the per-(station, class) job counts of every state, as an (nstates x nstation...
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
TransientResult< T > ctmc_transient_on_grid(const Matrix< T > &Q, const TransientResult< T > &r, const std::vector< T > &grid)
Resample an adaptive transient onto the uniform grid t0 : dt : t1.
TransientResult< T > ctmc_transient(const Matrix< T > &Q, const std::vector< T > &pi0, const T &t0, const T &t1, double rtol=1e-3, double atol=1e-6)
Transient distribution of a CTMC over a time interval, by integrating the forward equations d pi/dt =...
FauResult< T > ctmc_fau(const std::vector< T > &pi0, const Matrix< T > &Q, const T &t, double epsilon=1e-6, double delta=1e-12, long maxsteps=-1)
Transient distribution of a CTMC by fast adaptive uniformization.
Definition ctmc_fau.h:256
bool has_fork_join(const qn::NetworkStruct< T > &sn)
Whether the model needs the tag augmentation at all.
A queueing network and its refreshed NetworkStruct.
Port of solver_ctmc_analyzer.m and the parts of @@SolverCTMC/runAnalyzer.m that surround one solve: t...
The SolverCTMC knobs this port honours.
Everything one CTMC solve produces.
What one transient CTMC solve produces.
std::vector< T > t
the time grid the solver chose
std::vector< std::vector< std::vector< T > > > QNt
Matrix< T > pit
(ntimes x nstates) occupancy
CtmcSolution< T > chain
the generator and its state space
std::vector< std::vector< std::vector< T > > > UNt
std::vector< std::vector< std::vector< T > > > TNt
[station][class][time]
static constexpr double Zero
Definition lang_types.h:670
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:759