LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ctmc_getters.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_GETTERS_H
6#define LINE_SOLVERS_CTMC_SOLVER_CTMC_GETTERS_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The remaining `@@SolverCTMC` accessors: `getGenerator` / `getInfGen`,
12 * `getStateSpace` / `getStateSpaceAggr` and the `getTranProb*` family. The
13 * symbolic getters live in solver_ctmc_symbolic.h.
14 *
15 * WHAT AN ACCESSOR HERE IS INDEXED BY. Every quantity below is read off the
16 * chain `solver_ctmc_analyzer` SOLVED, which is the enumerated space after the
17 * DROP regions have censored it and, on a reducible encoding, restricted to one
18 * weakly connected component. The reference's `getGenerator` and
19 * `getStateSpace` instead call `solver_ctmc` and `State.spaceGenerator`
20 * directly and so report the pre-restriction space, which on such a model has
21 * more rows than its own `pi` vector does. The states that differ carry zero
22 * stationary mass and no other accessor in this port can index them, so they
23 * are dropped here rather than handed back as rows nothing else accepts.
24 *
25 * WHY THE FILTRATION IS AN OUTPUT AND NOT A DERIVATION. `eventFilt` is not
26 * recoverable from Q: assembling the generator adds every synchronization's
27 * contribution into one entry, and no inspection of the sum says which event
28 * put what there. It has to be recorded while Q is built, which is what
29 * `CtmcOptions::keep_filtration` turns on, so the accessors that return it
30 * force that flag on rather than reporting an empty filtration.
31 */
32
33#include <cmath>
34#include <cstddef>
35#include <limits>
36#include <set>
37#include <string>
38#include <vector>
39
42#include "line/lang/qn/state.h"
48#include "line/util/error.h"
49#include "line/util/matrix.h"
50
52
53namespace line {
54namespace ctmc {
55
56/** `[infGen, eventFilt, ev]` of `@@SolverCTMC/getGenerator.m`. */
57template <class T>
59 Matrix<T> Q; ///< the infinitesimal generator
60 std::vector<NetState<T>> space; ///< row i of Q is space[i]
61 /**
62 * `eventFilt`: `filt[a]` holds only what synchronization `sync[a]`
63 * contributed, so `sum_a filt[a]` is the off-diagonal part of Q. The two
64 * vectors are indexed alike; that pairing is the whole content of the
65 * filtration and is why `sync` travels with it.
66 */
67 std::vector<Matrix<T>> filt;
68 std::vector<Sync<T>> sync; ///< `ev`, the reference's `sn.sync`
69 /**
70 * The DERIVED START/PREEMPT filtrations, indexed [station-1][class-1]. They
71 * are NOT part of `filt` and are NOT paired with `sync`: a START rides on an
72 * arc `filt` already carries, so a caller summing `filt` must not see them.
73 */
74 std::vector<std::vector<Matrix<T>>> start_filt, preempt_filt;
75};
76
77/** `[stateSpace, localStateSpace]` of `@@SolverCTMC/getStateSpace.m`. */
78template <class T>
80 /**
81 * The enumerated states. `space[s].local[f]` is already the reference's
82 * `localStateSpace{f}` row: this port keeps a state as its per-node blocks
83 * and never flattens it, so the local decomposition costs nothing to
84 * report and cannot disagree with the flat form.
85 */
86 std::vector<NetState<T>> space;
87 Matrix<T> flat; ///< the blocks concatenated, as MATLAB returns them
88 std::vector<std::size_t> node_width; ///< column width of each stateful node's block
89 /**
90 * `localStateSpace{f}`: one matrix per stateful node, its DISTINCT local
91 * rows in first-appearance order.
92 *
93 * WITHOUT THIS THE SECOND RETURN VALUE CANNOT BE FORMED. `getStateSpace.m`
94 * under `lang='cpp'` returns `{stateSpace}` -- a single cell holding the
95 * whole flat space -- because the local decomposition was not on the wire,
96 * and a caller that indexes `localStateSpace{f}` per node then reads the
97 * global space for every node. `node_width` alone is not enough: it says
98 * where a block ENDS, not which rows a node admits.
99 *
100 * IT IS THE ENUMERATED CHAIN'S DECOMPOSITION, not `spaceGenerator`'s raw
101 * per-node enumeration. The reference's `qnc.space{f}` is built before the
102 * cartesian product and can hold a local row no global state uses; every
103 * row here appears in `space`. On a model whose lattice admits every
104 * combination the two coincide, and where they differ this one is the
105 * decomposition of the chain that was actually solved.
106 */
107 std::vector<Matrix<T>> local;
108};
109
110/** The time-dependent answer of one `getTranProb*` query. */
111template <class T>
113 /**
114 * The reference returns `Pi_t = [t, pi_t]`, one matrix with time glued on
115 * as column 1. The two are kept apart here because they are not the same
116 * quantity and concatenating them forces every consumer to know that
117 * column 1 is not a probability.
118 */
119 std::vector<T> t;
120 Matrix<T> pit; ///< (ntimes x nstates) occupancy over the solved chain
121 Matrix<T> labels; ///< (nstates x width) the state descriptor the query asked for
122};
123
124/**
125 * `SolverCTMC.getStartRate` and `getPreemptRate`: the DERIVED rates the
126 * START/PREEMPT filtration reduces to.
127 *
128 * StartN(i,r) is how often per unit time a class-r service STARTS at station i,
129 * and PreemptN(i,r) how often a class-r job in service is pushed back into the
130 * buffer there. Both are computed by `solver_ctmc_avg_from_pi` already; what was
131 * missing was any way to ask for them, because `solver_ctmc_run_analyzer` returns an
132 * `mva::AvgResult` and that shape has no slot for a derived rate. The reference
133 * exposes them as plain getters, and so do these.
134 *
135 * They need the filtration, so the solve is repeated with `keep_filtration` set
136 * rather than read off a result that may not carry it: a caller who already has
137 * one passes the `CtmcSolution` overload instead and pays nothing.
138 *
139 * At a lossless station with no in-service abandonment
140 * StartN == TN + PreemptN, which is the identity to check them against.
141 */
142template <class T>
144 CtmcOptions o = opt;
145 o.keep_filtration = true;
146 return solver_ctmc_analyzer(sn, o).avg.StartN;
147}
148
149/** As above, for a caller whose solution already carries the filtration. */
150template <class T>
152 return d.avg.StartN;
153}
154
155/** `SolverCTMC.getPreemptRate`; see {@link ctmc_get_start_rate}. */
156template <class T>
158 CtmcOptions o = opt;
159 o.keep_filtration = true;
160 return solver_ctmc_analyzer(sn, o).avg.PreemptN;
161}
162
163/** As above, for a caller whose solution already carries the filtration. */
164template <class T>
166 return d.avg.PreemptN;
167}
168
169namespace getters_detail {
170
171/** Per-node block widths, which every state shares by construction. */
172template <class T>
173std::vector<std::size_t> node_widths(const std::vector<NetState<T>>& space) {
174 std::vector<std::size_t> w;
175 if (space.empty()) return w;
176 for (std::size_t f = 0; f < space[0].local.size(); ++f) w.push_back(space[0].local[f].size());
177 return w;
178}
179
180/** Concatenate the per-node blocks of every state into one matrix. */
181template <class T>
182Matrix<T> flatten(const std::vector<NetState<T>>& space) {
183 const std::vector<std::size_t> w = node_widths(space);
184 std::size_t total = 0;
185 for (std::size_t f = 0; f < w.size(); ++f) total += w[f];
186 Matrix<T> out(space.size(), total, num_traits<T>::from_int(0));
187 for (std::size_t s = 0; s < space.size(); ++s) {
188 std::size_t c = 0;
189 for (std::size_t f = 0; f < space[s].local.size(); ++f)
190 for (std::size_t j = 0; j < space[s].local[f].size(); ++j) out(s, c++) = space[s].local[f][j];
191 }
192 return out;
193}
194
195/**
196 * The DISTINCT local rows of each stateful node, in first-appearance order.
197 *
198 * First-appearance and not sorted, because the reference's `qnc.space{f}` is
199 * built in enumeration order and a caller that pairs a local index with a row
200 * of `space` must see the same order on both sides.
201 */
202template <class T>
203std::vector<Matrix<T>> local_spaces(const std::vector<NetState<T>>& space) {
204 std::vector<Matrix<T>> out;
205 if (space.empty()) return out;
206 const std::size_t NF = space[0].local.size();
207 for (std::size_t f = 0; f < NF; ++f) {
208 std::vector<std::vector<T>> rows;
209 std::set<std::vector<double>> seen;
210 for (std::size_t s = 0; s < space.size(); ++s) {
211 if (space[s].local.size() <= f) continue;
212 const std::vector<T>& r = space[s].local[f];
213 std::vector<double> key(r.size());
214 for (std::size_t j = 0; j < r.size(); ++j) key[j] = num_traits<T>::to_double(r[j]);
215 if (!seen.insert(key).second) continue;
216 rows.push_back(r);
217 }
218 const std::size_t w = rows.empty() ? 0 : rows[0].size();
219 Matrix<T> m(rows.size(), w, num_traits<T>::from_int(0));
220 for (std::size_t i = 0; i < rows.size(); ++i)
221 for (std::size_t j = 0; j < w && j < rows[i].size(); ++j) m(i, j) = rows[i][j];
222 out.push_back(m);
223 }
224 return out;
225}
226
227/**
228 * The stateful index of a node, refusing a node that carries no state.
229 *
230 * A stateless node -- a Router, a ClassSwitch -- has no block in any state, so
231 * there is nothing to slice out for it and the query is a caller error rather
232 * than an empty answer.
233 */
234template <class T>
235std::size_t stateful_or_throw(const NetworkStruct<T>& sn, std::size_t ind, const char* what) {
236 if (ind < 1 || ind > sn.nodes.size())
237 throw InputError(std::string(what) + ": the node index is out of range");
238 const std::size_t isf = sn.stateful_index(ind);
239 if (isf == 0)
240 throw InputError(std::string(what) +
241 ": node " + std::to_string(ind) +
242 " is stateless and holds no block of the network state");
243 return isf;
244}
245
246/**
247 * The timespan gate of the `getTranProb*` family.
248 *
249 * The reference refuses an infinite horizon by name because there is nothing to
250 * integrate to: `pi(t)` on [0, Inf) is the stationary vector, which is what
251 * `getProb*` already answers.
252 */
253template <class T>
254void check_timespan(const T& t0, const T& t1, const char* what) {
255 const double a = num_traits<T>::to_double(t0), b = num_traits<T>::to_double(t1);
256 if (!std::isfinite(a) || !std::isfinite(b) || !(b > a))
257 throw InputError(std::string(what) +
258 " requires a finite timespan [t0, t1] with t1 > t0; for the limit as t "
259 "grows use the stationary family (getProb, getProbAggr, getProbSys, "
260 "getProbSysAggr)");
261}
262
263} // namespace getters_detail
264
265/**
266 * Port of `@@SolverCTMC/getGenerator.m`: the generator, its event filtration and
267 * the synchronization list the filtration is indexed by.
268 *
269 * The synchronization list is rebuilt rather than carried, `refresh_sync` being
270 * a pure function of the struct: it returns the same list, in the same order,
271 * that indexed `filt` when the generator was assembled.
272 */
273template <class T>
275 if (d.chain.filt.empty())
276 throw InputError(
277 "ctmc_get_generator: the solution carries no event filtration, which cannot be "
278 "recovered from Q because its entries have already summed every synchronization's "
279 "contribution; re-solve with CtmcOptions::keep_filtration = true");
281 g.Q = d.chain.Q;
282 g.space = d.chain.space;
283 g.filt = d.chain.filt;
284 g.start_filt = d.chain.start_filt;
285 g.preempt_filt = d.chain.preempt_filt;
286 g.sync = refresh_sync(sn);
287 return g;
288}
289
290/**
291 * As above, solving the chain first.
292 *
293 * `keep_filtration` is forced on because the filtration is half the answer and
294 * cannot be reconstructed afterwards; a caller who only wants Q should read
295 * `CtmcSolution::chain` instead and not pay for one n x n matrix per event.
296 */
297template <class T>
303
304/** `@@SolverCTMC/getInfGen.m`, a pure alias of `getGenerator` in the reference. */
305template <class T>
309
310/** `@@SolverCTMC/getInfGen.m`, solving the chain first. */
311template <class T>
315
316/**
317 * Port of `@@SolverCTMC/getStateSpace.m`.
318 *
319 * The reference derives `localStateSpace` by cutting the flat matrix at the
320 * width of each node's own space, a split that can only be got right by
321 * carrying those widths alongside. Here the state IS the split, so `flat` is
322 * the derived form and the widths are reported for a caller comparing columns
323 * against MATLAB.
324 */
325template <class T>
328 s.space = d.chain.space;
329 s.flat = getters_detail::flatten(d.chain.space);
330 s.node_width = getters_detail::node_widths(d.chain.space);
331 s.local = getters_detail::local_spaces(d.chain.space);
332 return s;
333}
334
335/** As above, solving the chain first. */
336template <class T>
340
341/** The answer of `@@SolverCTMC/getCdfFirstPassT.m`: the [F(t), t] curve with
342 * its grid, density and resolved state sets. */
344 std::vector<double> t; ///< the grid, 1000 points to the horizon
345 std::vector<double> F; ///< CDF at t, clamped to [0, 1]
346 std::vector<double> f; ///< density at t
347 std::vector<std::size_t> source; ///< resolved 0-based rows; empty = conditional stationary
348 std::vector<std::size_t> target; ///< resolved 0-based rows
349};
350
351namespace getters_detail {
352
353/**
354 * A state set given as 1-based ROW INDICES into the state space or as matrices
355 * of state rows, resolved to 0-based row indices against `flat`. An
356 * unrecognised row is an error rather than a silent drop, since a passage into
357 * a state that is not in the space is not a slow passage but an undefined one.
358 */
359template <class T>
360std::vector<std::size_t> resolve_state_set(const Matrix<double>& S, const Matrix<T>& flat,
361 const char* name) {
362 std::set<std::size_t> idx;
363 if (S.rows() == 0 || S.cols() == 0) return std::vector<std::size_t>();
364 const std::size_t n = flat.rows();
365 bool is_index_vector = (S.rows() == 1 || S.cols() == 1);
366 if (is_index_vector) {
367 for (std::size_t i = 0; i < S.rows() && is_index_vector; ++i)
368 for (std::size_t j = 0; j < S.cols(); ++j) {
369 const double v = S(i, j);
370 if (v != std::floor(v) || v < 1 || v > static_cast<double>(n)) {
371 is_index_vector = false;
372 break;
373 }
374 }
375 }
376 if (is_index_vector) {
377 for (std::size_t i = 0; i < S.rows(); ++i)
378 for (std::size_t j = 0; j < S.cols(); ++j)
379 idx.insert(static_cast<std::size_t>(S(i, j)) - 1);
380 } else {
381 if (S.cols() != flat.cols())
382 throw InputError(std::string("getCdfFirstPassT: a state row in set ") + name +
383 " has " + std::to_string(S.cols()) + " columns where the state "
384 "space has " + std::to_string(flat.cols()));
385 for (std::size_t i = 0; i < S.rows(); ++i) {
386 bool found = false;
387 for (std::size_t r = 0; r < n && !found; ++r) {
388 bool eq = true;
389 for (std::size_t j = 0; j < S.cols() && eq; ++j)
390 if (num_traits<T>::to_double(flat(r, j)) != S(i, j)) eq = false;
391 if (eq) {
392 idx.insert(r);
393 found = true;
394 }
395 }
396 if (!found)
397 throw InputError(std::string("A state given in set ") + name +
398 " is not in the state space.");
399 }
400 }
401 return std::vector<std::size_t>(idx.begin(), idx.end());
402}
403
404} // namespace getters_detail
405
406/**
407 * Port of `@@SolverCTMC/getCdfFirstPassT.m`: the distribution of the FIRST
408 * PASSAGE TIME from state set A into state set B, on the CTMC underlying the
409 * model. An empty A starts from the conditional stationary law on the
410 * complement of B.
411 *
412 * THIS IS NOT getCdfRespT. That getter times a tagged job between an arrival
413 * at a station and its departure, through the event filtration; this one times
414 * the chain between two sets of states the caller names, and answers questions
415 * the filtration cannot express -- the writer cycle time of a readers-writers
416 * model, the time to fill a buffer, the time to leave a degraded region.
417 *
418 * Reference: P. G. Harrison and W. J. Knottenbelt, "Passage Time Distributions
419 * in Large Markov Chains", 2002.
420 */
421template <class T>
423 const Matrix<double>& A, const Matrix<double>& B,
424 const std::string& method = "expm") {
426 "getCdfFirstPassT takes a matrix exponential and therefore requires an "
427 "arithmetic with transcendental functions");
428 const Matrix<T>& Q = d.chain.Q;
429 const std::size_t n = Q.rows();
430 const Matrix<T> flat = getters_detail::flatten(d.chain.space);
431
433 out.target = getters_detail::resolve_state_set(B, flat, "B");
434 if (out.target.empty())
435 throw InputError(
436 "The target state set B is empty: a first passage time into no state is undefined.");
437 out.source = getters_detail::resolve_state_set(A, flat, "A");
438
439 std::vector<T> pi0;
440 if (!out.source.empty()) {
441 pi0.assign(n, num_traits<T>::from_int(0));
443 static_cast<long>(out.source.size()));
444 for (std::size_t idx : out.source) pi0[idx] = w;
445 }
446
447 // The horizon is chosen the way the response-time getter chooses it: 100
448 // events at the slowest rate in the chain.
449 double min_rate = std::numeric_limits<double>::infinity();
450 for (std::size_t i = 0; i < n; ++i)
451 for (std::size_t j = 0; j < n; ++j) {
452 const double v = std::abs(num_traits<T>::to_double(Q(i, j)));
453 if (v > GlobalConstants::FineTol && v < min_rate) min_rate = v;
454 }
455 const double thor = std::abs(100.0 / min_rate);
456 std::vector<double> tset(1000);
457 for (std::size_t i = 0; i < tset.size(); ++i)
458 tset[i] = thor * static_cast<double>(i) / static_cast<double>(tset.size() - 1);
459
460 const mc::PassageCurve<T> curve = mc::ctmc_passage_time(Q, pi0, out.target, tset, method);
461 out.t = tset;
462 out.F = curve.F;
463 out.f = curve.f;
464 return out;
465}
466
467/** As above, solving the chain first. */
468template <class T>
470 const Matrix<double>& A, const Matrix<double>& B,
471 const std::string& method = "expm") {
472 return ctmc_cdf_firstpasst(sn, solver_ctmc_analyzer(sn, opt), A, B, method);
473}
474
475/** The answer of `@@SolverCTMC/getFirstPassTMoments.m`. */
476template <class T>
478 std::vector<T> m; ///< (nmax) moments for a passage started uniformly in A
479 Matrix<T> mall; ///< (nstates x nmax), one row per starting state
480 std::vector<std::size_t> source; ///< resolved 0-based rows; empty = conditional stationary
481 std::vector<std::size_t> target; ///< resolved 0-based rows
482};
483
484/**
485 * Port of `@@SolverCTMC/getFirstPassTMoments.m`: moments of order 1..nmax of
486 * the first passage time from state set A into state set B.
487 *
488 * NO TRANSFORM INVERSION AND NO TIME GRID ARE INVOLVED. The moments come from
489 * Eq. 3 of Harrison and Knottenbelt (2002) -- one linear solve per order -- so
490 * they are EXACT and are not limited by the horizon a CDF would have to be
491 * truncated at. That is the whole reason this getter exists beside
492 * `ctmc_cdf_firstpasst`: the variance or the skewness of a passage time costs
493 * nmax solves here and a numerical integration of a truncated curve there.
494 *
495 * `mall` is zero on B and infinite where B cannot be reached, as the reference
496 * reports it. A and B name states as in `ctmc_cdf_firstpasst`.
497 */
498template <class T>
500 const CtmcSolution<T>& d,
501 const Matrix<double>& A,
502 const Matrix<double>& B,
503 std::size_t nmax = 3) {
505 "getFirstPassTMoments marks an unreachable target with an infinity and "
506 "therefore requires an arithmetic that has one");
507 const Matrix<T>& Q = d.chain.Q;
508 const std::size_t n = Q.rows();
509 const Matrix<T> flat = getters_detail::flatten(d.chain.space);
510
512 out.target = getters_detail::resolve_state_set(B, flat, "B");
513 if (out.target.empty())
514 throw InputError(
515 "The target state set B is empty: a first passage time into no state is undefined.");
516 out.source = getters_detail::resolve_state_set(A, flat, "A");
517
518 std::vector<T> pi0;
519 if (!out.source.empty()) {
520 pi0.assign(n, num_traits<T>::from_int(0));
522 static_cast<long>(out.source.size()));
523 for (std::size_t idx : out.source) pi0[idx] = w;
524 }
525
526 const mc::PassageMoments<T> pm = mc::ctmc_passage_moments(Q, pi0, out.target, nmax);
527 out.m = pm.m;
528 out.mall = pm.mall;
529 return out;
530}
531
532/** As above, solving the chain first. */
533template <class T>
535 const CtmcOptions& opt,
536 const Matrix<double>& A,
537 const Matrix<double>& B,
538 std::size_t nmax = 3) {
540}
541
542/**
543 * Port of `@@SolverCTMC/getStateSpaceAggr.m`: the per-(station, class) job
544 * counts of every state, in column block order `(ist-1)*K + k`.
545 *
546 * The reference returns `[]` with a warning when the model has not been solved,
547 * since its copy is a by-product cached by a previous run. There is no such
548 * cache here: the aggregate is a function of the state space alone and is
549 * recomputed, so the accessor either answers or throws.
550 */
551template <class T>
555
556/** As above, for a caller who has already solved the chain. */
557template <class T>
561
562namespace getters_detail {
563
564/**
565 * The per-class marginal of one node's block, for every state.
566 *
567 * `prob_detail::marginal_of` is the single decoder of a local row into class
568 * counts -- it knows the phase blocks of a station and the leading count
569 * columns of a Cache -- and is reused rather than re-derived, so an aggregated
570 * transient query and an aggregated stationary one cannot disagree about what
571 * a state holds.
572 *
573 * A SOURCE ROW IS ZERO, not Inf. `to_marginal` reports an infinite reservoir
574 * for an EXT station, which describes the encoding rather than a queue length;
575 * `ctmc_state_space_aggr` zeroes it for the same reason, and a label matrix
576 * that disagreed with it would make the system and per-node aggregates
577 * inconsistent.
578 */
579template <class T>
580Matrix<T> node_marginal_labels(const NetworkStruct<T>& sn, std::size_t ind, std::size_t isf,
581 const std::vector<NetState<T>>& space) {
582 const std::size_t K = sn.nclasses;
583 const T zero = num_traits<T>::from_int(0);
584 Matrix<T> out(space.size(), K, zero);
585 const std::size_t ist = sn.nodes[ind - 1].station;
586 if (ist != 0 && sn.stations[ist - 1].nodetype == NodeType::Source) return out;
587 for (std::size_t s = 0; s < space.size(); ++s) {
588 const std::vector<T> m = prob_detail::marginal_of(sn, ind, space[s].local[isf - 1]);
589 for (std::size_t k = 0; k < K && k < m.size(); ++k) out(s, k) = m[k];
590 }
591 return out;
592}
593
594/**
595 * Refuse a (struct, trajectory) pair that cannot belong to one model.
596 *
597 * The overloads taking a ready `CtmcTransient` cannot verify in general that it
598 * was integrated from the struct they are handed, and they should not try: the
599 * point of those overloads is that four queries share one integration. This
600 * catches the one mismatch that would read past the end of a state instead of
601 * merely answering about the wrong model.
602 */
603template <class T>
604void check_pair(std::size_t isf, const std::vector<NetState<T>>& space, const char* what) {
605 if (!space.empty() && isf > space[0].local.size())
606 throw InputError(std::string(what) +
607 ": the transient solution has fewer stateful nodes than the model it "
608 "was queried with, so it was not integrated from that model");
609}
610
611/** The block of one node, for every state: the reference's `SSnode`. */
612template <class T>
613Matrix<T> node_labels(std::size_t isf, const std::vector<NetState<T>>& space) {
614 const std::size_t w = space.empty() ? 0 : space[0].local[isf - 1].size();
615 Matrix<T> out(space.size(), w, num_traits<T>::from_int(0));
616 for (std::size_t s = 0; s < space.size(); ++s)
617 for (std::size_t j = 0; j < w; ++j) out(s, j) = space[s].local[isf - 1][j];
618 return out;
619}
620
621/** The time grid and occupancy every `getTranProb*` query shares. */
622template <class T>
623CtmcTranProb<T> tran_common(const CtmcTransient<T>& tr) {
624 CtmcTranProb<T> p;
625 p.t = tr.t;
626 p.pit = tr.pit;
627 return p;
628}
629
630} // namespace getters_detail
631
632/**
633 * Port of `@@SolverCTMC/getTranProb.m`: pi(t) over the whole chain, labelled by
634 * one node's local state.
635 *
636 * IT IS NOT A PER-STATE TRANSIENT PROBABILITY, despite the name's symmetry with
637 * `getProb`. The reference returns the FULL occupancy vector together with the
638 * node's slice of the state space, leaving the caller to sum the rows sharing
639 * the local state it cares about; that is a strictly richer answer than one
640 * marginal and is reproduced as such.
641 *
642 * @param ind 1-based node index
643 * @param sn the refreshed network struct
644 * @param tr transient solution whose pi(t) is being labelled
645 */
646template <class T>
648 std::size_t ind) {
649 assert_phase_type_states(sn, "getTranProb");
650 const std::size_t isf = getters_detail::stateful_or_throw(sn, ind, "getTranProb");
651 getters_detail::check_pair(isf, tr.chain.chain.space, "getTranProb");
652 CtmcTranProb<T> p = getters_detail::tran_common(tr);
653 p.labels = getters_detail::node_labels(isf, tr.chain.chain.space);
654 return p;
655}
656
657/** As above, integrating the forward equation first. */
658template <class T>
660 std::size_t ind, const T& t0, const T& t1) {
662 "getTranProb integrates the forward equation, whose adaptive step controller "
663 "is transcendental; use --arith double or real");
664 assert_phase_type_states(sn, "getTranProb");
665 getters_detail::check_timespan(t0, t1, "getTranProb");
667}
668
669/**
670 * Port of `@@SolverCTMC/getTranProbAggr.m`: pi(t), labelled by one node's
671 * per-class job counts.
672 *
673 * THE REFERENCE SLICES THE AGGREGATE BY NODE INDEX, `SSa(:, (jnd-1)*K+1 :
674 * jnd*K)`, while `ctmc_ssg` writes that matrix in STATION blocks
675 * `(ist-1)*K+1 : ist*K`. The two indices coincide only when every node is a
676 * station, so on a model carrying a Router or a ClassSwitch the reference reads
677 * the wrong block. The labels are decoded from the node's own state here
678 * instead of sliced out of a station-indexed matrix, which sidesteps the
679 * mismatch and extends to a stateful non-station -- a Cache -- that no station
680 * block describes at all.
681 *
682 * @param ind 1-based node index
683 * @param sn the refreshed network struct
684 * @param tr transient solution whose pi(t) is being labelled
685 */
686template <class T>
688 std::size_t ind) {
689 assert_phase_type_states(sn, "getTranProbAggr");
690 const std::size_t isf = getters_detail::stateful_or_throw(sn, ind, "getTranProbAggr");
691 getters_detail::check_pair(isf, tr.chain.chain.space, "getTranProbAggr");
692 CtmcTranProb<T> p = getters_detail::tran_common(tr);
693 p.labels = getters_detail::node_marginal_labels(sn, ind, isf, tr.chain.chain.space);
694 return p;
695}
696
697/** As above, integrating the forward equation first. */
698template <class T>
700 std::size_t ind, const T& t0, const T& t1) {
702 "getTranProbAggr integrates the forward equation, whose adaptive step "
703 "controller is transcendental; use --arith double or real");
704 assert_phase_type_states(sn, "getTranProbAggr");
705 getters_detail::check_timespan(t0, t1, "getTranProbAggr");
707}
708
709/**
710 * Port of `@@SolverCTMC/getTranProbSys.m`: pi(t), labelled by the whole network
711 * state with its phases.
712 */
713template <class T>
715 assert_phase_type_states(sn, "getTranProbSys");
716 CtmcTranProb<T> p = getters_detail::tran_common(tr);
717 p.labels = getters_detail::flatten(tr.chain.chain.space);
718 return p;
719}
720
721/** As above, integrating the forward equation first. */
722template <class T>
724 const T& t0, const T& t1) {
726 "getTranProbSys integrates the forward equation, whose adaptive step "
727 "controller is transcendental; use --arith double or real");
728 assert_phase_type_states(sn, "getTranProbSys");
729 getters_detail::check_timespan(t0, t1, "getTranProbSys");
731}
732
733/**
734 * Port of `@@SolverCTMC/getTranProbSysAggr.m`: pi(t), labelled by the network's
735 * per-(station, class) job counts.
736 *
737 * The labels are `ctmc_state_space_aggr`, the same matrix the transient
738 * analyzer already integrates Q(t) and U(t) against, so a caller summing these
739 * rows by hand reproduces its `QNt` exactly.
740 */
741template <class T>
743 const CtmcTransient<T>& tr) {
744 assert_phase_type_states(sn, "getTranProbSysAggr");
745 CtmcTranProb<T> p = getters_detail::tran_common(tr);
746 p.labels = ctmc_state_space_aggr(sn, tr.chain.chain.space);
747 return p;
748}
749
750/** As above, integrating the forward equation first. */
751template <class T>
753 const T& t0, const T& t1) {
755 "getTranProbSysAggr integrates the forward equation, whose adaptive step "
756 "controller is transcendental; use --arith double or real");
757 assert_phase_type_states(sn, "getTranProbSysAggr");
758 getters_detail::check_timespan(t0, t1, "getTranProbSysAggr");
760}
761
762/*
763 * `@@SolverCTMC/getSymbolicGenerator.m` and `getSymbolicSolution.m` are NOT
764 * here: they live in solver_ctmc_symbolic.h as `ctmc_symbolic_generator` and
765 * `ctmc_symbolic_solution`.
766 *
767 * They were refused by name until 2026-07-31, on the grounds that this port had
768 * no computer algebra. Half of that was never true -- the generator is LINEAR in
769 * the event symbols, so it is one numeric filtration per event and needs no
770 * algebra to assemble -- and the other half stopped being true when `api/sym`
771 * gained a client for the line-sage-rest service the reference itself uses.
772 * They are kept in their own header so that this one, which every CTMC accessor
773 * includes, does not drag in a socket-using HTTP client.
774 */
775
776/*
777 * `@@SolverCTMC/getSjrnT.m` and `sjrnT.m` are aliases of `getCdfRespT` in the
778 * reference and are NOT defined here: an alias belongs beside the entry point
779 * it forwards to, and the CTMC response-time CDF lives in its own header (see
780 * `solver_mam_get_sjrn_t` in solver_mam_runner.h and `solver_nc_sjrnt` in
781 * solver_nc_cdf.h for the two established precedents). Defining it here would
782 * make this header depend on that one for nothing but a forwarding call.
783 */
784
785/**
786 * Port of `@@SolverCTMC/getAsymptoticVariance.m`: the asymptotic variance of the
787 * time-average of a reward along a sample path of this model's CTMC.
788 *
789 * WHAT IT IS FOR. A simulation estimate of a steady-state mean has a standard
790 * error that shrinks like sqrt(sigma^2/t), where sigma^2 is NOT the stationary
791 * variance of the reward but its ASYMPTOTIC variance, which also carries the
792 * autocorrelation of the path. That number is what says how long a run has to
793 * be, and `sim_runlength` turns it into a run length for a target precision. It
794 * cannot be guessed from the stationary variance: on M/M/1 the two differ by a
795 * factor that blows up like (1-rho)^-2.
796 *
797 * The reward is a function of the state ROW, evaluated on the state space the
798 * generator was built from.
799 */
800template <class T>
802 const NetworkStruct<T>& sn, const CtmcOptions& opt,
803 const std::function<T(const NetState<T>&)>& reward) {
805 std::vector<T> f;
806 f.reserve(g.space.size());
807 for (std::size_t i = 0; i < g.space.size(); ++i) f.push_back(reward(g.space[i]));
808 return sim::sim_asymvar_ctmc<T>(g.Q, f);
809}
810
811} // namespace ctmc
812} // namespace line
813
814#endif // LINE_SOLVERS_CTMC_SOLVER_CTMC_GETTERS_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
A network plus its refreshed NetworkStruct.
First passage times into a target STATE SET, for Markov and semi-Markov chains.
The exception types the port throws.
Dense matrix and non-owning view.
void assert_phase_type_states(const NetworkStruct< T > &sn, const std::string &what)
Port of @@SolverCTMC/assertPhaseTypeStates: refuse a query whose answer would be a per-state probabil...
CtmcGenerator< T > ctmc_get_generator(const NetworkStruct< T > &sn, const CtmcSolution< T > &d)
Port of @@SolverCTMC/getGenerator.m: the generator, its event filtration and the synchronization list...
CtmcTranProb< T > ctmc_get_tran_prob_sys(const NetworkStruct< T > &sn, const CtmcTransient< T > &tr)
Port of @@SolverCTMC/getTranProbSys.m: pi(t), labelled by the whole network state with its phases.
CtmcTranProb< T > ctmc_get_tran_prob_sys_aggr(const NetworkStruct< T > &sn, const CtmcTransient< T > &tr)
Port of @@SolverCTMC/getTranProbSysAggr.m: pi(t), labelled by the network's per-(station,...
CtmcGenerator< T > ctmc_get_infgen(const NetworkStruct< T > &sn, const CtmcSolution< T > &d)
@@SolverCTMC/getInfGen.m, a pure alias of getGenerator in the reference.
sim::AsymVarResult< T > ctmc_get_asymptotic_variance(const NetworkStruct< T > &sn, const CtmcOptions &opt, const std::function< T(const NetState< T > &)> &reward)
Port of @@SolverCTMC/getAsymptoticVariance.m: the asymptotic variance of the time-average of a reward...
CtmcStateSpace< T > ctmc_get_state_space(const NetworkStruct< T > &, const CtmcSolution< T > &d)
Port of @@SolverCTMC/getStateSpace.m.
Matrix< T > ctmc_get_state_space_aggr(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Port of @@SolverCTMC/getStateSpaceAggr.m: the per-(station, class) job counts of every state,...
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....
Matrix< T > ctmc_get_preempt_rate(const NetworkStruct< T > &sn, const CtmcOptions &opt)
SolverCTMC.getPreemptRate; see ctmc_get_start_rate.
Matrix< T > ctmc_get_start_rate(const NetworkStruct< T > &sn, const CtmcOptions &opt)
SolverCTMC.getStartRate and getPreemptRate: the DERIVED rates the START/PREEMPT filtration reduces to...
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.
CtmcTranProb< T > ctmc_get_tran_prob(const NetworkStruct< T > &sn, const CtmcTransient< T > &tr, std::size_t ind)
Port of @@SolverCTMC/getTranProb.m: pi(t) over the whole chain, labelled by one node's local state.
CtmcFirstPassage ctmc_cdf_firstpasst(const NetworkStruct< T > &, const CtmcSolution< T > &d, const Matrix< double > &A, const Matrix< double > &B, const std::string &method="expm")
Port of @@SolverCTMC/getCdfFirstPassT.m: the distribution of the FIRST PASSAGE TIME from state set A ...
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...
CtmcTranProb< T > ctmc_get_tran_prob_aggr(const NetworkStruct< T > &sn, const CtmcTransient< T > &tr, std::size_t ind)
Port of @@SolverCTMC/getTranProbAggr.m: pi(t), labelled by one node's per-class job counts.
CtmcFirstPassageMoments< T > ctmc_firstpasst_moments(const NetworkStruct< T > &, const CtmcSolution< T > &d, const Matrix< double > &A, const Matrix< double > &B, std::size_t nmax=3)
Port of @@SolverCTMC/getFirstPassTMoments.m: moments of order 1..nmax of the first passage time from ...
PassageMoments< T > ctmc_passage_moments(const Matrix< T > &Q, const std::vector< T > &pi0, const std::vector< std::size_t > &target, std::size_t nmax=1)
Moments of order 1..nmax of the first passage time into target.
PassageCurve< T > ctmc_passage_time(const Matrix< T > &Q, const std::vector< T > &pi0, const std::vector< std::size_t > &target, const std::vector< double > &tset, const std::string &method="expm", const std::string &lti_method="euler")
CDF and density of the first passage time on the grid tset: F(t) = 1 - alpha exp(St) 1 and f(t) = alp...
AsymVarResult< T > sim_asymvar_ctmc(const Matrix< T > &A, const std::vector< T > &f, const std::vector< T > &pi=std::vector< T >())
Asymptotic variance of a reward on a CTMC: 2 sum_x pi(x)g(x)d(x) with g = f - E_pi[f] and A d = -g,...
A queueing network and its refreshed NetworkStruct.
Run-length planning for steady-state simulation.
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...
The SolverCTMC probability family: solver_ctmc_joint, _jointaggr, _marg, _margaggr,...
Port of solver_ctmc_transient_analyzer.m: the time-dependent counterpart of solver_ctmc_analyzer,...
Port of the MATLAB +State package: the encoding that turns a station's state row into marginal job co...
Port of the event half of MATLAB's +State package: the successor states an event produces at one node...
The answer of @@SolverCTMC/getFirstPassTMoments.m.
Matrix< T > mall
(nstates x nmax), one row per starting state
std::vector< std::size_t > source
resolved 0-based rows; empty = conditional stationary
std::vector< std::size_t > target
resolved 0-based rows
std::vector< T > m
(nmax) moments for a passage started uniformly in A
The answer of @@SolverCTMC/getCdfFirstPassT.m: the [F(t), t] curve with its grid, density and resolve...
std::vector< double > F
CDF at t, clamped to [0, 1].
std::vector< double > t
the grid, 1000 points to the horizon
std::vector< std::size_t > target
resolved 0-based rows
std::vector< std::size_t > source
resolved 0-based rows; empty = conditional stationary
std::vector< double > f
density at t
[infGen, eventFilt, ev] of @@SolverCTMC/getGenerator.m.
std::vector< std::vector< Matrix< T > > > preempt_filt
std::vector< NetState< T > > space
row i of Q is space[i]
std::vector< Matrix< T > > filt
eventFilt: filt[a] holds only what synchronization sync[a] contributed, so sum_a filt[a] is the off-d...
std::vector< Sync< T > > sync
ev, the reference's sn.sync
std::vector< std::vector< Matrix< T > > > start_filt
The DERIVED START/PREEMPT filtrations, indexed [station-1][class-1].
Matrix< T > Q
the infinitesimal generator
The SolverCTMC knobs this port honours.
bool keep_filtration
Keep the per-synchronization EVENT FILTRATION alongside Q.
Everything one CTMC solve produces.
[stateSpace, localStateSpace] of @@SolverCTMC/getStateSpace.m.
std::vector< NetState< T > > space
The enumerated states.
std::vector< std::size_t > node_width
column width of each stateful node's block
std::vector< Matrix< T > > local
localStateSpace{f}: one matrix per stateful node, its DISTINCT local rows in first-appearance order.
Matrix< T > flat
the blocks concatenated, as MATLAB returns them
The time-dependent answer of one getTranProb* query.
Matrix< T > pit
(ntimes x nstates) occupancy over the solved chain
std::vector< T > t
The reference returns Pi_t = [t, pi_t], one matrix with time glued on as column 1.
Matrix< T > labels
(nstates x width) the state descriptor the query asked for
What one transient CTMC solve produces.
static constexpr double FineTol
Definition lang_types.h:668
A passage-time law on a grid.
std::vector< double > F
std::vector< double > f
Per-source and pi0-weighted passage moments.
Matrix< T > mall
(nstates x nmax), zero on the target, inf where unreachable
std::vector< T > m
the pi0-weighted moment vector
One network state: the per-stateful-node local rows it is composed of.
Definition state.h:2140
Second-order description of a steady-state estimator.