LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mam_ldqbd_flatten.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_MAM_SOLVER_MAM_LDQBD_FLATTEN_H
6#define LINE_SOLVERS_MAM_SOLVER_MAM_LDQBD_FLATTEN_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The two reductions that let an LD-QBD stand in for an enumerated CTMC.
12 *
13 * Ports of `solver_mam_ldqbd_flatten.m` and `solver_mam_ldqbd_avg.m`. They
14 * exist for one caller, the SolverENV state-vector analyzer, and the shape of
15 * that caller is what explains them: it propagates a DISTRIBUTION over a stage's
16 * state space through the stage's sojourn, so it needs the stage as a flat
17 * generator it can exponentiate, and it needs to reduce an ARBITRARY vector over
18 * that space to means -- not the stationary one, which `solver_mam_ldqbd` alone
19 * would give it.
20 *
21 * WHY FLATTENING IS NOT A LOSS HERE. `solver_mam_ldqbd` produces the chain as
22 * block-tridiagonal (Q0 up, Q1 within, Q2 down) and solves it by a level-by-
23 * level recursion that never forms the whole matrix -- which is the point of a
24 * QBD. The state-vector analyzer cannot use that recursion: it does not want a
25 * stationary vector, it wants `exp(Q t)` applied to a vector it brings with it.
26 * The blocks are finite here (`Nlev` is the closed population, or the open
27 * truncation from `cutoff`), so the flat matrix EXISTS; it is O(Nlev^2 nPhases^2)
28 * to store where the blocks are O(Nlev nPhases^2), and that is the cost of the
29 * question being asked.
30 *
31 * LEVEL 0 IS ONE STATE AND THE REST ARE nPhases WIDE, so the offsets are not a
32 * multiple of the level index. `levelOf` carries the level of each flat state
33 * rather than making the caller recompute it, because getting that mapping wrong
34 * misreports the queue length without making the generator invalid.
35 */
36
37#include <cstddef>
38#include <vector>
39
40#include "line/num/number.h"
42#include "line/util/error.h"
43#include "line/util/matrix.h"
44
45namespace line {
46namespace mam {
47
48/** The flat generator of an LD-QBD, with the level each flat state belongs to. */
49template <class T>
50struct LdqbdFlat {
51 Matrix<T> Q; ///< the dense generator over level/phase states
52 std::vector<std::size_t> levelOf; ///< levelOf[s] = the queue level of flat state s
53};
54
55/** Port of `solver_mam_ldqbd_flatten.m`. */
56template <class T>
58 const std::size_t Nlev = ld.Nlev;
59 if (ld.Q1.size() != Nlev + 1)
60 throw InputError(
61 "solver_mam_ldqbd_flatten: the block set carries " + std::to_string(ld.Q1.size()) +
62 " within-level blocks for " + std::to_string(Nlev + 1) + " levels");
63
64 std::vector<std::size_t> levelSize(Nlev + 1, 0), levelStart(Nlev + 2, 0);
65 for (std::size_t n = 0; n <= Nlev; ++n) {
66 levelSize[n] = ld.Q1[n].rows();
67 levelStart[n + 1] = levelStart[n] + levelSize[n];
68 }
69 const std::size_t dim = levelStart[Nlev + 1];
70
71 LdqbdFlat<T> out;
72 out.Q = Matrix<T>(dim, dim, num_traits<T>::from_int(0));
73 out.levelOf.assign(dim, 0);
74 for (std::size_t n = 0; n <= Nlev; ++n) {
75 const std::size_t r0 = levelStart[n], rn = levelSize[n];
76 for (std::size_t a = 0; a < rn; ++a) out.levelOf[r0 + a] = n;
77 for (std::size_t a = 0; a < rn; ++a)
78 for (std::size_t b = 0; b < rn; ++b) out.Q(r0 + a, r0 + b) = ld.Q1[n](a, b);
79 if (n < Nlev) {
80 const std::size_t c0 = levelStart[n + 1], cn = levelSize[n + 1];
81 for (std::size_t a = 0; a < rn; ++a)
82 for (std::size_t b = 0; b < cn && b < ld.Q0[n].cols(); ++b)
83 out.Q(r0 + a, c0 + b) = ld.Q0[n](a, b);
84 }
85 if (n >= 1) {
86 // Q2 IS INDEXED BY LEVEL HERE, NOT BY THE MATLAB CELL POSITION.
87 // `solver_mam_ldqbd.m` writes `Q2{n}` for the level n -> n-1 block,
88 // 1-based, so the flatten there reads `Q2{n}`; this port keeps an
89 // unused placeholder at index 0 so that Q0, Q1 and Q2 all line up by
90 // level, which makes the same block `Q2[n]`. Reading `Q2[n-1]`
91 // compiles, keeps the generator valid and conserves the population
92 // -- it just shifts every departure rate down one level, which on a
93 // three-job repairman model moved the mean queue from 1.42105 to
94 // 1.98824 with the total still exactly 3.
95 const std::size_t c0 = levelStart[n - 1], cn = levelSize[n - 1];
96 for (std::size_t a = 0; a < rn && a < ld.Q2[n].rows(); ++a)
97 for (std::size_t b = 0; b < cn && b < ld.Q2[n].cols(); ++b)
98 out.Q(r0 + a, c0 + b) = ld.Q2[n](a, b);
99 }
100 }
101 return out;
102}
103
104/** Per-(station,class) means read off an arbitrary distribution over the LD-QBD. */
105template <class T>
106struct LdqbdAvg {
107 Matrix<T> QN, UN, RN, TN; ///< (M x 1), the model being single-class by construction
108};
109
110/**
111 * Port of `solver_mam_ldqbd_avg.m`: map a distribution over the flat state
112 * space to means.
113 *
114 * NOT THE STATIONARY DISTRIBUTION. This mirrors the metric formulas of
115 * `solver_mam_ldqbd` but applies them to whatever vector it is handed -- in
116 * practice the TIME-AVERAGE over an environment stage's sojourn, which is not a
117 * stationary law of anything. That is why the formulas are written out again
118 * here rather than shared: the stationary versions in `solver_mam_ldqbd` reach
119 * for quantities (the level recursion's own R matrices) that only exist at the
120 * fixed point.
121 *
122 * The vector is clipped at zero and renormalized first, as the reference does:
123 * a transient vector that a quadrature has pushed a hair negative is a numerical
124 * artifact of the propagation, not a signed measure to be propagated further.
125 */
126template <class T>
127LdqbdAvg<T> solver_mam_ldqbd_avg(const LdqbdBlocks<T>& ld, const std::vector<T>& piflat_in,
128 const std::vector<std::size_t>& levelOf) {
129 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
130 const std::size_t Nlev = ld.Nlev, M = ld.M;
131 if (ld.queueIdx == 0 || ld.refIdx == 0 || M == 0)
132 throw InputError("solver_mam_ldqbd_avg: the block set names no queue or reference station");
133 if (piflat_in.size() != levelOf.size())
134 throw InputError(
135 "solver_mam_ldqbd_avg: the distribution and the level map disagree on the size of the "
136 "state space");
137
138 std::vector<T> piflat = piflat_in;
139 T total = zero;
140 for (std::size_t s = 0; s < piflat.size(); ++s) {
141 if (num_traits<T>::to_double(piflat[s]) < 0.0) piflat[s] = zero;
142 total = T(total + piflat[s]);
143 }
144 if (num_traits<T>::to_double(total) > 0.0)
145 for (std::size_t s = 0; s < piflat.size(); ++s) piflat[s] = T(piflat[s] / total);
146
147 std::vector<T> pLevel(Nlev + 1, zero);
148 for (std::size_t s = 0; s < piflat.size(); ++s)
149 if (levelOf[s] <= Nlev) pLevel[levelOf[s]] = T(pLevel[levelOf[s]] + piflat[s]);
150
151 T mean_queue = zero;
152 for (std::size_t n = 0; n <= Nlev; ++n)
153 mean_queue = T(mean_queue + T(num_traits<T>::from_int(static_cast<long long>(n)) * pLevel[n]));
154
155 // Utilization is the fraction of the station's PEAK capacity in use,
156 // sum_n p(n)*sf(n)/utilPeak, the work-based convention CTMC, MVA, NC and
157 // serial SSA all report. Without load dependence sf(n) = min(n,c) and
158 // utilPeak = c, giving the mean fraction of the c servers in use; at c = 1
159 // that is sf(n) = 1 for every n >= 1, so the sum collapses to 1 - p(0).
160 // The `/utilPeak` is inside the sum, so the branches below must NOT divide
161 // again.
162 T util = zero;
163 {
164 const T peak = num_traits<T>::from_double(ld.utilPeak);
165 for (std::size_t n = 1; n <= Nlev && n < ld.sf.size(); ++n)
166 util = T(util + T(ld.sf[n] / peak * pLevel[n]));
167 }
168
169 LdqbdAvg<T> out;
170 out.QN = Matrix<T>(M, 1, zero);
171 out.UN = Matrix<T>(M, 1, zero);
172 out.RN = Matrix<T>(M, 1, zero);
173 out.TN = Matrix<T>(M, 1, zero);
174 const std::size_t qi = ld.queueIdx - 1, ri = ld.refIdx - 1;
175
176 if (ld.isOpen) {
177 // The throughput is the arrival rate less the share LOST at the
178 // truncation level, which is what makes the open answer depend on
179 // `cutoff` rather than silently ignoring the loss.
180 const T X = T(ld.lambda_eff * T(one - pLevel[Nlev]));
181 const T Rq = num_traits<T>::to_double(X) > 0.0 ? T(mean_queue / X) : zero;
182 out.TN(ri, 0) = X;
183 out.QN(qi, 0) = mean_queue;
184 out.UN(qi, 0) = util;
185 out.RN(qi, 0) = Rq;
186 out.TN(qi, 0) = X;
187 } else {
188 const T mean_delay = T(num_traits<T>::from_double(ld.N) - mean_queue);
189 const T X = T(mean_delay * ld.lambda_eff);
190 const T Rq = num_traits<T>::to_double(X) > 0.0 ? T(mean_queue / X) : zero;
191 out.QN(ri, 0) = mean_delay;
192 // A Delay's utilization IS its mean population: it has one server per
193 // job, so "fraction busy" has no other meaning there.
194 out.UN(ri, 0) = mean_delay;
195 out.RN(ri, 0) = num_traits<T>::to_double(ld.delayRate) > 0.0 ? T(one / ld.delayRate) : zero;
196 out.TN(ri, 0) = X;
197 out.QN(qi, 0) = mean_queue;
198 out.UN(qi, 0) = util;
199 out.RN(qi, 0) = Rq;
200 out.TN(qi, 0) = X;
201 }
202 return out;
203}
204
205} // namespace mam
206} // namespace line
207
208#endif // LINE_SOLVERS_MAM_SOLVER_MAM_LDQBD_FLATTEN_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
Dense matrix and non-owning view.
LdqbdFlat< T > solver_mam_ldqbd_flatten(const LdqbdBlocks< T > &ld)
Port of solver_mam_ldqbd_flatten.m.
LdqbdAvg< T > solver_mam_ldqbd_avg(const LdqbdBlocks< T > &ld, const std::vector< T > &piflat_in, const std::vector< std::size_t > &levelOf)
Port of solver_mam_ldqbd_avg.m: map a distribution over the flat state space to means.
Number-type abstraction for the templated API port.
Port of solver_mam_ldqbd.m: the level-dependent QBD analyzer for a single-class network of one infini...
Per-(station,class) means read off an arbitrary distribution over the LD-QBD.
Matrix< T > TN
(M x 1), the model being single-class by construction
The LD-QBD blocks and parameters, the reference's optional eighth output.
std::vector< Matrix< T > > Q0
std::vector< Matrix< T > > Q1
std::vector< Matrix< T > > Q2
std::vector< T > sf
Per-level service factor sf(n), with sf[0] unused so it lines up by level.
double utilPeak
The capacity that normalizes the utilization: max(c, max(alpha)), the LARGEST factor the load-depende...
The flat generator of an LD-QBD, with the level each flat state belongs to.
Matrix< T > Q
the dense generator over level/phase states
std::vector< std::size_t > levelOf
levelOf[s] = the queue level of flat state s