LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fluid_aoi.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_AOI_H
6#define LINE_SOLVERS_FLUID_FLUID_AOI_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Age of Information by Markovian fluid queues: a port of `solver_mfq_aoi.m`
12 * (identical to `solver_fluid_aoi.m`) with the gate `aoi_is_aoi.m`, the
13 * parameter map `aoi_extract_params.m`, and the two aoi-fluid algorithms
14 * `solveBufferless.m` and `solveSingleBuffer.m`.
15 *
16 * WHAT AGE OF INFORMATION IS. Not a delay of a job but the freshness of the
17 * information a monitor holds: at time t, the age is t minus the generation
18 * time of the most recent update DELIVERED so far. It grows at unit rate and
19 * drops on every delivery, so its mean depends on the whole delivery process
20 * and not only on the response time -- a queue that delivers late but often can
21 * beat one that delivers fast and rarely. Peak AoI (PAoI) is the value reached
22 * just before a drop.
23 *
24 * WHY A FLUID QUEUE COMPUTES IT. The age is a sawtooth: it climbs with slope +1
25 * and resets. That is exactly the sample path of a Markov-modulated fluid queue
26 * with drift +1 in every state but one, so the stationary age distribution is
27 * the stationary distribution of an MFQ level, and it comes out as a
28 * MATRIX-EXPONENTIAL triple (g, A, h): P(AoI > t) = g exp(A t) h. Both
29 * algorithms build the modulating chain of the age process, then solve one
30 * linear system for g and read the moments off A. The reference for the pair is
31 * the aoi-fluid toolbox of Dogan, Akar and Atay (BSD 2-Clause, 2020).
32 *
33 * THE TWO SYSTEMS COVERED, and no others: a BUFFERLESS PH/PH/1/1 where an
34 * arrival meeting a busy server is discarded (p = 0) or preempts it (p = 1),
35 * and a SINGLE-BUFFER M/PH/1/2 where a waiting update is kept (r = 0) or
36 * REPLACED by a fresher one (r = 1). Anything else -- more capacity, more
37 * servers, more classes, a second queue -- is refused by name.
38 *
39 * WHERE p AND r COME FROM. `aoi_extract_params.m` reads the scheduling policy:
40 * FCFS gives no preemption and no replacement, LCFS-PR preempts, and LCFS
41 * replaces in the buffered system while behaving non-preemptively in the
42 * bufferless one. `FluidOptions::aoi_preemption` overrides both, as
43 * `options.config.aoi_preemption` does in the reference.
44 *
45 * A CAVEAT THE REFERENCE CARRIES AND THIS PORT KEEPS. `aoi_dist2ph` builds the
46 * PH pair from the (D0, D1) MAP with alpha proportional to theta .* (D1 e),
47 * which is the phase distribution AT A COMPLETION rather than at a start. For a
48 * multi-phase Erlang the two differ, so the mean service time the AoI branch
49 * uses is not the distribution's mean (Erlang(2) with mean 1 is read as mean
50 * 0.5). The standard metrics reported alongside AoI inherit that reading. This
51 * is reproduced exactly, because MATLAB is the reference and the AoI numbers
52 * would otherwise not match; it is called out here so it is never mistaken for
53 * a defect of this port.
54 *
55 * THE STANDARD METRICS ARE AN M/M/1 APPROXIMATION, as the reference states in
56 * so many words: QN = rho/(1 - rho) and RN = 1/(mu - lambda) with mu the
57 * reciprocal of the mean service time above. They are NOT the metrics of the
58 * finite-capacity system being analyzed, which by construction holds at most
59 * one or two jobs; the AoI numbers are the output that means something here.
60 */
61
62#include <algorithm>
63#include <cmath>
64#include <cstddef>
65#include <limits>
66#include <string>
67#include <vector>
68
72#include "line/util/eig.h"
73#include "line/util/error.h"
74#include "line/util/expm.h"
75#include "line/util/linalg.h"
76#include "line/util/lstsq.h"
77#include "line/util/lu.h"
78#include "line/util/matrix.h"
79
80namespace line {
81namespace fluid {
82
83/** What the AoI gate found, when it matches. */
85 bool ok = false;
86 std::string error;
87 std::size_t source = 0; ///< 0-based station index
88 std::size_t queue = 0;
89 std::size_t cls = 0; ///< the single open class
90 double capacity = 0.0; ///< 1 = bufferless, 2 = single buffer
92};
93
94/**
95 * Port of `aoi_is_aoi.m`.
96 *
97 * Returns the reason rather than throwing, because the reference uses this as
98 * a DISPATCH test inside the `mfq` method: a model that fails it is not an
99 * error, it is a model for the ordinary single-queue fluid branch.
100 */
101template <class T>
103 AoiTopology t;
104 const std::size_t M = sn.nstations, K = sn.nclasses;
105 std::vector<std::size_t> open_cls;
106 for (std::size_t r = 0; r < K; ++r)
107 if (!std::isfinite(sn.classes[r].population)) open_cls.push_back(r);
108 if (open_cls.empty()) {
109 t.error = "Not an open model - all classes are closed";
110 return t;
111 }
112 if (open_cls.size() > 1) {
113 t.error = "Multiple open classes found - AoI analysis requires single class";
114 return t;
115 }
116 t.cls = open_cls[0];
117
118 std::size_t nsrc = 0, nq = 0, nsink = 0;
119 for (const qn::NodeDef& nd : sn.nodes) {
120 if (nd.nodetype == qn::NodeType::Source) ++nsrc;
121 if (nd.nodetype == qn::NodeType::Sink) ++nsink;
122 if (nd.nodetype == qn::NodeType::Queue) ++nq;
123 }
124 if (nsrc != 1) {
125 t.error = nsrc == 0 ? "No source node found" : "Multiple source nodes found";
126 return t;
127 }
128 if (nsink != 1) {
129 t.error = nsink == 0 ? "No sink node found" : "Multiple sink nodes found";
130 return t;
131 }
132 if (nq != 1) {
133 t.error = nq == 0 ? "No queue node found"
134 : "Multiple queue nodes found - AoI analysis supports single queue only";
135 return t;
136 }
137 bool has_src = false, has_q = false;
138 for (std::size_t i = 0; i < M; ++i) {
139 if (sn.stations[i].nodetype == qn::NodeType::Source) {
140 t.source = i;
141 has_src = true;
142 } else if (sn.stations[i].nodetype == qn::NodeType::Queue) {
143 t.queue = i;
144 has_q = true;
145 } else if (sn.stations[i].nodetype != qn::NodeType::Sink) {
146 t.error = "AoI analysis supports Source -> Queue -> Sink only";
147 return t;
148 }
149 }
150 if (!has_src || !has_q) {
151 t.error = "The Source and the Queue must both be stations";
152 return t;
153 }
154
155 if (sn.stations[t.queue].nservers != 1.0) {
156 t.error = "Queue is not single-server - AoI analysis requires c = 1";
157 return t;
158 }
159 const double cap = (t.queue < sn.cap.size()) ? sn.cap[t.queue]
160 : std::numeric_limits<double>::infinity();
161 if (!std::isfinite(cap) || cap > 2.0 || cap < 1.0) {
162 t.error =
163 "AoI analysis requires queue capacity 1 (bufferless) or 2 (single-buffer)";
164 return t;
165 }
166 t.capacity = cap;
167
168 t.sched = sn.stations[t.queue].sched;
171 t.error = "AoI analysis supports FCFS, LCFS or LCFSPR scheduling only";
172 return t;
173 }
174
175 // A single buffer is analyzed under Poisson arrivals only.
176 if (cap == 2.0 && sn.service[t.source][t.cls].D0.rows() > 1) {
177 t.error = "Single-buffer (capacity 2) requires exponential arrivals";
178 return t;
179 }
180
181 const std::size_t rt_idx = t.queue * K + t.cls;
182 if (rt_idx < sn.rt.rows() && num_traits<T>::to_double(sn.rt(rt_idx, rt_idx)) > 0.0) {
183 t.error = "Self-loop detected at queue - violates AoI model assumptions";
184 return t;
185 }
186 t.ok = true;
187 return t;
188}
189
190/** The (tau, T) / (sigma, S) pairs and the preemption probability. */
191struct AoiParams {
193 std::vector<double> tau, sigma;
194 double p = 0.0; ///< preemption (bufferless) or replacement (single buffer)
195 double lambda = 0.0; ///< arrival rate, the single-buffer input
196};
197
198/**
199 * Port of `aoi_extract_params.m`.
200 *
201 * @param preempt_override `options.config.aoi_preemption`; negative selects the
202 * policy-driven default
203 * @param sn the refreshed network struct
204 * @param top the detected age-of-information topology
205 */
206template <class T>
208 double preempt_override) {
209 AoiParams par;
210 const lang::Distrib<T>& svc = sn.service[top.queue][top.cls];
211 const std::size_t ls = svc.D0.rows();
212 if (ls == 0) throw InputError("aoi_extract_params: the queue has no service process");
213 {
214 Matrix<double> D0(ls, ls, 0.0), D1(ls, ls, 0.0);
215 for (std::size_t a = 0; a < ls; ++a)
216 for (std::size_t b = 0; b < ls; ++b) {
217 D0(a, b) = num_traits<T>::to_double(svc.D0(a, b));
218 D1(a, b) = num_traits<T>::to_double(svc.D1(a, b));
219 }
220 const aoi::AoiPh<double> ph = aoi::aoi_dist2ph(D0, D1);
221 par.sigma = ph.alpha;
222 par.Ssvc = ph.Tmat;
223 }
224
225 par.lambda = num_traits<T>::to_double(sn.rates(top.source, top.cls));
226 if (top.capacity == 1.0) {
227 const lang::Distrib<T>& arr = sn.service[top.source][top.cls];
228 const std::size_t la = arr.D0.rows();
229 if (la == 0) throw InputError("aoi_extract_params: the source has no arrival process");
230 Matrix<double> D0(la, la, 0.0), D1(la, la, 0.0);
231 for (std::size_t a = 0; a < la; ++a)
232 for (std::size_t b = 0; b < la; ++b) {
233 D0(a, b) = num_traits<T>::to_double(arr.D0(a, b));
234 D1(a, b) = num_traits<T>::to_double(arr.D1(a, b));
235 }
236 const aoi::AoiPh<double> ph = aoi::aoi_dist2ph(D0, D1);
237 par.tau = ph.alpha;
238 par.Tarr = ph.Tmat;
239 }
240
241 if (preempt_override >= 0.0) {
242 par.p = preempt_override;
243 } else if (top.capacity == 1.0) {
244 // Bufferless: only LCFS-PR preempts the update in service.
245 par.p = (top.sched == lang::SchedStrategy::LCFSPR) ? 1.0 : 0.0;
246 } else {
247 // Single buffer: both LCFS variants replace the waiting update.
248 par.p = (top.sched == lang::SchedStrategy::FCFS) ? 0.0 : 1.0;
249 }
250 return par;
251}
252
253/** A matrix-exponential age law: P(age > t) = g exp(A t) h. */
254struct AoiMe {
255 std::vector<double> g;
257 std::vector<double> h;
258 double mean = std::numeric_limits<double>::quiet_NaN();
259 double var = std::numeric_limits<double>::quiet_NaN();
260};
261
262/** Both age laws of one system, with the policy parameter that produced them. */
265 std::string system_type; ///< "bufferless" or "singlebuffer"
266 double preemption = std::numeric_limits<double>::quiet_NaN();
267};
268
269namespace aoi_detail {
270
271/** Write B into A at (r0, c0). */
272inline void put(Matrix<double>& A, std::size_t r0, std::size_t c0, const Matrix<double>& B) {
273 for (std::size_t i = 0; i < B.rows(); ++i)
274 for (std::size_t j = 0; j < B.cols(); ++j) A(r0 + i, c0 + j) = B(i, j);
275}
276
277/** A row vector as a 1 x n matrix, and a column vector as n x 1. */
278inline Matrix<double> row(const std::vector<double>& v) {
279 Matrix<double> M(1, v.size(), 0.0);
280 for (std::size_t j = 0; j < v.size(); ++j) M(0, j) = v[j];
281 return M;
282}
283inline Matrix<double> col(const std::vector<double>& v) {
284 Matrix<double> M(v.size(), 1, 0.0);
285 for (std::size_t i = 0; i < v.size(); ++i) M(i, 0) = v[i];
286 return M;
287}
288
289/** MATLAB `g / A` for a row vector g: the x solving x A = g. */
290inline std::vector<double> rdivide(const std::vector<double>& g, const Matrix<double>& A) {
291 const std::size_t n = A.rows();
292 if (g.size() != n) throw InputError("aoi: row division size mismatch");
293 Matrix<double> At(n, n, 0.0);
294 for (std::size_t i = 0; i < n; ++i)
295 for (std::size_t j = 0; j < n; ++j) At(i, j) = A(j, i);
296 return solve(At, g);
297}
298
299/** g A^{-k} h, the moment form the reference writes as g/(A^k)*h. */
300inline double moment_form(const std::vector<double>& g, const Matrix<double>& A,
301 const std::vector<double>& h, unsigned k) {
302 std::vector<double> x = g;
303 for (unsigned i = 0; i < k; ++i) x = rdivide(x, A);
304 double s = 0.0;
305 for (std::size_t i = 0; i < x.size() && i < h.size(); ++i) s += x[i] * h[i];
306 return s;
307}
308
309/**
310 * Line 2 of Algorithm 1: the Householder reflector that maps the drift signs
311 * onto the first coordinate, used when the level-zero split is known a priori.
312 */
313inline Matrix<double> householder_p(std::size_t z) {
314 std::vector<double> u1(z, 1.0);
315 u1[z - 1] = -1.0;
316 double nrm = 0.0;
317 for (std::size_t i = 0; i < z; ++i) nrm += u1[i] * u1[i];
318 nrm = std::sqrt(nrm);
319 std::vector<double> u = u1;
320 u[0] -= nrm;
321 double uu = 0.0;
322 for (std::size_t i = 0; i < z; ++i) uu += u[i] * u[i];
323 Matrix<double> P = line::eye<double>(z);
324 if (uu > 0.0)
325 for (std::size_t i = 0; i < z; ++i)
326 for (std::size_t j = 0; j < z; ++j) P(i, j) -= 2.0 * u[i] * u[j] / uu;
327 return P;
328}
329
330/**
331 * Lines 3 and 4 of Algorithm 1, shared by both systems: split QR by P, solve
332 * the boundary system for (g, d), and return g.
333 *
334 * @param QR Q R^{-1}
335 * @param P the orthogonal splitter
336 * @param Rd the drift matrix R
337 * @param Qtil the modified generator Qtilde
338 * @param a the number of negative-drift states
339 */
340struct AlgOneSplit {
341 Matrix<double> A, H;
342 std::vector<double> g, d;
343};
344inline AlgOneSplit alg_one(const Matrix<double>& QR, const Matrix<double>& P,
345 const Matrix<double>& Rd, const Matrix<double>& Qtil, std::size_t a) {
346 const std::size_t z = QR.rows();
347 const std::size_t b = z - a;
348 AlgOneSplit s;
349 // Ae = P' QR P, then A is its trailing (z-a) block and H the trailing
350 // columns of P, transposed.
351 Matrix<double> Pt(z, z, 0.0);
352 for (std::size_t i = 0; i < z; ++i)
353 for (std::size_t j = 0; j < z; ++j) Pt(i, j) = P(j, i);
354 const Matrix<double> Ae = matmul(matmul(Pt, QR), P);
355 s.A = Matrix<double>(b, b, 0.0);
356 for (std::size_t i = 0; i < b; ++i)
357 for (std::size_t j = 0; j < b; ++j) s.A(i, j) = Ae(a + i, a + j);
358 s.H = Matrix<double>(b, z, 0.0);
359 for (std::size_t i = 0; i < b; ++i)
360 for (std::size_t j = 0; j < z; ++j) s.H(i, j) = P(j, a + i);
361
362 // EqnMatrix = [H R, -A^{-1} H e; -Qtilde(b+1:z, :), e], solved transposed.
363 const Matrix<double> HR = matmul(s.H, Rd);
364 std::vector<double> He(b, 0.0);
365 for (std::size_t i = 0; i < b; ++i)
366 for (std::size_t j = 0; j < z; ++j) He[i] += s.H(i, j);
367 const Matrix<double> Ainv = inverse(s.A);
368 std::vector<double> AinvHe(b, 0.0);
369 for (std::size_t i = 0; i < b; ++i)
370 for (std::size_t j = 0; j < b; ++j) AinvHe[i] += Ainv(i, j) * He[j];
371
372 Matrix<double> Eq(z, z + 1, 0.0);
373 for (std::size_t i = 0; i < b; ++i) {
374 for (std::size_t j = 0; j < z; ++j) Eq(i, j) = HR(i, j);
375 Eq(i, z) = -AinvHe[i];
376 }
377 for (std::size_t i = 0; i < a; ++i) {
378 for (std::size_t j = 0; j < z; ++j) Eq(b + i, j) = -Qtil(b + i, j);
379 Eq(b + i, z) = 1.0;
380 }
381 // The system is EqnMatrix' x = [0 ... 0 1]', overdetermined by one row.
382 Matrix<double> EqT(z + 1, z, 0.0);
383 for (std::size_t i = 0; i < z; ++i)
384 for (std::size_t j = 0; j < z + 1; ++j) EqT(j, i) = Eq(i, j);
385 std::vector<double> rhs(z + 1, 0.0);
386 rhs[z] = 1.0;
387 const LstsqResult<double> sol = lstsq(EqT, rhs);
388 s.g.assign(sol.x.begin(), sol.x.begin() + static_cast<std::ptrdiff_t>(b));
389 s.d.assign(sol.x.begin() + static_cast<std::ptrdiff_t>(b), sol.x.end());
390 return s;
391}
392
393/** Normalize g by -g A^{-1} h and read the first two moments. */
394inline AoiMe finish_me(const std::vector<double>& g, const Matrix<double>& A,
395 const std::vector<double>& h) {
396 AoiMe me;
397 me.A = A;
398 me.h = h;
399 const double nc = -moment_form(g, A, h, 1);
400 me.g.assign(g.size(), 0.0);
401 for (std::size_t i = 0; i < g.size(); ++i) me.g[i] = g[i] / nc;
402 me.mean = moment_form(me.g, A, h, 2);
403 me.var = -2.0 * moment_form(me.g, A, h, 3) - me.mean * me.mean;
404 return me;
405}
406
407} // namespace aoi_detail
408
409/**
410 * Port of `solveBufferless.m`: the AoI and PAoI laws of a PH/PH/1/1 system in
411 * which an arrival meeting a busy server preempts it with probability p.
412 */
413inline AoiSolution aoi_solve_bufferless(const std::vector<double>& tau, const Matrix<double>& Tm,
414 const std::vector<double>& sigma, const Matrix<double>& Sm,
415 double p) {
416 using namespace aoi_detail;
417 const std::size_t k = Tm.cols(), l = Sm.cols();
418 if (k == 0 || l == 0) throw InputError("aoi_solve_bufferless: empty representation");
419 if (tau.size() != k || sigma.size() != l)
420 throw InputError("aoi_solve_bufferless: the initial vectors do not match their generators");
421 const std::size_t z = 2 * k * l + k + 1, a = 1, b = z - 1;
422
423 std::vector<double> kappa(k, 0.0), nu(l, 0.0);
424 for (std::size_t i = 0; i < k; ++i)
425 for (std::size_t j = 0; j < k; ++j) kappa[i] -= Tm(i, j);
426 for (std::size_t i = 0; i < l; ++i)
427 for (std::size_t j = 0; j < l; ++j) nu[i] -= Sm(i, j);
428 const Matrix<double> kap = col(kappa), nuc = col(nu);
429 const Matrix<double> taur = row(tau), sigr = row(sigma);
431 const Matrix<double> ones_l(l, 1, 1.0), ones_k(k, 1, 1.0);
432
433 // Q11 and Q33 of Eqn (13): service and arrival running together, with the
434 // non-preempting and the preempting restart respectively.
435 Matrix<double> Q11 = mam::kron(Ik, Sm);
436 {
437 const Matrix<double> t2 = mam::kron(Tm, Il);
438 const Matrix<double> t3 = mam::kron(mam::kron(kap, taur), Il);
439 for (std::size_t i = 0; i < k * l; ++i)
440 for (std::size_t j = 0; j < k * l; ++j) Q11(i, j) += t2(i, j) + (1.0 - p) * t3(i, j);
441 }
442 Matrix<double> Q33 = Q11;
443 {
444 const Matrix<double> t4 = mam::kron(kap, mam::kron(ones_l, mam::kron(taur, sigr)));
445 for (std::size_t i = 0; i < k * l; ++i)
446 for (std::size_t j = 0; j < k * l; ++j) Q33(i, j) += p * t4(i, j);
447 }
448
449 Matrix<double> Q(z, z, 0.0);
450 put(Q, 0, 0, Q11);
451 put(Q, 0, k * l, mam::kron(Ik, nuc));
452 {
453 Matrix<double> lastcol = mam::kron(kap, ones_l);
454 for (std::size_t i = 0; i < k * l; ++i) Q(i, z - 1) = p * lastcol(i, 0);
455 }
456 put(Q, k * l, k * l, Tm);
457 put(Q, k * l, k * l + k, mam::kron(kap, mam::kron(taur, sigr)));
458 put(Q, k * l + k, k * l + k, Q33);
459 {
460 const Matrix<double> lastcol = mam::kron(ones_k, nuc);
461 for (std::size_t i = 0; i < k * l; ++i) Q(k * l + k + i, z - 1) = lastcol(i, 0);
462 }
463
465 Rd(z - 1, z - 1) = -1.0;
466 Matrix<double> Qtil = Q;
467 {
468 const Matrix<double> ts = mam::kron(taur, sigr);
469 for (std::size_t j = 0; j < k * l; ++j) Qtil(z - 1, j) = ts(0, j);
470 Qtil(z - 1, z - 1) = -1.0;
471 }
472 // Q / R with R diagonal +-1 is Q with its last column negated.
473 Matrix<double> QR = Q;
474 for (std::size_t i = 0; i < z; ++i) QR(i, z - 1) = -QR(i, z - 1);
475
476 const AlgOneSplit s = alg_one(QR, householder_p(z), Rd, Qtil, a);
477
478 AoiSolution out;
479 out.system_type = "bufferless";
480 out.preemption = p;
481 // AoI lives in phases 2 and 3; PAoI in phase 3, weighted by the exit rates.
482 std::vector<double> sel(z, 0.0);
483 for (std::size_t i = k * l; i < k * l + k * l + k; ++i) sel[i] = 1.0;
484 std::vector<double> h(b, 0.0);
485 for (std::size_t i = 0; i < b; ++i)
486 for (std::size_t j = 0; j < z; ++j) h[i] += s.H(i, j) * sel[j];
487 out.aoi = finish_me(s.g, s.A, h);
488
489 std::vector<double> selp(z, 0.0);
490 {
491 const Matrix<double> kn = mam::kron(ones_k, nuc);
492 for (std::size_t i = 0; i < k * l; ++i) selp[k * l + k + i] = kn(i, 0);
493 }
494 std::vector<double> hp(b, 0.0);
495 for (std::size_t i = 0; i < b; ++i)
496 for (std::size_t j = 0; j < z; ++j) hp[i] += s.H(i, j) * selp[j];
497 out.paoi = finish_me(s.g, s.A, hp);
498 return out;
499}
500
501/**
502 * Port of `solveSingleBuffer.m`: the AoI and PAoI laws of an M/PH/1/2 system in
503 * which a waiting update is replaced by a fresher arrival with probability r.
504 *
505 * Two fluid queues are solved in sequence. The FIRST gives the waiting-time law
506 * of an update that finds the server busy, and needs the SCHUR splitter rather
507 * than the Householder one: its drift has two negative states and their
508 * invariant subspace is not known a priori. Lemma 1 of the reference then
509 * turns that law into a PH pair (beta, B), which drives the SECOND queue --
510 * the age process proper -- where the split is again explicit.
511 */
512inline AoiSolution aoi_solve_singlebuffer(double lambda, const std::vector<double>& sigma,
513 const Matrix<double>& Sm, double r) {
514 using namespace aoi_detail;
515 const std::size_t l = Sm.cols();
516 if (l == 0) throw InputError("aoi_solve_singlebuffer: empty representation");
517 if (sigma.size() != l)
518 throw InputError("aoi_solve_singlebuffer: sigma does not match its generator");
519 if (!(lambda > 0.0)) throw InputError("aoi_solve_singlebuffer: the arrival rate must be positive");
520
521 std::vector<double> nu(l, 0.0);
522 for (std::size_t i = 0; i < l; ++i)
523 for (std::size_t j = 0; j < l; ++j) nu[i] -= Sm(i, j);
524 const Matrix<double> nuc = col(nu), sigr = row(sigma);
525
526 // ---- the waiting-time fluid queue of Eqn (16) --------------------------
527 std::size_t z = l + 2;
528 const std::size_t a1 = 2, b1 = l;
529 Matrix<double> Q(z, z, 0.0);
530 put(Q, 0, 0, Sm);
531 for (std::size_t i = 0; i < l; ++i) Q(i, l) = nu[i];
532 Q(l, l) = -lambda;
533 Q(l, l + 1) = lambda;
534
536 Rd(z - 2, z - 2) = -1.0;
537 Rd(z - 1, z - 1) = -1.0;
538
539 Matrix<double> Qtil(z, z, 0.0);
540 for (std::size_t j = 0; j < l; ++j) {
541 Qtil(l, j) = lambda * sigma[j];
542 Qtil(l + 1, j) = sigma[j];
543 }
544 Qtil(l, l) = -lambda;
545 Qtil(l + 1, l + 1) = -1.0;
546
547 Matrix<double> QR = Q;
548 for (std::size_t i = 0; i < z; ++i) {
549 QR(i, z - 2) = -QR(i, z - 2);
550 QR(i, z - 1) = -QR(i, z - 1);
551 }
552
553 // pik solves pik (Q + e e') = e', the reference's rank-one regularization.
554 std::vector<double> pik;
555 {
556 Matrix<double> Aug(z, z, 0.0);
557 for (std::size_t i = 0; i < z; ++i)
558 for (std::size_t j = 0; j < z; ++j) Aug(i, j) = Q(i, j) + 1.0;
559 pik = rdivide(std::vector<double>(z, 1.0), Aug);
560 }
561 // A1 shifts the singular generator so its stable subspace is separated.
562 Matrix<double> A1 = QR;
563 {
564 std::vector<double> xR(z, 0.0);
565 for (std::size_t i = 0; i < z; ++i)
566 for (std::size_t j = 0; j < z; ++j) xR[i] += Rd(i, j);
567 double den = 0.0;
568 for (std::size_t i = 0; i < z; ++i) den += pik[i] * xR[i];
569 for (std::size_t i = 0; i < z; ++i)
570 for (std::size_t j = 0; j < z; ++j) A1(i, j) += xR[i] * pik[j] / den;
571 }
572 // ordschur(..., 'rhp'): the right-half-plane eigenvalues come first.
574 {
575 const RealSchur sc = schur_decomposition(A1);
576 std::vector<double> key(z, 0.0);
577 for (std::size_t i = 0; i < z; ++i) key[i] = (sc.T(i, i) >= 0.0) ? 1.0 : 0.0;
578 P = schur_reorder(sc, key).Z;
579 }
580
581 const AlgOneSplit s1 = alg_one(QR, P, Rd, Qtil, a1);
582 const double c_0 = s1.d.empty() ? 0.0 : s1.d[0];
583
584 // Lemma 1: the waiting law as a PH pair (beta, B).
585 Matrix<double> wait_A = s1.A;
586 for (std::size_t i = 0; i < b1; ++i) wait_A(i, i) -= r * lambda;
587 std::vector<double> selw(z, 0.0);
588 selw[l] = 1.0;
589 selw[l + 1] = r;
590 std::vector<double> wait_H(b1, 0.0);
591 for (std::size_t i = 0; i < b1; ++i)
592 for (std::size_t j = 0; j < z; ++j) wait_H[i] += s1.H(i, j) * selw[j];
593 std::vector<double> wait_g = s1.g;
594 {
595 const double n1 = 1.0 / (-moment_form(wait_g, wait_A, wait_H, 1) + c_0);
596 for (std::size_t i = 0; i < b1; ++i) wait_g[i] *= n1;
597 }
598 const std::vector<double> Mdiag = [&]() {
599 std::vector<double> y = solve(wait_A, wait_H);
600 for (std::size_t i = 0; i < y.size(); ++i) y[i] = -y[i];
601 return y;
602 }();
603 Matrix<double> B(l, l, 0.0);
604 for (std::size_t i = 0; i < l; ++i)
605 for (std::size_t j = 0; j < l; ++j) B(i, j) = wait_A(i, j) * Mdiag[j] / Mdiag[i];
606 std::vector<double> beta(l, 0.0);
607 for (std::size_t i = 0; i < l; ++i) beta[i] = wait_g[i] * Mdiag[i];
608 double beta_0 = 1.0;
609 for (std::size_t i = 0; i < l; ++i) beta_0 -= beta[i];
610 std::vector<double> psi(l, 0.0);
611 for (std::size_t i = 0; i < l; ++i)
612 for (std::size_t j = 0; j < l; ++j) psi[i] -= B(i, j);
613
614 // ---- the age fluid queue of Eqn (19) ----------------------------------
615 z = 4 * l + 2;
616 const std::size_t a2 = 1, b2 = z - 1;
617 Matrix<double> Q2(z, z, 0.0);
618 put(Q2, 0, 0, B);
619 put(Q2, 0, l, mam::kron(col(psi), sigr));
620 for (std::size_t i = 0; i < l; ++i) {
621 for (std::size_t j = 0; j < l; ++j) {
622 Q2(l + i, l + j) = Sm(i, j) - (i == j ? lambda : 0.0);
623 Q2(l + i, 2 * l + j) = (i == j) ? lambda : 0.0;
624 Q2(2 * l + i, 2 * l + j) = Sm(i, j);
625 Q2(3 * l + 1 + i, 3 * l + 1 + j) = Sm(i, j);
626 }
627 Q2(l + i, 3 * l) = nu[i];
628 Q2(3 * l + 1 + i, z - 1) = nu[i];
629 }
630 put(Q2, 2 * l, 3 * l + 1, mam::kron(nuc, sigr));
631 Q2(3 * l, 3 * l) = -lambda;
632 for (std::size_t j = 0; j < l; ++j) Q2(3 * l, 3 * l + 1 + j) = lambda * sigma[j];
633
635 Rd2(z - 1, z - 1) = -1.0;
636 Matrix<double> Qtil2 = Q2;
637 for (std::size_t j = 0; j < l; ++j) {
638 Qtil2(z - 1, j) = beta[j];
639 Qtil2(z - 1, l + j) = beta_0 * sigma[j];
640 }
641 Qtil2(z - 1, z - 1) = -1.0;
642 Matrix<double> QR2 = Q2;
643 for (std::size_t i = 0; i < z; ++i) QR2(i, z - 1) = -QR2(i, z - 1);
644
645 const AlgOneSplit s2 = alg_one(QR2, householder_p(z), Rd2, Qtil2, a2);
646
647 AoiSolution out;
648 out.system_type = "singlebuffer";
649 out.preemption = r;
650 std::vector<double> sel(z, 0.0);
651 for (std::size_t i = 3 * l; i < 4 * l + 1; ++i) sel[i] = 1.0;
652 std::vector<double> h(b2, 0.0);
653 for (std::size_t i = 0; i < b2; ++i)
654 for (std::size_t j = 0; j < z; ++j) h[i] += s2.H(i, j) * sel[j];
655 out.aoi = finish_me(s2.g, s2.A, h);
656
657 std::vector<double> selp(z, 0.0);
658 for (std::size_t i = 0; i < l; ++i) selp[3 * l + 1 + i] = nu[i];
659 std::vector<double> hp(b2, 0.0);
660 for (std::size_t i = 0; i < b2; ++i)
661 for (std::size_t j = 0; j < z; ++j) hp[i] += s2.H(i, j) * selp[j];
662 out.paoi = finish_me(s2.g, s2.A, hp);
663 return out;
664}
665
666/**
667 * `getCdfAoI`: F(t) = 1 - S(t) with S the survival function of the age law.
668 *
669 * (g, A, h) IS A DENSITY TRIPLE, not a survival one: `finish_me` normalizes g by
670 * `-g A^-1 h` so that the density f(t) = g exp(A t) h integrates to 1 and the
671 * mean is `g A^-2 h`. The survival function of such a law is
672 * S(t) = -g exp(A t) A^-1 h, so F(t) = 1 + g exp(A t) A^-1 h, which is 0 at
673 * t = 0 (`g A^-1 h = -1`) and rises to 1. Subtracting the DENSITY instead --
674 * the form all four codebases carried until 2026-07-31 -- gives a curve that
675 * falls before it rises and is not a distribution function at all.
676 */
677inline double aoi_cdf(const AoiMe& me, double t) {
678 if (t <= 0.0) return 0.0;
679 const Matrix<double> E = expm(me.A, t);
680 // gE = g exp(A t), then (gE) A^-1 by the same row solve the moments use.
681 std::vector<double> gE(me.g.size(), 0.0);
682 for (std::size_t j = 0; j < gE.size(); ++j)
683 for (std::size_t i = 0; i < me.g.size(); ++i) gE[j] += me.g[i] * E(i, j);
684 const std::vector<double> y = aoi_detail::rdivide(gE, me.A);
685 double f = 1.0;
686 for (std::size_t i = 0; i < y.size() && i < me.h.size(); ++i) f += y[i] * me.h[i];
687 return std::max(0.0, std::min(1.0, f));
688}
689
690/** What the AoI branch of `mfq` returns: the age laws and the metrics beside them. */
693 std::vector<double> QN, UN, RN, TN; ///< per class, at the queue
694 double lambda = 0.0, mu = 0.0;
695};
696
697/** Port of `solver_mfq_aoi.m`. */
698template <class T>
700 double preempt_override) {
701 if (!top.ok)
702 throw UnsupportedError("fluid aoi: " +
703 (top.error.empty() ? std::string("not an AoI topology") : top.error));
704 const AoiParams par = aoi_extract_params(sn, top, preempt_override);
705
706 FluidAoiResult out;
707 if (top.capacity == 1.0)
708 out.age = aoi_solve_bufferless(par.tau, par.Tarr, par.sigma, par.Ssvc, par.p);
709 else
710 out.age = aoi_solve_singlebuffer(par.lambda, par.sigma, par.Ssvc, par.p);
711
712 const std::size_t K = sn.nclasses;
713 out.QN.assign(K, 0.0);
714 out.UN.assign(K, 0.0);
715 out.RN.assign(K, 0.0);
716 out.TN.assign(K, 0.0);
717
718 // The mean service time of the PH pair the AoI algorithm was given.
719 const std::size_t l = par.Ssvc.rows();
720 const std::vector<double> y = aoi_detail::rdivide(par.sigma, par.Ssvc);
721 double mean_svc = 0.0;
722 for (std::size_t i = 0; i < l; ++i) mean_svc -= y[i];
723 const double lambda = par.lambda;
724 const double mu = 1.0 / mean_svc;
725 out.lambda = lambda;
726 out.mu = mu;
727 const double rho = lambda / mu;
728 const std::size_t c = top.cls;
729 out.UN[c] = std::min(1.0, rho);
730 out.TN[c] = (rho < 1.0) ? lambda : mu;
731 if (rho < 1.0) {
732 out.QN[c] = rho / (1.0 - rho);
733 out.RN[c] = 1.0 / (mu - lambda);
734 } else {
735 out.QN[c] = std::numeric_limits<double>::infinity();
736 out.RN[c] = std::numeric_limits<double>::infinity();
737 }
738 return out;
739}
740
741} // namespace fluid
742} // namespace line
743
744#endif // LINE_SOLVERS_FLUID_FLUID_AOI_H
MAP to PH conversion for the age-of-information solvers.
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
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
Eigenvalues and singular values, backed by LAPACK.
The exception types the port throws.
Matrix exponential by scaling and squaring with a diagonal Pade approximant.
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
Least squares for a rectangular system, exact-capable.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
Marked MAP (MMAP) algebra: per-class rates, class probabilities, superposition, normalization and sca...
AoiPh< T > aoi_dist2ph(const Matrix< T > &D0, const Matrix< T > &D1)
Convert a MAP (D0, D1) into the PH pair (alpha, T).
Definition aoi_dist2ph.h:69
double aoi_cdf(const AoiMe &me, double t)
getCdfAoI: F(t) = 1 - S(t) with S the survival function of the age law.
Definition fluid_aoi.h:677
AoiTopology aoi_is_aoi(const qn::NetworkStruct< T > &sn)
Port of aoi_is_aoi.m.
Definition fluid_aoi.h:102
AoiSolution aoi_solve_bufferless(const std::vector< double > &tau, const Matrix< double > &Tm, const std::vector< double > &sigma, const Matrix< double > &Sm, double p)
Port of solveBufferless.m: the AoI and PAoI laws of a PH/PH/1/1 system in which an arrival meeting a ...
Definition fluid_aoi.h:413
AoiParams aoi_extract_params(const qn::NetworkStruct< T > &sn, const AoiTopology &top, double preempt_override)
Port of aoi_extract_params.m.
Definition fluid_aoi.h:207
AoiSolution aoi_solve_singlebuffer(double lambda, const std::vector< double > &sigma, const Matrix< double > &Sm, double r)
Port of solveSingleBuffer.m: the AoI and PAoI laws of an M/PH/1/2 system in which a waiting update is...
Definition fluid_aoi.h:512
FluidAoiResult fluid_aoi(const qn::NetworkStruct< T > &sn, const AoiTopology &top, double preempt_override)
Port of solver_mfq_aoi.m.
Definition fluid_aoi.h:699
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
Matrix< T > kron(const Matrix< T > &A, const Matrix< T > &B)
Kronecker product.
Definition mmap_lambda.h:57
LstsqResult< T > lstsq(const Matrix< T > &A, const std::vector< T > &b, const T &tol)
Least-squares solution of A x = b, minimum-norm when A is rank deficient.
Definition lstsq.h:152
Matrix< T > inverse(const Matrix< T > &A)
Inverse by LU with one factorization and n back substitutions.
Definition linalg.h:72
RealSchur schur_reorder(const RealSchur &s, const std::vector< double > &key)
Reorder the diagonal blocks of a real Schur form into DESCENDING key order, stably,...
Definition eig.h:248
Matrix< T > matmul(const Matrix< T > &A, const Matrix< T > &B)
Matrix product A B.
Definition linalg.h:36
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
Matrix< T > eye(std::size_t n)
Identity of order n.
Definition linalg.h:28
RealSchur schur_decomposition(const Matrix< double > &A)
Real Schur factorization of a general square matrix (LAPACK dgees, unsorted).
Definition eig.h:182
Matrix< T > expm(const Matrix< T > &A)
Matrix exponential exp(A).
Definition expm.h:141
A queueing network and its refreshed NetworkStruct.
Real Schur factorization A = Z T Z^T, with Z orthogonal and T upper quasi-triangular: 1 x 1 diagonal ...
Definition eig.h:169
Matrix< double > Z
orthogonal Schur vectors
Definition eig.h:170
Matrix< double > T
upper quasi-triangular factor
Definition eig.h:171
The (alpha, T) PH pair produced by aoi_dist2ph.
Definition aoi_dist2ph.h:57
Matrix< T > Tmat
sub-generator, MATLAB's T
Definition aoi_dist2ph.h:59
std::vector< T > alpha
initial probability vector, sums to one
Definition aoi_dist2ph.h:58
A matrix-exponential age law: P(age > t) = g exp(A t) h.
Definition fluid_aoi.h:254
std::vector< double > g
Definition fluid_aoi.h:255
std::vector< double > h
Definition fluid_aoi.h:257
Matrix< double > A
Definition fluid_aoi.h:256
The (tau, T) / (sigma, S) pairs and the preemption probability.
Definition fluid_aoi.h:191
Matrix< double > Ssvc
Definition fluid_aoi.h:192
double p
preemption (bufferless) or replacement (single buffer)
Definition fluid_aoi.h:194
double lambda
arrival rate, the single-buffer input
Definition fluid_aoi.h:195
std::vector< double > tau
Definition fluid_aoi.h:193
std::vector< double > sigma
Definition fluid_aoi.h:193
Matrix< double > Tarr
Definition fluid_aoi.h:192
Both age laws of one system, with the policy parameter that produced them.
Definition fluid_aoi.h:263
std::string system_type
"bufferless" or "singlebuffer"
Definition fluid_aoi.h:265
What the AoI gate found, when it matches.
Definition fluid_aoi.h:84
double capacity
1 = bufferless, 2 = single buffer
Definition fluid_aoi.h:90
lang::SchedStrategy sched
Definition fluid_aoi.h:91
std::size_t cls
the single open class
Definition fluid_aoi.h:89
std::size_t source
0-based station index
Definition fluid_aoi.h:87
What the AoI branch of mfq returns: the age laws and the metrics beside them.
Definition fluid_aoi.h:691
std::vector< double > RN
Definition fluid_aoi.h:693
std::vector< double > TN
per class, at the queue
Definition fluid_aoi.h:693
std::vector< double > UN
Definition fluid_aoi.h:693
std::vector< double > QN
Definition fluid_aoi.h:693
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:759
A node of the network.