LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fluid_matrix.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_FLUID_FLUID_MATRIX_H
6#define LINE_SOLVERS_FLUID_FLUID_MATRIX_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The `matrix` fluid method: a port of `solver_fluid_matrix.m`, the
12 * formulation of Ruuskanen, Berg, Lehtinen et al., PEVA 151 (2021).
13 *
14 * WHY THIS METHOD EXISTS ALONGSIDE `closing`. Both integrate the same fluid
15 * limit; they differ in how the drift is written. `closing` enumerates events
16 * and sums their jumps. This one writes the whole drift as one matrix,
17 *
18 * dx/dt = W' theta(x) + A_lambda, W = Psi + B P A'
19 *
20 * where Psi is the block diagonal of the phase generators D0, B the column of
21 * completion rates (the row sums of D1), P the station-to-station routing and
22 * A the block diagonal of entry-phase vectors. `theta(x)` is the only
23 * nonlinear part: it is the mass actually IN SERVICE,
24 *
25 * theta = x / (sum of x at the station) * min(servers, sum of x)
26 *
27 * so it equals x while the station is under-loaded and saturates at the server
28 * count above it. Writing the drift this way is what lets the p-norm smoothing
29 * be applied in one place, and it is why MATLAB and native Python make this
30 * the DEFAULT method.
31 *
32 * IT INTEGRATES ONCE, NOT ITERATIVELY. `closing` restarts from its own end
33 * state repeatedly; this method picks a single horizon,
34 * 10 * iter_max / min|W|, and integrates straight to it. Same fixed point,
35 * different route.
36 *
37 * THE SOURCE IS NOT IN THE DYNAMICS. An EXT station's states are held at zero
38 * with `theta = 0`, and its arrivals are injected directly into the phases of
39 * the queues they route to, through `A_lambda`. The reference also strips the
40 * routing back INTO a Source first, so an open class cannot cycle through it.
41 * That is why the fluid state of an open model carries no Source mass here,
42 * unlike the `closing` drift, which keeps a unit pool.
43 *
44 * DISABLED PAIRS. A (station, class) with no service still contributes one
45 * placeholder column so the block structure lines up; the reference marks it
46 * with NaN in A and then drops every NaN column from W. Reproduced, because
47 * the surviving index order is what every result map is aligned to.
48 */
49
50#include <algorithm>
51#include <cmath>
52#include <cstddef>
53#include <limits>
54#include <vector>
55
60#include "line/util/error.h"
61#include "line/util/matrix.h"
62
63namespace line {
64namespace fluid {
65
66/** The assembled matrix-form drift and the maps that read metrics off it. */
68 Matrix<double> W; ///< (n x n) drift generator
69 std::vector<double> alambda; ///< (n) external arrivals per state
70 std::vector<double> x0; ///< (n) initial state
71 std::vector<std::size_t> qa; ///< (n) 0-based station of each state
72 std::vector<double> sa; ///< (n) server count of that station; unread where is_inf
73 std::vector<bool> is_source; ///< (n) state belongs to an EXT station
74 std::vector<bool> is_inf; ///< (n) state belongs to an INF station
75 Matrix<double> sqc, suc, stc; ///< (M*K x n) queue, utilization, throughput maps
76 Matrix<double> src_arrival; ///< (M x K) per-class arrival rate of each EXT station
77 std::size_t nstates = 0;
78 double min_rate = 1.0; ///< smallest nonzero |W|, sets the horizon
79 double pstar = 0.0; ///< >0 selects the p-norm smoothing
80 /**
81 * When true, min(E[n], c) is replaced by E[min(n, c)] under the station's
82 * equilibrium geometric marginal. Set ONLY by the degeneracy repair in
83 * `solver_fluid`: min() is FLAT above the server count, so a network of
84 * saturated stations has a CONTINUUM of fixed points and the integrator
85 * returns whichever one it stopped at. See BUGS.md, _kb/06-solver-catalog.md.
86 */
87 bool var_closure = false;
88};
89
90namespace detail {
91
92/**
93 * Port of `compute_theta`: the mass in service.
94 *
95 * The FineTol in the denominator is the reference's, and is load bearing -- an
96 * empty station would otherwise divide zero by zero and poison the drift.
97 */
98inline void fluid_matrix_theta(const FluidMatrixSystem& s, const double* x,
99 std::vector<double>& theta) {
100 const std::size_t n = s.nstates;
101 // The total mass at each station, which every state of it shares.
102 std::vector<double> station_sum(s.sa.size(), 0.0);
103 std::vector<double> sums(n, 0.0);
104 std::size_t nst = 0;
105 for (std::size_t i = 0; i < n; ++i) nst = std::max(nst, s.qa[i] + 1);
106 station_sum.assign(nst, 0.0);
107 for (std::size_t i = 0; i < n; ++i) station_sum[s.qa[i]] += x[i];
108 for (std::size_t i = 0; i < n; ++i) {
109 const double tot = lang::GlobalConstants::FineTol + station_sum[s.qa[i]];
110 if (s.pstar > 0.0) {
111 // p-norm smoothing: ghat stands in for min(1, c/tot) smoothly. An
112 // INF station has k = infinity and so no min() to smooth; sa carries
113 // no server count there, which would read it as a finite queue.
114 const double c = s.sa[i];
115 double g = 1.0;
116 if (!s.is_inf[i] && tot > 0.0 && c > 0.0)
117 g = 1.0 / std::pow(1.0 + std::pow(tot / c, s.pstar), 1.0 / s.pstar);
118 if (std::isnan(g)) g = 0.0;
119 theta[i] = x[i] * g;
120 } else if (s.var_closure) {
121 // E[min(n, c)] UNDER A GEOMETRIC MARGINAL, not min(E[n], c):
122 // n ~ Geometric(mean m) => E[min(n,c)] = sum_{k=1..c} p^k
123 // = m * (1 - p^c), p = m/(1+m).
124 // Strictly increasing in m (slope 1/(1+m)^2 at c = 1, still 1e-2 at
125 // m = 9, a restoring force the integrator can follow inside its
126 // horizon) and with the same asymptotes, -> c as m -> inf and -> m
127 // as m -> 0. An INF station has a server per job, so there is no
128 // min() to close and sa carries no server count there.
129 double emin;
130 if (s.is_inf[i]) {
131 emin = tot;
132 } else {
133 const double p = tot > 0.0 ? tot / (1.0 + tot) : 0.0;
134 emin = tot * (1.0 - std::pow(p, std::max(s.sa[i], 0.0)));
135 if (!std::isfinite(emin)) emin = 0.0;
136 emin = std::min(emin, tot);
137 }
138 theta[i] = x[i] / tot * emin;
139 } else if (s.is_inf[i]) {
140 // EVERY job at an INF station is in service: there is no min() to
141 // apply, the same statement the two branches above make. The
142 // reference writes it as an ARITHMETIC bound instead -- S(infServers)
143 // = sum(NK) in solver_fluid_matrix.m, njobs.elementSum() in
144 // MatrixMethodAnalyzer.java -- which is +Inf as soon as one class is
145 // open and the exact population bound when none is, so min() is a
146 // no-op there either way. `sa` cannot carry that bound: it is a
147 // double substituted from the CLOSED population alone, which is zero
148 // in an open model and too small in a mixed one, and min(0, tot) = 0
149 // froze every delay in an open model solid -- no mass left it, so its
150 // queue grew without bound and its throughput stayed at zero.
151 theta[i] = x[i];
152 } else {
153 theta[i] = x[i] / tot * std::min(s.sa[i], tot);
154 }
155 if (s.is_source[i]) theta[i] = 0.0; // the Source is out of the dynamics
156 }
157}
158
159} // namespace detail
160
161/** Assemble the matrix-form drift of `sn`. */
162template <class T>
164 const std::vector<double>& init_sol, double pstar) {
165 const std::size_t M = sn.nstations, K = sn.nclasses;
166 const FluidLayout L = fluid_layout(sn);
168 s.pstar = pstar;
169
170 // ---- station-to-station routing, via the stochastic complement ---------
171 // sn.rt is over stateful nodes; a Router is stateful but not a station, so
172 // the reference complements it out rather than indexing around it.
173 const std::size_t S = sn.nof_stateful();
174 std::vector<std::size_t> keep_idx;
175 keep_idx.reserve(M * K);
176 for (std::size_t i = 0; i < M; ++i) {
177 const std::size_t isf = sn.stateful_of_station(i + 1) - 1;
178 for (std::size_t r = 0; r < K; ++r) keep_idx.push_back(isf * K + r);
179 }
180 Matrix<double> rt_full(S * K, S * K, 0.0);
181 if (sn.rt.rows() == S * K)
182 for (std::size_t a = 0; a < S * K; ++a)
183 for (std::size_t b = 0; b < S * K; ++b)
184 rt_full(a, b) = num_traits<T>::to_double(sn.rt(a, b));
185 Matrix<double> P = mc::dtmc_stochcomp(rt_full, keep_idx);
186
187 // Per-class arrival rate of each EXT station, and the removal of any
188 // routing back INTO it: an open class leaves the Source once.
189 Matrix<double> src_arrival(M, K, 0.0);
190 for (std::size_t i = 0; i < M; ++i) {
191 if (sn.stations[i].sched != lang::SchedStrategy::EXT) continue;
192 for (std::size_t r = 0; r < K; ++r) {
193 if (!L.enabled[i][r]) continue;
195 const std::size_t nn = sn.service[i][r].D0.rows();
196 mp.D0 = Matrix<double>(nn, nn, 0.0);
197 mp.D1 = Matrix<double>(nn, nn, 0.0);
198 for (std::size_t a = 0; a < nn; ++a)
199 for (std::size_t b = 0; b < nn; ++b) {
200 mp.D0(a, b) = num_traits<T>::to_double(sn.service[i][r].D0(a, b));
201 mp.D1(a, b) = num_traits<T>::to_double(sn.service[i][r].D1(a, b));
202 }
203 const double mean = mam::map_mean(mp);
204 if (mean > 0.0) src_arrival(i, r) = 1.0 / mean;
205 if (src_arrival(i, r) > 0.0)
206 for (std::size_t j = 0; j < M; ++j) {
207 if (j == i) continue;
208 for (std::size_t q = 0; q < K; ++q) P(j * K + q, i * K + r) = 0.0;
209 }
210 }
211 }
212
213 // ---- the block-structured state space ---------------------------------
214 // One column per phase, or a single placeholder for a disabled pair. The
215 // placeholder is dropped below; it exists so the blocks line up first.
216 std::vector<std::size_t> blk_state; // first state index of block (i,r)
217 std::vector<std::size_t> blk_len; // phases in block (i,r), 0 when disabled
218 std::size_t nfull = 0;
219 for (std::size_t i = 0; i < M; ++i)
220 for (std::size_t r = 0; r < K; ++r) {
221 blk_state.push_back(nfull);
222 const std::size_t p = L.kic[i][r];
223 blk_len.push_back(p);
224 nfull += (p == 0) ? 1 : p;
225 }
226
227 Matrix<double> Wfull(nfull, nfull, 0.0);
228 std::vector<double> pie_full(nfull, 0.0), brate(nfull, 0.0);
229 std::vector<bool> disabled_state(nfull, false);
230 for (std::size_t i = 0; i < M; ++i)
231 for (std::size_t r = 0; r < K; ++r) {
232 const std::size_t b = blk_state[i * K + r], p = blk_len[i * K + r];
233 if (p == 0) {
234 disabled_state[b] = true; // the reference's NaN column
235 continue;
236 }
237 const lang::Distrib<T>& d = sn.service[i][r];
238 const std::vector<double> pie = detail::fluid_pie(d);
239 for (std::size_t a = 0; a < p; ++a) {
240 pie_full[b + a] = a < pie.size() ? pie[a] : 0.0;
241 for (std::size_t c = 0; c < p; ++c)
242 Wfull(b + a, b + c) = num_traits<T>::to_double(d.D0(a, c)); // Psi
243 double row = 0.0;
244 for (std::size_t c = 0; c < d.D1.cols(); ++c)
245 row += num_traits<T>::to_double(d.D1(a, c));
246 brate[b + a] = row; // B
247 }
248 }
249 // W += B P A': a completion at (i,r,a) routes to (j,l) and enters phase c.
250 for (std::size_t ir = 0; ir < M * K; ++ir) {
251 const std::size_t bi = blk_state[ir], pi = blk_len[ir];
252 if (pi == 0) continue;
253 for (std::size_t jl = 0; jl < M * K; ++jl) {
254 const double p = P(ir, jl);
255 if (!(p > 0.0)) continue;
256 const std::size_t bj = blk_state[jl], pj = blk_len[jl];
257 if (pj == 0) continue;
258 for (std::size_t a = 0; a < pi; ++a)
259 for (std::size_t c = 0; c < pj; ++c)
260 Wfull(bi + a, bj + c) += brate[bi + a] * p * pie_full[bj + c];
261 }
262 }
263
264 // ---- external arrivals, injected at the queues, not the Source --------
265 std::vector<double> alam_full(nfull, 0.0);
266 for (std::size_t i = 0; i < M; ++i) {
267 if (sn.stations[i].sched == lang::SchedStrategy::EXT) continue;
268 for (std::size_t r = 0; r < K; ++r) {
269 const std::size_t b = blk_state[i * K + r], p = blk_len[i * K + r];
270 if (p == 0) continue;
271 double rate = 0.0;
272 for (std::size_t sidx = 0; sidx < M; ++sidx)
273 if (src_arrival(sidx, r) > 0.0)
274 rate += src_arrival(sidx, r) * P(sidx * K + r, i * K + r);
275 if (!(rate > 0.0)) continue;
276 for (std::size_t a = 0; a < p; ++a) alam_full[b + a] = pie_full[b + a] * rate;
277 }
278 }
279
280 // ---- drop the disabled placeholders and build the result maps ---------
281 std::vector<std::size_t> keep;
282 for (std::size_t i = 0; i < nfull; ++i)
283 if (!disabled_state[i]) keep.push_back(i);
284 const std::size_t n = keep.size();
285 s.nstates = n;
286 s.W = Matrix<double>(n, n, 0.0);
287 for (std::size_t a = 0; a < n; ++a)
288 for (std::size_t b = 0; b < n; ++b) s.W(a, b) = Wfull(keep[a], keep[b]);
289 s.alambda.assign(n, 0.0);
290 s.x0.assign(n, 0.0);
291 s.qa.assign(n, 0);
292 s.sa.assign(n, 1.0);
293 s.is_source.assign(n, false);
294 s.is_inf.assign(n, false);
295 s.sqc = Matrix<double>(M * K, n, 0.0);
296 s.suc = Matrix<double>(M * K, n, 0.0);
297 s.stc = Matrix<double>(M * K, n, 0.0);
298
299 // The stand-in server count of an INF station. The reference uses the total
300 // population, which bounds the mass at any one station, so its min() is a
301 // no-op; this sum drops the open classes and is therefore NOT that bound,
302 // which is why `is_inf` and not `sa` decides where the min() applies. It
303 // survives only as the divisor of the utilization map, whose delay rows are
304 // restated as the queue length itself by the caller.
305 double closed_pop = 0.0;
306 for (std::size_t r = 0; r < K; ++r)
307 if (std::isfinite(sn.classes[r].population)) closed_pop += sn.classes[r].population;
308
309 // Map every surviving state back to its (station, class, phase).
310 std::vector<std::size_t> st_of(nfull, 0), cl_of(nfull, 0), ph_of(nfull, 0);
311 for (std::size_t i = 0; i < M; ++i)
312 for (std::size_t r = 0; r < K; ++r) {
313 const std::size_t b = blk_state[i * K + r], p = blk_len[i * K + r];
314 for (std::size_t a = 0; a < (p == 0 ? 1u : p); ++a) {
315 st_of[b + a] = i;
316 cl_of[b + a] = r;
317 ph_of[b + a] = a;
318 }
319 }
320
321 for (std::size_t a = 0; a < n; ++a) {
322 const std::size_t f = keep[a], i = st_of[f], r = cl_of[f], k = ph_of[f];
323 const double c = sn.stations[i].nservers;
324 const double servers = std::isfinite(c) ? c : closed_pop;
325 s.qa[a] = i;
326 s.sa[a] = servers;
327 s.alambda[a] = alam_full[f];
328 s.is_source[a] = (sn.stations[i].sched == lang::SchedStrategy::EXT);
329 s.is_inf[a] = !std::isfinite(c);
330 s.sqc(i * K + r, a) = 1.0;
331 s.suc(i * K + r, a) = (servers > 0.0) ? 1.0 / servers : 0.0;
332 double row = 0.0;
333 for (std::size_t cc = 0; cc < sn.service[i][r].D1.cols(); ++cc)
334 row += num_traits<T>::to_double(sn.service[i][r].D1(k, cc));
335 s.stc(i * K + r, a) = row;
336 // The Source carries no mass: its arrivals enter downstream.
337 s.x0[a] = s.is_source[a] ? 0.0 : (f < init_sol.size() ? 0.0 : 0.0);
338 }
339
340 // The initial state is given in the fluid layout's order (one entry per
341 // enabled phase); map it onto the surviving states.
342 if (!init_sol.empty()) {
343 for (std::size_t a = 0; a < n; ++a) {
344 const std::size_t f = keep[a], i = st_of[f], r = cl_of[f], k = ph_of[f];
345 if (s.is_source[a] || !L.enabled[i][r]) continue;
346 const std::size_t src = L.qidx[i][r] + k;
347 if (src < init_sol.size()) s.x0[a] = init_sol[src];
348 }
349 }
350
351 s.src_arrival = src_arrival;
352 double mr = std::numeric_limits<double>::infinity();
353 for (std::size_t a = 0; a < n; ++a)
354 for (std::size_t b = 0; b < n; ++b) {
355 const double v = std::fabs(s.W(a, b));
356 if (v > 0.0) mr = std::min(mr, v);
357 }
358 s.min_rate = std::isfinite(mr) ? mr : 1.0;
359 return s;
360}
361
362/** The drift dx/dt = W' theta(x) + A_lambda. */
363inline std::function<void(double, const double*, double*)> fluid_matrix_drift(
364 const FluidMatrixSystem& s) {
365 const std::size_t n = s.nstates;
366 return [s, n](double, const double* x, double* dx) {
367 std::vector<double> theta(n, 0.0);
368 detail::fluid_matrix_theta(s, x, theta);
369 for (std::size_t i = 0; i < n; ++i) {
370 double acc = s.alambda[i];
371 for (std::size_t j = 0; j < n; ++j) acc += s.W(j, i) * theta[j]; // W' theta
372 dx[i] = acc;
373 }
374 };
375}
376
377/**
378 * Is the returned point one of a CONTINUUM of fixed points?
379 *
380 * A station whose queue exceeds its server count has theta pinned at the server
381 * count, so the drift cannot tell one split of the mass between two such
382 * stations from another. The test is direct rather than structural: move a
383 * little mass from one station to another along a POPULATION-CONSERVING
384 * direction and see whether the drift moves at all. Both directions are tried,
385 * because the integrator typically stops on the BOUNDARY of the degenerate set,
386 * where one of the two does change the drift.
387 *
388 * The DIRECTIONAL DERIVATIVE is the scale-free quantity to threshold: a live
389 * direction moves the drift at the station's own service rate (measured
390 * 9.99e-01) and a null one only by the FineTol the share carries (measured
391 * 1.00e-08), four orders apart. Source and INF states are excluded: neither can
392 * be the pinned coordinate. Returns false whenever the point is not a fixed
393 * point at all, so a transient run is never repaired.
394 */
396 const FluidMatrixSystem& s,
397 const std::function<void(double, const double*, double*)>& drift,
398 const std::vector<double>& x, std::size_t K) {
399 const std::size_t n = s.nstates;
400 if (x.size() != n || n == 0 || K == 0) return false;
401 double max_abs_x = 1.0;
402 for (std::size_t i = 0; i < n; ++i) max_abs_x = std::max(max_abs_x, std::fabs(x[i]));
403 std::vector<double> d0(n, 0.0);
404 drift(0.0, x.data(), d0.data());
405 double max_d0 = 0.0;
406 for (std::size_t i = 0; i < n; ++i) max_d0 = std::max(max_d0, std::fabs(d0[i]));
407 if (max_d0 > 1e-6 * max_abs_x) return false;
408 if (s.sqc.rows() % K != 0) return false;
409 const std::size_t M = s.sqc.rows() / K;
410
411 double rate_scale = 0.0;
412 for (std::size_t a = 0; a < n; ++a)
413 for (std::size_t b = 0; b < n; ++b)
414 rate_scale = std::max(rate_scale, std::fabs(s.W(a, b)));
415 rate_scale = std::max(rate_scale, 1e-12);
416
417 const double step = 1e-3 * max_abs_x;
418 std::vector<double> xp(n, 0.0), dp(n, 0.0);
419 for (std::size_t r = 0; r < K; ++r) {
420 // PER CLASS, NOT PER STATION: a direction that moves a station's mass
421 // across ALL its classes is infeasible where a SelfLoopingClass is
422 // pinned at one station, and a well-posed model then reads as
423 // degenerate. Source and INF states carry no min() to pin.
424 std::vector<std::vector<std::size_t> > groups;
425 for (std::size_t i = 0; i < M; ++i) {
426 std::vector<std::size_t> members;
427 for (std::size_t a = 0; a < n; ++a)
428 if (!s.is_source[a] && !s.is_inf[a] && s.sqc(i * K + r, a) > 0.0)
429 members.push_back(a);
430 if (!members.empty()) groups.push_back(members);
431 }
432 if (groups.size() < 2) continue;
433 std::vector<double> mass(groups.size(), 0.0);
434 for (std::size_t a = 0; a < groups.size(); ++a)
435 for (std::size_t idx : groups[a]) mass[a] += x[idx];
436 for (std::size_t a = 0; a < groups.size(); ++a) {
437 if (mass[a] <= step) continue;
438 for (std::size_t b = 0; b < groups.size(); ++b) {
439 if (a == b) continue;
440 xp = x;
441 for (std::size_t idx : groups[a]) xp[idx] -= step * x[idx] / mass[a];
442 for (std::size_t idx : groups[b])
443 xp[idx] += (mass[b] > 0.0) ? step * x[idx] / mass[b]
444 : step / static_cast<double>(groups[b].size());
445 drift(0.0, xp.data(), dp.data());
446 double max_dd = 0.0;
447 for (std::size_t i = 0; i < n; ++i)
448 max_dd = std::max(max_dd, std::fabs(dp[i] - d0[i]));
449 if (max_dd / step <= 1e-4 * rate_scale) return true;
450 }
451 }
452 }
453 return false;
454}
455
456} // namespace fluid
457} // namespace line
458
459#endif // LINE_SOLVERS_FLUID_FLUID_MATRIX_H
std::size_t rows() const
Definition matrix.h:89
A network plus its refreshed NetworkStruct.
Stochastic complement of a DTMC partition, a port of matlab/lib/kpctoolbox/mc/dtmc_stochcomp....
The exception types the port throws.
The fluid drift: a port of solver_fluid_odes.m and the ode_jumps_new / ode_rate_base / ode_rates_clos...
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
FluidLayout fluid_layout(const qn::NetworkStruct< T > &sn)
Port of the layout half of solver_fluid_odes.m.
Definition fluid_odes.h:282
bool fluid_matrix_degenerate(const FluidMatrixSystem &s, const std::function< void(double, const double *, double *)> &drift, const std::vector< double > &x, std::size_t K)
Is the returned point one of a CONTINUUM of fixed points?
FluidMatrixSystem fluid_matrix_system(const qn::NetworkStruct< T > &sn, const std::vector< double > &init_sol, double pstar)
Assemble the matrix-form drift of sn.
std::function< void(double, const double *, double *)> fluid_matrix_drift(const FluidMatrixSystem &s)
The drift dx/dt = W' theta(x) + A_lambda.
T map_mean(const Map< T > &m)
Mean inter-arrival time, 1/lambda.
Definition map_moment.h:101
Matrix< T > dtmc_stochcomp(const Matrix< T > &P, const std::vector< std::size_t > &keep)
Stochastic complement of a DTMC partition, a port of matlab/lib/kpctoolbox/mc/dtmc_stochcomp....
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Where each (station, class) block sits in the state vector.
Definition fluid_odes.h:86
std::vector< std::vector< std::size_t > > qidx
0-based first index of (i,r)
Definition fluid_odes.h:88
std::vector< std::vector< bool > > enabled
whether (i,r) is served at all
Definition fluid_odes.h:90
std::vector< std::vector< std::size_t > > kic
phases held by (i,r); 0 when disabled
Definition fluid_odes.h:89
The assembled matrix-form drift and the maps that read metrics off it.
double min_rate
smallest nonzero |W|, sets the horizon
bool var_closure
When true, min(E[n], c) is replaced by E[min(n, c)] under the station's equilibrium geometric margina...
Matrix< double > stc
(M*K x n) queue, utilization, throughput maps
Matrix< double > src_arrival
(M x K) per-class arrival rate of each EXT station
std::vector< double > alambda
(n) external arrivals per state
double pstar
>0 selects the p-norm smoothing
Matrix< double > W
(n x n) drift generator
std::vector< bool > is_inf
(n) state belongs to an INF station
std::vector< double > sa
(n) server count of that station; unread where is_inf
std::vector< double > x0
(n) initial state
std::vector< bool > is_source
(n) state belongs to an EXT station
std::vector< std::size_t > qa
(n) 0-based station of each state
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:853
static constexpr double FineTol
Definition lang_types.h:760
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