LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fluid_kp.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_KP_H
6#define LINE_SOLVERS_FLUID_FLUID_KP_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_fluid_kp.m`: the fluid AND diffusion limits of the
12 * (MAP_t/Ph_t/inf)^N network of Y. M. Ko and J. Pender, "Diffusion limits for the
13 * (MAP_t/Ph_t/inf)^N queueing network", Oper. Res. Lett. 45 (2017) 248-253.
14 *
15 * The mean and the covariance are integrated JOINTLY:
16 *
17 * dq/dt = A f(t,q)
18 * dSigma/dt = J Sigma + Sigma J' + G, J = A df/dq, G = A diag(f) A'
19 *
20 * with A the jump matrix whose column e is the jump vector of event e. G is
21 * exactly dH dH' of the paper's Theorem 3.3, each independent Poisson term
22 * contributing l_e l_e' f_e. Where f is affine in q -- infinite-server stations
23 * and the arrival phase process -- J does not depend on q and both equations close
24 * EXACTLY, so for the (MAP/Ph/inf)^N case the mean and covariance are exact rather
25 * than asymptotic. Finite-server stations are admitted through the usual fluid
26 * min(x,c) term, where the covariance degrades to a linear noise approximation.
27 *
28 * THIS IS THE ONLY FLUID METHOD IN THE PORT THAT RETURNS A SECOND MOMENT FOR AN
29 * OPEN NETWORK, and it is the only one whose second moment is TRANSIENT rather
30 * than stationary -- `minnormal` solves a stationary Lyapunov equation, this
31 * integrates the covariance along the trajectory.
32 *
33 * IT DELIBERATELY DOES NOT REUSE THE CLOSING ODE. That formulation routes a
34 * departure from the source to the destination station and returns mass through
35 * the STATIONARY arrival-instant vector pie, replacing the D1' operator by the
36 * rank-one map pie*(D1*e)', i.e. by the PH renewal process with representation
37 * (pie, D0). Its stationary arrival rate is exact but its autocorrelation is gone,
38 * and a non-renewal arrival stream is the entire point of a MAP.
39 *
40 * WHAT THE `_t` OF (MAP_t/Ph_t/inf)^N IS. A `MAPt` or `PHt` carries a
41 * piecewise-constant (D0(t), D1(t)) schedule, and `kp_pair_at` returns the pair
42 * in force at time t -- the nominal pair for a process with no schedule, which
43 * is what the reference's `local_pair_at` returns in the same case. Three things
44 * follow from a schedule being present, and all three are consequences of the
45 * SAME fact, that a cyclic model has no fixed point:
46 *
47 * THE HORIZON. An unbounded timespan is resolved from the slowest rate, as
48 * before, but is then extended to at least ten full periods so that the
49 * trajectory has reached its periodic regime before anything is read off it.
50 *
51 * THE STEP CAP. LSODA picks its step for accuracy of the SOLUTION and will
52 * happily step over a whole segment of a schedule, integrating a rate that was
53 * never in force. `h_max` is capped at a quarter of the narrowest segment.
54 *
55 * THE STEADY-STATE ANSWER IS A TIME AVERAGE. The value at the horizon is an
56 * arbitrary point of the cycle, at which the source and the station throughput
57 * do not even agree. The metrics are instead the trapezoidal average over the
58 * last full period, on a mesh refined uniformly and BRACKETED at every segment
59 * boundary, so that no trapezoid interval straddles a jump in the arrival rate.
60 *
61 * A non-cyclic schedule has a fixed point again -- it is constant on its last
62 * segment -- so it takes none of the three, exactly as in the reference.
63 *
64 * State layout, station-major, arrival phases before service phases:
65 * u-block one per (EXT station, class): arrival MAP phase occupancy, sum 1
66 * x-block one per (queueing station, class): fluid count in each service phase
67 */
68
69#include <algorithm>
70#include <cmath>
71#include <cstddef>
72#include <limits>
73#include <string>
74#include <type_traits>
75#include <vector>
76
82#include "line/util/error.h"
83#include "line/util/lsoda.h"
84#include "line/util/matrix.h"
85
86namespace line {
87namespace fluid {
88
89/** One (station, class) block of the Ko-Pender state vector. */
90struct KpBlock {
91 std::size_t station = 0;
92 std::size_t cls = 0;
93 std::size_t offset = 0;
94 std::size_t nphases = 0;
95};
96
97/** The five event families of Ko-Pender (3.1)-(3.2). */
98enum class KpEventKind {
99 ArrivalPhase = 1, ///< A0: arrival-MAP phase change without an arrival
100 Arrival = 2, ///< A1: phase change WITH an arrival, into a service phase
101 ServicePhase = 3, ///< S: service phase change inside a station
102 Departure = 4, ///< D: completion leaving the network
103 Routed = 5 ///< R: completion routed onward
104};
105
106struct KpEvent {
108 std::size_t i = 0, c = 0; ///< source station and class
109 std::size_t k = 0, j = 0; ///< source and target phase of the modulating chain
110 std::size_t n = 0, l = 0; ///< destination station and class
111 std::size_t ip = 0; ///< destination entry phase
112 std::size_t off_src = 0; ///< offset of the source block
113 std::size_t off_dst = 0; ///< offset of the destination block
114 double weight = 1.0; ///< routing probability times entry-phase probability
115 /** The jump: -1 at `minus`, +1 at each `plus`; a phase change carries both. */
116 std::vector<std::size_t> minus, plus;
117};
118
119/** The transient the covariance equation produces, i.e. `getTranAvgVar`. */
121 std::vector<double> t;
122 std::vector<Matrix<double>> QVar; ///< per time point, (nstations x nclasses)
123 std::vector<Matrix<double>> Sigma; ///< per time point, (dim x dim)
124 std::vector<std::vector<double>> q;
125};
126
127namespace kp_detail {
128
129/** The (D0, D1) pair of a station-class, lowered to double. */
130template <class T>
131void kp_pair(const qn::NetworkStruct<T>& sn, std::size_t i, std::size_t r, Matrix<double>& D0,
132 Matrix<double>& D1) {
133 const lang::Distrib<T>& d = sn.service[i][r];
134 const std::size_t n = d.D0.rows();
135 D0 = Matrix<double>(n, n, 0.0);
136 D1 = Matrix<double>(n, n, 0.0);
137 for (std::size_t a = 0; a < n; ++a)
138 for (std::size_t b = 0; b < n; ++b) {
139 D0(a, b) = num_traits<T>::to_double(d.D0(a, b));
140 D1(a, b) = num_traits<T>::to_double(d.D1(a, b));
141 }
142}
143
144/** `map_pie`: the arrival-instant phase distribution of (D0, D1). */
145inline std::vector<double> kp_pie(const Matrix<double>& D0, const Matrix<double>& D1) {
146 if (D0.rows() <= 1) return std::vector<double>{1.0};
147 mam::Map<double> m;
148 m.D0 = D0;
149 m.D1 = D1;
150 return mam::map_pie(m);
151}
152
153/**
154 * The stationary phase distribution of the modulating chain Q = D0 + D1, from
155 * `[Q'; ones] \ [zeros; 1]`.
156 *
157 * A SOURCE STARTS IN ITS STATIONARY PHASE, NOT IN A KNOWN ONE, which is why the
158 * initial covariance is diag(theta) - theta theta' rather than zero: zero would
159 * assert a known initial phase and understate the variance early on.
160 */
161inline std::vector<double> kp_stationary(const Matrix<double>& D0, const Matrix<double>& D1) {
162 const std::size_t h = D0.rows();
163 if (h == 1) return std::vector<double>{1.0};
164 // The least-squares system the reference solves: Q' theta = 0 with sum = 1.
165 Matrix<double> A(h + 1, h, 0.0);
166 for (std::size_t a = 0; a < h; ++a)
167 for (std::size_t b = 0; b < h; ++b) A(a, b) = D0(b, a) + D1(b, a);
168 for (std::size_t b = 0; b < h; ++b) A(h, b) = 1.0;
169 std::vector<double> rhs(h + 1, 0.0);
170 rhs[h] = 1.0;
171 // Normal equations: A'A theta = A'rhs, which is what MATLAB's backslash gives
172 // for an overdetermined system.
173 Matrix<double> AtA(h, h, 0.0);
174 std::vector<double> Atb(h, 0.0);
175 for (std::size_t a = 0; a < h; ++a) {
176 for (std::size_t b = 0; b < h; ++b) {
177 double acc = 0.0;
178 for (std::size_t e = 0; e <= h; ++e) acc += A(e, a) * A(e, b);
179 AtA(a, b) = acc;
180 }
181 double acc = 0.0;
182 for (std::size_t e = 0; e <= h; ++e) acc += A(e, a) * rhs[e];
183 Atb[a] = acc;
184 }
185 std::vector<std::size_t> piv = lu_factor(AtA);
186 lu_solve(AtA, piv, Atb);
187 double s = 0.0;
188 for (std::size_t a = 0; a < h; ++a) {
189 if (Atb[a] < 0.0) Atb[a] = 0.0;
190 s += Atb[a];
191 }
192 if (s > 0.0)
193 for (std::size_t a = 0; a < h; ++a) Atb[a] /= s;
194 return Atb;
195}
196
197/** One (station, class) schedule, lowered to double. */
198struct KpSchedule {
199 std::size_t station = 0, cls = 0;
200 std::vector<double> bp; ///< boundary vector, nseg + 1 long
201 std::vector<Matrix<double>> segD0, segD1;
202 bool cyclic = false;
203};
204
205/**
206 * `local_pair_at`: the (D0, D1) in force at time t.
207 *
208 * A CYCLIC schedule wraps the offset into [0, T); a NON-CYCLIC one returns the
209 * ZERO pair outside its own window, which is the reference's behaviour and is
210 * not a defect to fix -- a process whose schedule has not started, or has ended,
211 * produces no events, and substituting the nominal pair there would invent
212 * arrivals the model does not declare.
213 */
214inline void kp_pair_at(const std::vector<KpSchedule>& sched, const Matrix<double>& nomD0,
215 const Matrix<double>& nomD1, std::size_t i, std::size_t r, double t,
216 Matrix<double>& D0, Matrix<double>& D1) {
217 for (std::size_t e = 0; e < sched.size(); ++e) {
218 const KpSchedule& sc = sched[e];
219 if (sc.station != i || sc.cls != r) continue;
220 const double T0 = sc.bp.front(), T1 = sc.bp.back();
221 const double period = T1 - T0;
222 double offset = t - T0;
223 if (sc.cyclic) {
224 if (period > 0.0) {
225 offset = std::fmod(offset, period);
226 if (offset < 0.0) offset += period;
227 } else {
228 offset = 0.0;
229 }
230 } else if (offset < 0.0 || offset >= period) {
231 const std::size_t n = sc.segD0.front().rows();
232 D0 = Matrix<double>(n, n, 0.0);
233 D1 = Matrix<double>(n, n, 0.0);
234 return;
235 }
236 const double pos = T0 + offset;
237 std::size_t idx = sc.segD0.size() - 1;
238 for (std::size_t k = 1; k < sc.bp.size(); ++k)
239 if (pos < sc.bp[k]) {
240 idx = k - 1;
241 break;
242 }
243 D0 = sc.segD0[idx];
244 D1 = sc.segD1[idx];
245 return;
246 }
247 D0 = nomD0;
248 D1 = nomD1;
249}
250
251/** `local_summarise`: the trapezoidal average over `window`, or the last value. */
252inline double kp_summarise(const std::vector<double>& series, const std::vector<double>& t,
253 double w0, double w1, bool have_window) {
254 if (series.empty()) return 0.0;
255 if (!have_window) return series.back();
256 std::vector<std::size_t> idx;
257 for (std::size_t a = 0; a < t.size(); ++a)
258 if (t[a] >= w0 && t[a] <= w1) idx.push_back(a);
259 if (idx.size() < 2) return series.back();
260 double acc = 0.0;
261 for (std::size_t a = 0; a + 1 < idx.size(); ++a)
262 acc += 0.5 * (series[idx[a]] + series[idx[a + 1]]) * (t[idx[a + 1]] - t[idx[a]]);
263 const double span = t[idx.back()] - t[idx.front()];
264 return span > 0.0 ? acc / span : series.back();
265}
266
267} // namespace kp_detail
268
269/**
270 * The Ko-Pender solve, returning both the steady table and the covariance
271 * trajectory so that neither has to integrate twice.
272 */
273template <class T>
275 FluidKpTransient* tran) {
276 if (!std::is_same<T, double>::value)
277 throw UnsupportedError(
278 "solver_fluid_kp: the covariance equation is integrated with LSODA, whose coefficients "
279 "assume double precision; rerun with --arith double");
280
281 const std::size_t M = sn.nstations, K = sn.nclasses;
282 for (std::size_t r = 0; r < K; ++r)
283 if (std::isfinite(sn.classes[r].population))
284 throw UnsupportedError(
285 "solver_fluid_kp: the 'kp' method analyses the OPEN (MAP_t/Ph_t/inf)^N network of "
286 "Ko and Pender (2017); a closed class has no arrival process to modulate. Use "
287 "'closing' or 'matrix' for closed models");
288
289 // ---- blocks -----------------------------------------------------------
290 std::vector<KpBlock> ublocks, xblocks;
291 std::size_t off = 0;
292 std::vector<std::vector<std::size_t>> uof(M, std::vector<std::size_t>(K, 0));
293 std::vector<std::vector<std::size_t>> xof(M, std::vector<std::size_t>(K, 0));
294 std::vector<std::vector<bool>> is_u(M, std::vector<bool>(K, false));
295 std::vector<std::vector<bool>> is_x(M, std::vector<bool>(K, false));
296 for (std::size_t i = 0; i < M; ++i) {
297 const bool ext = sn.stations[i].sched == lang::SchedStrategy::EXT;
298 for (std::size_t r = 0; r < K; ++r) {
299 if (sn.disabled[i][r]) continue;
300 const std::size_t h = sn.service[i][r].D0.rows();
301 const double rate = num_traits<T>::to_double(sn.rates(i, r));
302 if (h == 0 || !std::isfinite(rate) || rate <= 0.0) continue;
303 KpBlock b;
304 b.station = i;
305 b.cls = r;
306 b.offset = off;
307 b.nphases = h;
308 if (ext) {
309 ublocks.push_back(b);
310 uof[i][r] = off;
311 is_u[i][r] = true;
312 } else {
313 xblocks.push_back(b);
314 xof[i][r] = off;
315 is_x[i][r] = true;
316 }
317 off += h;
318 }
319 }
320 const std::size_t dim = off;
321 if (ublocks.empty())
322 throw InputError(
323 "solver_fluid_kp: the 'kp' method needs at least one Source with an arrival process");
324
325 bool linear_model = true;
326 for (std::size_t b = 0; b < xblocks.size(); ++b) {
327 const std::size_t i = xblocks[b].station;
328 if (sn.stations[i].sched != lang::SchedStrategy::INF &&
329 std::isfinite(sn.stations[i].nservers))
330 linear_model = false;
331 }
332 // The reference warns here. A finite-server station makes the rate functions
333 // nonlinear, so the covariance is a linear noise approximation rather than the
334 // exact second moment; it is exact for infinite-server stations. There is no
335 // warning channel in this port, so the fact is recorded on the solution.
336 (void)linear_model;
337
338 // ---- the nominal pairs and entry-phase vectors -------------------------
339 std::vector<std::vector<Matrix<double>>> D0(M, std::vector<Matrix<double>>(K)),
340 D1(M, std::vector<Matrix<double>>(K));
341 std::vector<std::vector<std::vector<double>>> pie(M, std::vector<std::vector<double>>(K));
342 // The schedules, and the NOMINAL pair of everything else. `kp_pair` reads
343 // `Distrib::D0`/`D1`, which for a MAPt/PHt already hold the width-weighted
344 // time average -- `sn_schedule_nominal`'s first two outputs -- so the
345 // nominal arm needs no special case here.
346 std::vector<kp_detail::KpSchedule> sched;
347 for (std::size_t i = 0; i < M; ++i)
348 for (std::size_t r = 0; r < K; ++r) {
349 if (!is_u[i][r] && !is_x[i][r]) continue;
350 kp_detail::kp_pair(sn, i, r, D0[i][r], D1[i][r]);
351 // `pie` is the arrival-instant vector of the NOMINAL pair, and stays
352 // so under a schedule: it seeds a job's service phase, which is a
353 // property of the service process as a whole, and the reference's
354 // `nomPie` is built from `sn_schedule_nominal`'s nominal pair too.
355 pie[i][r] = kp_detail::kp_pie(D0[i][r], D1[i][r]);
356 if (!sn::sn_has_schedule(sn, i, r)) continue;
358 kp_detail::KpSchedule ks;
359 ks.station = i;
360 ks.cls = r;
361 ks.cyclic = sc.cyclic;
362 for (std::size_t a = 0; a < sc.breakpoints.size(); ++a)
363 ks.bp.push_back(num_traits<T>::to_double(sc.breakpoints[a]));
364 for (std::size_t k = 0; k < sc.segD0.size(); ++k) {
365 const std::size_t nph = sc.segD0[k].rows();
366 Matrix<double> A(nph, nph, 0.0), B(nph, nph, 0.0);
367 for (std::size_t a = 0; a < nph; ++a)
368 for (std::size_t b = 0; b < nph; ++b) {
369 A(a, b) = num_traits<T>::to_double(sc.segD0[k](a, b));
370 B(a, b) = num_traits<T>::to_double(sc.segD1[k](a, b));
371 }
372 ks.segD0.push_back(A);
373 ks.segD1.push_back(B);
374 }
375 sched.push_back(ks);
376 }
377
378 // The pairs in force at time t, for every block. Built once per right-hand
379 // side evaluation and handed to `rates`, so the Jacobian's 2*dim difference
380 // calls all see the SAME instant -- differencing across a segment boundary
381 // would report the jump in the schedule as a derivative in q.
382 std::vector<std::vector<Matrix<double>>> Dt0 = D0, Dt1 = D1;
383 const bool time_varying = !sched.empty();
384 const auto pairs_at = [&](double t) {
385 if (!time_varying) return;
386 for (std::size_t e = 0; e < sched.size(); ++e) {
387 const std::size_t i = sched[e].station, r = sched[e].cls;
388 kp_detail::kp_pair_at(sched, D0[i][r], D1[i][r], i, r, t, Dt0[i][r], Dt1[i][r]);
389 }
390 };
391
392 // Routing in stateful space, as `fluid_ode_system` reads it.
393 const std::size_t S = sn.nof_stateful();
394 const bool have_rt = sn.rt.rows() == S * K;
395 std::vector<std::size_t> sf(M, 0);
396 for (std::size_t i = 0; i < M; ++i) sf[i] = sn.stateful_of_station(i + 1) - 1;
397 const auto route = [&](std::size_t i, std::size_t c, std::size_t j, std::size_t l) -> double {
398 if (!have_rt) return 0.0;
399 return num_traits<T>::to_double(sn.rt(sf[i] * K + c, sf[j] * K + l));
400 };
401 // The probability that a completion at (i,r) LEAVES the network: sn.rt is
402 // closed through the Source, so any destination that is not a service block is
403 // an exit.
404 std::vector<std::vector<double>> pout(M, std::vector<double>(K, 0.0));
405 for (std::size_t i = 0; i < M; ++i)
406 for (std::size_t r = 0; r < K; ++r) {
407 if (!is_x[i][r]) continue;
408 double acc = 0.0;
409 for (std::size_t j = 0; j < M; ++j)
410 for (std::size_t l = 0; l < K; ++l)
411 if (!is_x[j][l]) acc += route(i, r, j, l);
412 pout[i][r] = acc;
413 }
414
415 // ---- events ------------------------------------------------------------
416 std::vector<KpEvent> ev;
417 const auto push = [&](KpEvent e) {
418 ev.push_back(e);
419 };
420 for (std::size_t b = 0; b < ublocks.size(); ++b) { // (A0)
421 const KpBlock& u = ublocks[b];
422 for (std::size_t k = 0; k < u.nphases; ++k)
423 for (std::size_t j = 0; j < u.nphases; ++j) {
424 if (k == j) continue;
425 KpEvent e;
427 e.i = u.station;
428 e.c = u.cls;
429 e.k = k;
430 e.j = j;
431 e.off_src = u.offset;
432 e.minus.push_back(u.offset + k);
433 e.plus.push_back(u.offset + j);
434 push(e);
435 }
436 }
437 for (std::size_t b = 0; b < ublocks.size(); ++b) { // (A1)
438 const KpBlock& u = ublocks[b];
439 for (std::size_t d = 0; d < xblocks.size(); ++d) {
440 const KpBlock& xb = xblocks[d];
441 const double p = route(u.station, u.cls, xb.station, xb.cls);
442 if (!(p > 0.0)) continue;
443 for (std::size_t k = 0; k < u.nphases; ++k)
444 for (std::size_t j = 0; j < u.nphases; ++j)
445 for (std::size_t ip = 0; ip < xb.nphases; ++ip) {
446 KpEvent e;
448 e.i = u.station;
449 e.c = u.cls;
450 e.k = k;
451 e.j = j;
452 e.n = xb.station;
453 e.l = xb.cls;
454 e.ip = ip;
455 e.off_src = u.offset;
456 e.off_dst = xb.offset;
457 e.weight = p * (ip < pie[xb.station][xb.cls].size()
458 ? pie[xb.station][xb.cls][ip]
459 : 0.0);
460 e.minus.push_back(u.offset + k);
461 e.plus.push_back(u.offset + j);
462 e.plus.push_back(xb.offset + ip);
463 push(e);
464 }
465 }
466 }
467 for (std::size_t b = 0; b < xblocks.size(); ++b) { // (S)
468 const KpBlock& xb = xblocks[b];
469 for (std::size_t p = 0; p < xb.nphases; ++p)
470 for (std::size_t q = 0; q < xb.nphases; ++q) {
471 if (p == q) continue;
472 KpEvent e;
474 e.i = xb.station;
475 e.c = xb.cls;
476 e.k = p;
477 e.j = q;
478 e.off_src = xb.offset;
479 e.minus.push_back(xb.offset + p);
480 e.plus.push_back(xb.offset + q);
481 push(e);
482 }
483 }
484 for (std::size_t b = 0; b < xblocks.size(); ++b) { // (D) and (R)
485 const KpBlock& xb = xblocks[b];
486 if (pout[xb.station][xb.cls] > 0.0)
487 for (std::size_t p = 0; p < xb.nphases; ++p) {
488 KpEvent e;
490 e.i = xb.station;
491 e.c = xb.cls;
492 e.k = p;
493 e.off_src = xb.offset;
494 e.weight = pout[xb.station][xb.cls];
495 e.minus.push_back(xb.offset + p);
496 push(e);
497 }
498 for (std::size_t d = 0; d < xblocks.size(); ++d) {
499 const KpBlock& nb = xblocks[d];
500 const double p = route(xb.station, xb.cls, nb.station, nb.cls);
501 if (!(p > 0.0)) continue;
502 for (std::size_t q = 0; q < xb.nphases; ++q)
503 for (std::size_t ip = 0; ip < nb.nphases; ++ip) {
504 KpEvent e;
506 e.i = xb.station;
507 e.c = xb.cls;
508 e.k = q;
509 e.n = nb.station;
510 e.l = nb.cls;
511 e.ip = ip;
512 e.off_src = xb.offset;
513 e.off_dst = nb.offset;
514 e.weight =
515 p * (ip < pie[nb.station][nb.cls].size() ? pie[nb.station][nb.cls][ip] : 0.0);
516 e.minus.push_back(xb.offset + q);
517 e.plus.push_back(nb.offset + ip);
518 push(e);
519 }
520 }
521 }
522 const std::size_t nev = ev.size();
523
524 // The server-capacity factor min(n,c)/n, shared by every class at a station.
525 const auto capacity = [&](const double* q, std::size_t i) -> double {
526 if (sn.stations[i].sched == lang::SchedStrategy::INF ||
527 !std::isfinite(sn.stations[i].nservers))
528 return 1.0;
529 double ni = 0.0;
530 for (std::size_t b = 0; b < xblocks.size(); ++b) {
531 if (xblocks[b].station != i) continue;
532 for (std::size_t p = 0; p < xblocks[b].nphases; ++p)
533 ni += std::max(q[xblocks[b].offset + p], 0.0);
534 }
535 const double c = sn.stations[i].nservers;
536 return (ni <= c) ? 1.0 : c / ni;
537 };
538
539 const auto rates = [&](const double* q, std::vector<double>& f) {
540 f.assign(nev, 0.0);
541 std::vector<double> cap(M, 1.0);
542 for (std::size_t i = 0; i < M; ++i) cap[i] = capacity(q, i);
543 for (std::size_t e = 0; e < nev; ++e) {
544 const KpEvent& s = ev[e];
545 const double mass = std::max(q[s.off_src + s.k], 0.0);
546 switch (s.kind) {
548 f[e] = Dt0[s.i][s.c](s.k, s.j) * mass;
549 break;
551 f[e] = Dt1[s.i][s.c](s.k, s.j) * s.weight * mass;
552 break;
554 f[e] = Dt0[s.i][s.c](s.k, s.j) * mass * cap[s.i];
555 break;
557 case KpEventKind::Routed: {
558 double rowsum = 0.0;
559 for (std::size_t b = 0; b < Dt1[s.i][s.c].cols(); ++b)
560 rowsum += Dt1[s.i][s.c](s.k, b);
561 f[e] = rowsum * s.weight * mass * cap[s.i];
562 break;
563 }
564 }
565 }
566 };
567 const auto apply_jumps = [&](const std::vector<double>& f, double* dq) {
568 for (std::size_t a = 0; a < dim; ++a) dq[a] = 0.0;
569 for (std::size_t e = 0; e < nev; ++e) {
570 if (f[e] == 0.0) continue;
571 for (std::size_t a = 0; a < ev[e].minus.size(); ++a) dq[ev[e].minus[a]] -= f[e];
572 for (std::size_t a = 0; a < ev[e].plus.size(); ++a) dq[ev[e].plus[a]] += f[e];
573 }
574 };
575
576 // ---- horizon -----------------------------------------------------------
577 double t0 = 0.0;
578 double tend = opt.timespan_end;
579 const bool unbounded = !std::isfinite(tend);
580 // The longest cycle among the CYCLIC schedules; zero when none is cyclic,
581 // and a non-cyclic schedule is constant on its last segment, so it has a
582 // fixed point and needs neither the extended horizon nor the averaging.
583 double period = 0.0;
584 for (std::size_t e = 0; e < sched.size(); ++e)
585 if (sched[e].cyclic) period = std::max(period, sched[e].bp.back() - sched[e].bp.front());
586 if (unbounded) {
587 double slow = std::numeric_limits<double>::infinity();
588 for (std::size_t i = 0; i < M; ++i)
589 for (std::size_t r = 0; r < K; ++r) {
590 const double rate = num_traits<T>::to_double(sn.rates(i, r));
591 if (std::isfinite(rate) && rate > 0.0) slow = std::min(slow, rate);
592 }
593 if (!std::isfinite(slow)) slow = 1.0;
594 tend = t0 + std::max(10.0, 30.0 / slow);
595 if (period > 0.0) tend = std::max(tend, t0 + 10.0 * period);
596 }
597
598 // ---- initial condition -------------------------------------------------
599 std::vector<double> z(dim + dim * dim, 0.0);
600 // The arrival phase is drawn from the stationary vector of the pair IN FORCE
601 // AT t0, not of the time average: a schedule that starts in a quiet segment
602 // starts in that segment's phase mix.
603 pairs_at(t0);
604 for (std::size_t b = 0; b < ublocks.size(); ++b) {
605 const KpBlock& u = ublocks[b];
606 const std::vector<double> theta =
607 kp_detail::kp_stationary(Dt0[u.station][u.cls], Dt1[u.station][u.cls]);
608 for (std::size_t a = 0; a < u.nphases; ++a) z[u.offset + a] = theta[a];
609 for (std::size_t a = 0; a < u.nphases; ++a)
610 for (std::size_t c2 = 0; c2 < u.nphases; ++c2)
611 z[dim + (u.offset + a) * dim + (u.offset + c2)] =
612 (a == c2 ? theta[a] : 0.0) - theta[a] * theta[c2];
613 }
614 // NOT `opt.init_sol`, which is laid out for the CLOSING state vector: the two
615 // can have the same length on the same model, so reading it here would let a
616 // closing-layout seed zero the source phase mass and with it the network.
617 //
618 // A WRONG-SIZED SEED IS REFUSED, not ignored. Dropping it would integrate from
619 // the default initial condition under the caller's name and return a plausible
620 // trajectory for a model the caller did not ask about.
621 if (!opt.kp_init_sol.empty()) {
622 if (opt.kp_init_sol.size() != dim)
623 throw InputError("solver_fluid_kp: config.kp_init_sol has " +
624 std::to_string(opt.kp_init_sol.size()) +
625 " entries but the 'kp' state vector of this model has " +
626 std::to_string(dim) +
627 ", laid out station-major over the (station, class) blocks. "
628 "It is NOT laid out like init_sol.");
629 for (std::size_t a = 0; a < dim; ++a) z[a] = opt.kp_init_sol[a];
630 }
631 // Companion seed for the covariance. A caller that carries a DISTRIBUTION across
632 // a handoff supplies the second moment beside the mean, so the next stage does
633 // not restart from a point mass it never had. Same layout as kp_init_sol.
634 if (opt.init_cov.rows() > 0 || opt.init_cov.cols() > 0) {
635 if (opt.init_cov.rows() != dim || opt.init_cov.cols() != dim)
636 throw InputError("solver_fluid_kp: config.init_cov is " +
637 std::to_string(opt.init_cov.rows()) + "x" +
638 std::to_string(opt.init_cov.cols()) +
639 " but the 'kp' state vector of this model has " +
640 std::to_string(dim) + " entries, so the covariance must be " +
641 std::to_string(dim) + "x" + std::to_string(dim) + ".");
642 double asym = 0.0, scale = 0.0;
643 for (std::size_t a = 0; a < dim; ++a)
644 for (std::size_t c2 = 0; c2 < dim; ++c2) {
645 const double d = opt.init_cov(a, c2) - opt.init_cov(c2, a);
646 asym += d * d;
647 scale += opt.init_cov(a, c2) * opt.init_cov(a, c2);
648 }
649 // Loose enough for the rounding of a covariance that was itself integrated,
650 // tight enough to catch a matrix that is simply not one.
651 if (std::sqrt(asym) > 1e-6 * std::max(1.0, std::sqrt(scale)))
652 throw InputError("solver_fluid_kp: config.init_cov must be symmetric.");
653 for (std::size_t a = 0; a < dim; ++a)
654 for (std::size_t c2 = 0; c2 < dim; ++c2)
655 z[dim + a * dim + c2] = opt.init_cov(a, c2);
656 }
657
658 // ---- integrate ---------------------------------------------------------
659 // The Jacobian is taken by central differences ON THE ASSEMBLED RATES, so
660 // every capacity term is differentiated consistently with the drift actually
661 // integrated rather than with an algebraic derivative of a different function.
662 const LsodaRhs rhs = [&](double t, const double* zz, double* dz) {
663 pairs_at(t);
664 std::vector<double> f;
665 rates(zz, f);
666 apply_jumps(f, dz);
667 double qmax = 1.0;
668 for (std::size_t a = 0; a < dim; ++a) qmax = std::max(qmax, std::fabs(zz[a]));
669 const double hstep = 1e-6 * qmax;
670 Matrix<double> J(dim, dim, 0.0);
671 std::vector<double> qp(zz, zz + dim), fp, fm, dp(dim, 0.0), dm(dim, 0.0);
672 for (std::size_t m = 0; m < dim; ++m) {
673 const double keep = qp[m];
674 qp[m] = keep + hstep;
675 rates(qp.data(), fp);
676 apply_jumps(fp, dp.data());
677 qp[m] = keep - hstep;
678 rates(qp.data(), fm);
679 apply_jumps(fm, dm.data());
680 qp[m] = keep;
681 for (std::size_t a = 0; a < dim; ++a) J(a, m) = (dp[a] - dm[a]) / (2.0 * hstep);
682 }
683 // G = A diag(f) A', assembled from the jump lists.
684 Matrix<double> G(dim, dim, 0.0);
685 std::vector<double> col(dim, 0.0);
686 for (std::size_t e = 0; e < nev; ++e) {
687 if (f[e] == 0.0) continue;
688 std::fill(col.begin(), col.end(), 0.0);
689 for (std::size_t a = 0; a < ev[e].minus.size(); ++a) col[ev[e].minus[a]] -= 1.0;
690 for (std::size_t a = 0; a < ev[e].plus.size(); ++a) col[ev[e].plus[a]] += 1.0;
691 for (std::size_t a = 0; a < dim; ++a) {
692 if (col[a] == 0.0) continue;
693 for (std::size_t b = 0; b < dim; ++b)
694 if (col[b] != 0.0) G(a, b) += col[a] * f[e] * col[b];
695 }
696 }
697 for (std::size_t a = 0; a < dim; ++a)
698 for (std::size_t b = 0; b < dim; ++b) {
699 double acc = G(a, b);
700 for (std::size_t c2 = 0; c2 < dim; ++c2)
701 acc += J(a, c2) * zz[dim + c2 * dim + b] + zz[dim + a * dim + c2] * J(b, c2);
702 dz[dim + a * dim + b] = acc;
703 }
704 };
705
706 LsodaOptions lopt;
707 lopt.rtol = opt.tol;
708 lopt.atol = opt.tol * 1e-3;
709 lopt.h_max = (tend - t0) / 10.0;
710 // A step that crosses a whole segment integrates a rate that was never in
711 // force. Cap it at a quarter of the NARROWEST segment of any schedule.
712 if (period > 0.0) {
713 double narrowest = std::numeric_limits<double>::infinity();
714 for (std::size_t e = 0; e < sched.size(); ++e)
715 for (std::size_t k = 1; k < sched[e].bp.size(); ++k)
716 narrowest = std::min(narrowest, sched[e].bp[k] - sched[e].bp[k - 1]);
717 if (std::isfinite(narrowest) && narrowest > 0.0)
718 lopt.h_max = std::min(lopt.h_max, narrowest / 4.0);
719 }
720
721 // The output grid. Uniform over the whole horizon as before; when the answer
722 // is a period average the last cycle is refined and every segment boundary
723 // in it is BRACKETED, so no trapezoid interval straddles a jump.
724 const bool averaging = unbounded && period > 0.0;
725 const double w0 = averaging ? std::max(t0, tend - period) : t0;
726 std::vector<double> grid;
727 {
728 const std::size_t ngrid = 201;
729 for (std::size_t a = 0; a < ngrid; ++a)
730 grid.push_back(t0 + (tend - t0) * static_cast<double>(a) /
731 static_cast<double>(ngrid - 1));
732 if (averaging) {
733 const std::size_t nref = 2001;
734 for (std::size_t a = 0; a < nref; ++a)
735 grid.push_back(w0 + (tend - w0) * static_cast<double>(a) /
736 static_cast<double>(nref - 1));
737 std::vector<double> bounds;
738 for (std::size_t e = 0; e < sched.size(); ++e) {
739 const std::vector<double>& bp = sched[e].bp;
740 const double per = bp.back() - bp.front();
741 if (sched[e].cyclic && per > 0.0) {
742 const long kmax = static_cast<long>(std::ceil((tend - w0) / per)) + 2;
743 for (long kk = -1; kk <= kmax; ++kk)
744 for (std::size_t a = 0; a < bp.size(); ++a)
745 bounds.push_back(bp[a] + static_cast<double>(kk) * per);
746 } else {
747 for (std::size_t a = 0; a < bp.size(); ++a) bounds.push_back(bp[a]);
748 }
749 }
750 const double eps_b = std::max(1e-9, 1e-7 * (tend - w0));
751 for (std::size_t a = 0; a < bounds.size(); ++a) {
752 const double b = bounds[a];
753 if (!(b > w0 && b < tend)) continue;
754 grid.push_back(b - eps_b);
755 grid.push_back(b);
756 grid.push_back(b + eps_b);
757 }
758 }
759 std::sort(grid.begin(), grid.end());
760 grid.erase(std::remove_if(grid.begin(), grid.end(),
761 [&](double v) { return v < t0 || v > tend; }),
762 grid.end());
763 grid.erase(std::unique(grid.begin(), grid.end()), grid.end());
764 if (grid.empty() || grid.front() > t0) grid.insert(grid.begin(), t0);
765 }
766 const LsodaSolution sol = fluid_integrate_grid(rhs, z, grid, lopt);
767
768 // ---- metrics -----------------------------------------------------------
769 // A time-homogeneous model has a fixed point, so the steady-state answer is
770 // the value at the horizon. A CYCLIC schedule has none, so the answer is the
771 // trapezoidal average over the last full period; the value at the horizon
772 // would be an arbitrary point of the cycle, at which the source and station
773 // throughputs do not even agree.
774 const std::vector<double>& zend = sol.final_state();
775 FluidSolution out;
776 out.method = "kp";
777 out.iters = 1;
778 out.QN = Matrix<double>(M, K, 0.0);
779 out.UN = Matrix<double>(M, K, 0.0);
780 out.RN = Matrix<double>(M, K, 0.0);
781 out.TN = Matrix<double>(M, K, 0.0);
782 out.xvec.assign(zend.begin(), zend.begin() + dim);
783
784 const std::size_t nt = sol.t.size();
785 Matrix<double> QVar(M, K, 0.0);
786 for (std::size_t b = 0; b < xblocks.size(); ++b) {
787 const KpBlock& xb = xblocks[b];
788 std::vector<double> qser(nt, 0.0), user(nt, 0.0), tser(nt, 0.0);
789 double vend = 0.0;
790 for (std::size_t n = 0; n < nt; ++n) {
791 const std::vector<double>& zs = sol.y[n];
792 double q = 0.0;
793 for (std::size_t p = 0; p < xb.nphases; ++p) q += zs[xb.offset + p];
794 pairs_at(sol.t[n]);
795 const double cap = capacity(zs.data(), xb.station);
796 double tn = 0.0;
797 for (std::size_t p = 0; p < xb.nphases; ++p) {
798 double rowsum = 0.0;
799 for (std::size_t c2 = 0; c2 < Dt1[xb.station][xb.cls].cols(); ++c2)
800 rowsum += Dt1[xb.station][xb.cls](p, c2);
801 tn += rowsum * std::max(zs[xb.offset + p], 0.0) * cap;
802 }
803 const double c = sn.stations[xb.station].nservers;
804 qser[n] = q;
805 tser[n] = tn;
806 user[n] = (sn.stations[xb.station].sched == lang::SchedStrategy::INF ||
807 !std::isfinite(c))
808 ? q
809 : std::min(q, c) / c;
810 }
811 for (std::size_t p = 0; p < xb.nphases; ++p)
812 for (std::size_t p2 = 0; p2 < xb.nphases; ++p2)
813 vend += zend[dim + (xb.offset + p) * dim + (xb.offset + p2)];
814 out.QN(xb.station, xb.cls) = kp_detail::kp_summarise(qser, sol.t, w0, tend, averaging);
815 out.UN(xb.station, xb.cls) = kp_detail::kp_summarise(user, sol.t, w0, tend, averaging);
816 out.TN(xb.station, xb.cls) = kp_detail::kp_summarise(tser, sol.t, w0, tend, averaging);
817 QVar(xb.station, xb.cls) = vend;
818 // TN is zero only to the integrator's accuracy: a class that never visits leaves
819 // a ~1e-20 residue in TN too, and a strict > 0 test then divides residue by residue.
820 if (out.TN(xb.station, xb.cls) > lang::GlobalConstants::Zero)
821 out.RN(xb.station, xb.cls) = out.QN(xb.station, xb.cls) / out.TN(xb.station, xb.cls);
822 }
823 for (std::size_t b = 0; b < ublocks.size(); ++b) {
824 const KpBlock& u = ublocks[b];
825 std::vector<double> aser(nt, 0.0);
826 for (std::size_t n = 0; n < nt; ++n) {
827 pairs_at(sol.t[n]);
828 double tn = 0.0;
829 for (std::size_t p = 0; p < u.nphases; ++p) {
830 double rowsum = 0.0;
831 for (std::size_t c2 = 0; c2 < Dt1[u.station][u.cls].cols(); ++c2)
832 rowsum += Dt1[u.station][u.cls](p, c2);
833 tn += rowsum * std::max(sol.y[n][u.offset + p], 0.0);
834 }
835 aser[n] = tn;
836 }
837 out.TN(u.station, u.cls) = kp_detail::kp_summarise(aser, sol.t, w0, tend, averaging);
838 }
839
840 // The covariance IS the answer here, so it is reported through the same
841 // `moments` channel the stationary closures use; `Sigma` is the full state
842 // covariance at the horizon, so cross-station terms survive.
844 rep.Sigma = Matrix<double>(dim, dim, 0.0);
845 for (std::size_t a = 0; a < dim; ++a)
846 for (std::size_t b = 0; b < dim; ++b) rep.Sigma(a, b) = zend[dim + a * dim + b];
847 rep.QVar = QVar;
848 rep.QStd = Matrix<double>(M, K, 0.0);
849 for (std::size_t i = 0; i < M; ++i)
850 for (std::size_t r = 0; r < K; ++r)
851 rep.QStd(i, r) = std::sqrt(std::max(0.0, QVar(i, r)));
852 rep.outer_iters = 1;
853 out.has_moments = true;
854 out.moments = rep;
855
856 out.XN.assign(K, 0.0);
857 out.CN.assign(K, 0.0);
858 for (std::size_t r = 0; r < K; ++r) {
859 const std::size_t rs = sn.classes[r].refstat;
860 if (rs >= 1 && rs <= M) out.XN[r] = out.TN(rs - 1, r);
861 double q = 0.0;
862 for (std::size_t i = 0; i < M; ++i) q += out.QN(i, r);
863 if (out.XN[r] > 0.0) out.CN[r] = q / out.XN[r];
864 }
865
866 if (tran != nullptr) {
867 tran->t = sol.t;
868 tran->q.clear();
869 tran->QVar.clear();
870 tran->Sigma.clear();
871 for (std::size_t s = 0; s < sol.y.size(); ++s) {
872 const std::vector<double>& zs = sol.y[s];
873 tran->q.push_back(std::vector<double>(zs.begin(), zs.begin() + dim));
874 Matrix<double> V(M, K, 0.0), Sg(dim, dim, 0.0);
875 for (std::size_t a = 0; a < dim; ++a)
876 for (std::size_t b = 0; b < dim; ++b) Sg(a, b) = zs[dim + a * dim + b];
877 for (std::size_t b = 0; b < xblocks.size(); ++b) {
878 const KpBlock& xb = xblocks[b];
879 double v = 0.0;
880 for (std::size_t p = 0; p < xb.nphases; ++p)
881 for (std::size_t p2 = 0; p2 < xb.nphases; ++p2)
882 v += Sg(xb.offset + p, xb.offset + p2);
883 V(xb.station, xb.cls) = v;
884 }
885 tran->QVar.push_back(V);
886 tran->Sigma.push_back(Sg);
887 }
888 }
889 return out;
890}
891
892/** Port of `solver_fluid_kp.m`: the steady table at the horizon. */
893template <class T>
897
898/**
899 * Port of `@@SolverFLD/getTranAvgVar`: the queue-length VARIANCE along the
900 * trajectory, per station and class, plus the full state covariance.
901 *
902 * ONLY `kp` HAS THIS. Every other fluid method integrates the mean alone and
903 * carries no second moment, so asking them for one is an error rather than a
904 * misleading zero -- and `minnormal`'s covariance is STATIONARY, so it is not this
905 * quantity either. A caller that left the horizon unbounded gets one resolved the
906 * way `getTranAvg` resolves it, from the slowest rate in the model.
907 */
908template <class T>
910 const FluidOptions& opt) {
911 std::string m = opt.method;
912 if (m.size() > 6 && m.compare(0, 6, "fluid.") == 0) m = m.substr(6);
913 if (m != "kp")
914 throw UnsupportedError(
915 "solver_fluid_tran_avg_var: getTranAvgVar needs method 'kp'; the other fluid methods "
916 "integrate the mean only and carry no second moment");
917 FluidKpTransient tran;
918 FluidOptions o = opt;
919 o.method = "kp";
920 solver_fluid_kp_core(sn, o, &tran);
921 return tran;
922}
923
924} // namespace fluid
925} // namespace line
926
927#endif // LINE_SOLVERS_FLUID_FLUID_KP_H
InputError(const std::string &what)
Definition error.h:39
std::size_t rows() const
Definition matrix.h:89
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
The exception types the port throws.
Port of ode_eliminate_immediate.m, eliminate_immediate_matrix.m and ode_solve_stiff....
LSODA: the LINE-facing wrapper over the vendored solver in third_party/lsoda.hpp.
Markovian arrival process descriptors: stationary vectors, rate, moments, autocorrelation and the ind...
Dense matrix and non-owning view.
FluidKpTransient solver_fluid_tran_avg_var(const qn::NetworkStruct< T > &sn, const FluidOptions &opt)
Port of @@SolverFLD/getTranAvgVar: the queue-length VARIANCE along the trajectory,...
Definition fluid_kp.h:909
FluidSolution solver_fluid_kp_core(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, FluidKpTransient *tran)
The Ko-Pender solve, returning both the steady table and the covariance trajectory so that neither ha...
Definition fluid_kp.h:274
FluidSolution solver_fluid_kp(const qn::NetworkStruct< T > &sn, const FluidOptions &opt)
Port of solver_fluid_kp.m: the steady table at the horizon.
Definition fluid_kp.h:894
KpEventKind
The five event families of Ko-Pender (3.1)-(3.2).
Definition fluid_kp.h:98
@ Departure
D: completion leaving the network.
Definition fluid_kp.h:102
@ Routed
R: completion routed onward.
Definition fluid_kp.h:103
@ Arrival
A1: phase change WITH an arrival, into a service phase.
Definition fluid_kp.h:100
@ ArrivalPhase
A0: arrival-MAP phase change without an arrival.
Definition fluid_kp.h:99
@ ServicePhase
S: service phase change inside a station.
Definition fluid_kp.h:101
LsodaSolution fluid_integrate_grid(const std::function< void(double, const double *, double *)> &f, const std::vector< double > &y0, const std::vector< double > &grid, const LsodaOptions &lopt)
The same retry over a whole output grid, for the callers that ask LSODA for a trajectory rather than ...
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
ScheduleNominal< T > sn_schedule_nominal(const qn::NetworkStruct< T > &sn, std::size_t ist, std::size_t r)
Port of sn_schedule_nominal(sn, ist, r); ist and r are 0-based here.
bool sn_has_schedule(const qn::NetworkStruct< T > &sn, std::size_t ist, std::size_t r)
True when (ist, r) carries a MAPt / PHt / NHPP schedule.
void lu_solve(const Matrix< T > &LU, const std::vector< std::size_t > &piv, std::vector< T > &b)
Solve LUx = Pb in place on b, using the factors from lu_factor.
Definition lu.h:94
std::function< void(double t, const double *y, double *dydt)> LsodaRhs
The right-hand side dy/dt = f(t, y).
Definition lsoda.h:54
std::vector< std::size_t > lu_factor(Matrix< T > &A)
In-place LU of A (n x n).
Definition lu.h:48
A queueing network and its refreshed NetworkStruct.
Port of matlab/src/api/sn/sn_schedule_nominal.m: unpack the MAPt or PHt slot of sn....
SolverFluid: the closing method, a port of solver_fluid.m, solver_fluid_iteration....
Integration controls.
Definition lsoda.h:64
double atol
absolute tolerance, applied to every component
Definition lsoda.h:66
double rtol
relative tolerance, applied to every component
Definition lsoda.h:65
double h_max
largest admissible step; 0 means no bound
Definition lsoda.h:78
Result of an integration, mirroring OdeSolution in ode.h.
Definition lsoda.h:126
const std::vector< double > & final_state() const
Definition lsoda.h:137
std::vector< std::vector< double > > y
y[i] is the state at t[i]
Definition lsoda.h:128
std::vector< double > t
output times, t[0] = t_eval[0]
Definition lsoda.h:127
The transient the covariance equation produces, i.e.
Definition fluid_kp.h:120
std::vector< Matrix< double > > QVar
per time point, (nstations x nclasses)
Definition fluid_kp.h:122
std::vector< Matrix< double > > Sigma
per time point, (dim x dim)
Definition fluid_kp.h:123
std::vector< double > t
Definition fluid_kp.h:121
std::vector< std::vector< double > > q
Definition fluid_kp.h:124
The second-order results of the moment-closure methods, i.e.
Matrix< double > Sigma
state-level covariance, on range(D)
Matrix< double > QStd
per station and class queue-length variance
Controls, defaulting to SolverOptions('Fluid') in the reference.
What the analyzer returns, in the same shape as the MVA solver's result.
bool has_moments
result.solverSpecific.moments: set only by minnormal and refined.
std::vector< double > XN
FluidMomentReport moments
std::vector< double > xvec
the converged fluid state
std::vector< double > CN
One (station, class) block of the Ko-Pender state vector.
Definition fluid_kp.h:90
std::size_t cls
Definition fluid_kp.h:92
std::size_t offset
Definition fluid_kp.h:93
std::size_t nphases
Definition fluid_kp.h:94
std::size_t station
Definition fluid_kp.h:91
double weight
routing probability times entry-phase probability
Definition fluid_kp.h:114
std::size_t c
source station and class
Definition fluid_kp.h:108
std::size_t ip
destination entry phase
Definition fluid_kp.h:111
KpEventKind kind
Definition fluid_kp.h:107
std::size_t l
destination station and class
Definition fluid_kp.h:110
std::size_t j
source and target phase of the modulating chain
Definition fluid_kp.h:109
std::size_t off_dst
offset of the destination block
Definition fluid_kp.h:113
std::vector< std::size_t > minus
The jump: -1 at minus, +1 at each plus; a phase change carries both.
Definition fluid_kp.h:116
std::vector< std::size_t > plus
Definition fluid_kp.h:116
std::size_t off_src
offset of the source block
Definition fluid_kp.h:112
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:759
static constexpr double Zero
Definition lang_types.h:670
What sn_schedule_nominal returns, in the reference's own output order.
std::vector< Matrix< T > > segD1
per-segment D1
std::vector< T > breakpoints
the boundary vector, nseg + 1 long
std::vector< Matrix< T > > segD0
per-segment D0, already in MAP form