LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
infer_variational.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_API_INFER_INFER_VARIATIONAL_H
6#define LINE_API_INFER_INFER_VARIATIONAL_H
7
8/**
9 * @file
10 * @ingroup api_infer
11 * Variational inference for Markovian queueing networks.
12 *
13 * Port of matlab/src/api/infer/infer_variational.m and of the JAR twin
14 * jline.inference.api.Infer_variational, following I. Perez, G. Casale,
15 * "Variational Inference for Markovian Queueing Networks", Advances in Applied
16 * Probability 53(3), 2021.
17 *
18 * The network trajectory is reparameterised by the transition counts Y^eta,
19 * eta = (i,j,c), so that the station marginals decouple:
20 *
21 * x_{i,c}(t) = x_{i,c}(0) + sum_{eta in In(i,c)} Y^eta(t)
22 * - sum_{eta in Out(i,c)} Y^eta(t)
23 *
24 * The variational family is a product of inhomogeneous pure-birth processes,
25 * one per transition, with rate nu^eta(t,y), times a product of Gamma
26 * densities over the unknown service rates. The state space is expanded by
27 * adding DELTA to every feasible rate, so that queue lengths may go negative
28 * and the approximating measure stays mutually absolutely continuous with the
29 * target; the original model is recovered as DELTA -> 0.
30 *
31 * Each iteration performs, per transition, a backward pass for the Lagrange
32 * multipliers r^eta with multiplicative jumps at the observation epochs, the
33 * rate update nu^eta(t,y) = exp(E log Xi^eta(t,y)) r^eta(t,y+1)/r^eta(t,y),
34 * and a forward pass of the master equation for the marginal. The conjugate
35 * Gamma posteriors are then refreshed from the expected number of firings and
36 * the expected exposure time of each station-class pair.
37 *
38 * ARITHMETIC: the backward and forward passes are uniformizations, i.e. convex
39 * combinations of sub-stochastic matrix actions, so they stay positive and
40 * bounded whatever the rate scale. The transcendental content is exp, log and
41 * the digamma of the Gamma posteriors; there is no random-number stream, since
42 * the expectations over the other transitions are taken on a Halton lattice
43 * mapped through the inverse marginal c.d.f. The estimator therefore
44 * reproduces the MATLAB, Java and Python implementations digit for digit.
45 */
46
47#include <algorithm>
48#include <array>
49#include <cmath>
50#include <cstddef>
51#include <limits>
52#include <numeric>
53#include <vector>
54
55#include "line/num/number.h"
56#include "line/util/error.h"
57#include "line/util/matrix.h"
58
59namespace line {
60namespace infer {
61
62/** Service discipline of a station, as seen by the load factor Upsilon. */
63enum class VariationalSched { INF = 0, SHARED = 1, EXTERNAL = 2 };
64
65/**
66 * Inference problem handed to infer_variational.
67 *
68 * Station-class pairs are flattened column-major, so that pair (m,r) sits at
69 * index r*M+m, matching the MATLAB, Java and Python specifications. Station
70 * and class indices inside `arcs` are one-based; index 0 marks the external
71 * source or sink.
72 */
73template <class T>
75 /** (T x 3) transitions [i j c]; i==0 external source, j==0 sink. */
76 std::vector<std::array<std::size_t, 3>> arcs;
77 /** (M x R) initial queue lengths. */
79 /** (M) discipline codes. */
80 std::vector<int> sched;
81 /** (M) number of servers. */
82 std::vector<T> nservers;
83 /** (T) routing probability of each transition. */
84 std::vector<T> routeprob;
85 /** (T) index in 1..P of the rate governing the transition, 0 when known. */
86 std::vector<std::size_t> arcparam;
87 /** (T) known rate for transitions with arcparam == 0. */
88 std::vector<T> arcrate;
89 /** (P) Gamma prior shapes and rates. */
90 std::vector<T> alpha0, beta0;
91 /** (K) observation epochs. */
92 std::vector<T> obsTimes;
93 /** (K x M*R) observed queue lengths; an unobserved entry is `unobserved`. */
95 /** (M*R) support size of the uniform contamination. */
96 std::vector<T> obsRange;
97 /** probability that a reading is faulty. */
99 /**
100 * (M*R) upper bound on the queue length, empty for none. In a closed
101 * network this is the chain population, and clamping the load there keeps
102 * the expanded state space from crediting a station with more jobs than
103 * the network holds.
104 */
105 std::vector<T> capacity;
106
107 /** Sentinel marking an unobserved entry of obsData. */
108 static T unobserved() { return num_traits<T>::from_double(-1.0); }
109};
110
111/** Options of infer_variational; a negative box means "derive a default". */
112template <class T>
114 int verbose = 0;
115 std::size_t iter_max = 20;
116 double tol = 1e-3;
117 std::size_t nsamples = 200;
118 /** rate added to every feasible transition by the space expansion. */
119 double delta = 1e-3;
120 double floor = 1e-4;
121 /** cap on the variational rates; derived from ymax and tmax when negative. */
122 double rate_max = -1.0;
123 double rate_cap_factor = 10.0;
124 double unifmax = 30.0;
125 double unif_tol = 1e-12;
126 std::size_t unif_max_terms = 2000;
127 double tmax = -1.0;
128 double dt = -1.0;
129 long ngrid = -1;
130 long ymax = -1;
131};
132
133/** Outcome of infer_variational. */
134template <class T>
137 std::vector<T> bound;
139 std::vector<Matrix<T>> Y, nu; // one (G x ymax+1) block per transition
140 std::vector<T> tgrid;
141 Matrix<T> qlen; // (G x M*R)
142 std::size_t iter = 0;
143 bool converged = false;
145};
146
147namespace detail {
148
149/** Recurrence threshold: the Stirling tails are below 1e-17 from here on. */
150inline int iv_gamma_shift() { return 20; }
151
152/** psi(x) for x > 0, upward recurrence to x >= 20 then the Stirling series. */
153template <class T>
154T iv_digamma(const T& x0) {
155 const T one = num_traits<T>::from_int(1);
156 const T lim = num_traits<T>::from_int(iv_gamma_shift());
157 T x = x0, acc = num_traits<T>::from_int(0);
158 while (x < lim) {
159 acc -= one / x;
160 x += one;
161 }
162 const T inv = one / x, inv2 = inv * inv;
163 using std::log;
164 T s = log(x) - inv / num_traits<T>::from_int(2);
165 T p = inv2;
166 s -= p / num_traits<T>::from_int(12);
167 p *= inv2;
168 s += p / num_traits<T>::from_int(120);
169 p *= inv2;
170 s -= p / num_traits<T>::from_int(252);
171 p *= inv2;
172 s += p / num_traits<T>::from_int(240);
173 p *= inv2;
174 s -= p / num_traits<T>::from_int(132);
175 return acc + s;
176}
177
178/** log Gamma(x) for x > 0, the same construction on log Gamma(x)=log Gamma(x+1)-log x. */
179template <class T>
180T iv_lgamma(const T& x0) {
181 using std::log;
182 const T one = num_traits<T>::from_int(1);
183 const T lim = num_traits<T>::from_int(iv_gamma_shift());
184 T x = x0, acc = num_traits<T>::from_int(0);
185 while (x < lim) {
186 acc -= log(x);
187 x += one;
188 }
189 const T half = one / num_traits<T>::from_int(2);
190 const T log2pi = num_traits<T>::from_double(std::log(2.0 * 3.14159265358979323846));
191 const T inv = one / x, inv2 = inv * inv;
192 T s = (x - half) * log(x) - x + log2pi / num_traits<T>::from_int(2);
193 T p = inv;
194 s += p / num_traits<T>::from_int(12);
195 p *= inv2;
196 s -= p / num_traits<T>::from_int(360);
197 p *= inv2;
198 s += p / num_traits<T>::from_int(1260);
199 p *= inv2;
200 s -= p / num_traits<T>::from_int(1680);
201 p *= inv2;
202 s += p / num_traits<T>::from_int(1188);
203 return acc + s;
204}
205
206/** Load factor Upsilon of a transition leaving a station-class pair. */
207template <class T>
208T iv_ups(const T& xic, const T& xis, const T& nservers, int sched, const T& cap,
209 const T& capstat) {
210 const T zero = num_traits<T>::from_int(0);
211 if (sched == static_cast<int>(VariationalSched::EXTERNAL)) return num_traits<T>::from_int(1);
212 T a = xic < zero ? zero : xic;
213 if (a > cap) a = cap;
214 if (sched == static_cast<int>(VariationalSched::INF)) return a;
215 T b = xis < zero ? zero : xis;
216 if (b > capstat) b = capstat;
217 if (!(b > zero)) return zero;
218 const T srv = nservers < b ? nservers : b;
219 return a / b * srv;
220}
221
222/** k-th prime, k >= 1. */
223inline std::size_t iv_prime(std::size_t k) {
224 std::size_t n = 0, c = 1, p = 2;
225 while (n < k) {
226 ++c;
227 bool isp = true;
228 for (std::size_t d = 2; d * d <= c; ++d) {
229 if (c % d == 0) {
230 isp = false;
231 break;
232 }
233 }
234 if (isp) {
235 ++n;
236 p = c;
237 }
238 }
239 return p;
240}
241
242/** Van der Corput radical inverse of i in the given base. */
243inline double iv_radical_inverse(std::size_t i, std::size_t base) {
244 double r = 0.0, f = 1.0 / static_cast<double>(base);
245 while (i > 0) {
246 r += f * static_cast<double>(i % base);
247 i /= base;
248 f /= static_cast<double>(base);
249 }
250 return r;
251}
252
253/**
254 * Inverse-c.d.f. samples of a marginal on a Halton lattice. Each transition
255 * uses its own prime base, so the samples of distinct transitions are jointly
256 * equidistributed rather than comonotone.
257 */
258template <class T>
259Matrix<T> iv_sample(const Matrix<T>& q, std::size_t S, std::size_t e) {
260 const std::size_t G = q.rows(), ny = q.cols();
261 const std::size_t base = iv_prime(e + 1);
262 std::vector<double> u(S);
263 std::vector<std::size_t> ord(S);
264 for (std::size_t s = 0; s < S; ++s) {
265 u[s] = iv_radical_inverse(s + 1, base);
266 ord[s] = s;
267 }
268 std::stable_sort(ord.begin(), ord.end(),
269 [&u](std::size_t a, std::size_t b) { return u[a] < u[b]; });
270 std::vector<double> us(S);
271 for (std::size_t s = 0; s < S; ++s) us[s] = u[ord[s]];
272 Matrix<T> ys(G, S, num_traits<T>::from_int(0));
273 std::vector<double> c(ny);
274 for (std::size_t g = 0; g < G; ++g) {
275 double acc = 0.0;
276 for (std::size_t y = 0; y < ny; ++y) {
277 acc += num_traits<T>::to_double(q(g, y));
278 c[y] = acc;
279 }
280 if (c[ny - 1] > 0) {
281 for (std::size_t y = 0; y < ny; ++y) c[y] /= c[ny - 1];
282 }
283 c[ny - 1] = 1.0;
284 std::size_t j = 0;
285 for (std::size_t s = 0; s < S; ++s) {
286 while (j + 1 < ny && c[j] < us[s]) ++j;
287 ys(g, ord[s]) = num_traits<T>::from_int(static_cast<int>(j));
288 }
289 }
290 return ys;
291}
292
293/** Slack multiplier of the rate cap; unity when the cap is inactive. */
294template <class T>
295T iv_damp(const T& sl, const T& ye, double floor) {
296 const T zero = num_traits<T>::from_int(0);
297 if (!(sl > zero)) return num_traits<T>::from_int(1);
298 using std::exp;
299 const T fl = num_traits<T>::from_double(floor);
300 const T z = sl / (ye > fl ? ye : fl);
301 const T d = (num_traits<T>::from_int(1) + z) / exp(z);
302 if (!(d > zero)) return zero;
303 return d;
304}
305
306/** Normalise by the largest entry; only the ratios of the multipliers matter. */
307template <class T>
308void iv_rescale(std::vector<T>& v) {
309 const T zero = num_traits<T>::from_int(0);
310 T m = zero;
311 for (std::size_t i = 0; i < v.size(); ++i)
312 if (v[i] > m) m = v[i];
313 if (m > zero) {
314 for (std::size_t i = 0; i < v.size(); ++i) v[i] = v[i] / m;
315 }
316}
317
318/** One uniformization step of the backward sub-generator. */
319template <class T>
320std::vector<T> iv_back_uniformize(const std::vector<T>& v0, const std::vector<T>& pd,
321 const std::vector<T>& pu, const T& lt,
322 const VariationalOptions<T>& opt) {
323 using std::exp;
324 const std::size_t ny = v0.size();
325 const T one = num_traits<T>::from_int(1);
326 T w = exp(-lt);
327 std::vector<T> v(ny), u(v0), un(ny);
328 for (std::size_t i = 0; i < ny; ++i) v[i] = w * v0[i];
329 T cum = w;
330 std::size_t n = 1;
331 const T tol = num_traits<T>::from_double(opt.unif_tol);
332 while ((one - cum) > tol && n < opt.unif_max_terms) {
333 for (std::size_t i = 0; i < ny; ++i) un[i] = u[i] * (one - pd[i]);
334 for (std::size_t i = 0; i + 1 < ny; ++i) un[i] += u[i + 1] * pu[i];
335 u = un;
336 w = w * lt / num_traits<T>::from_int(static_cast<int>(n));
337 for (std::size_t i = 0; i < ny; ++i) v[i] += w * u[i];
338 cum += w;
339 ++n;
340 }
341 return v;
342}
343
344/** One uniformization step of the pure-birth chain. */
345template <class T>
346std::vector<T> iv_uniformize(const std::vector<T>& v0, const std::vector<T>& p, const T& lt,
347 const VariationalOptions<T>& opt) {
348 using std::exp;
349 const std::size_t ny = v0.size();
350 const T one = num_traits<T>::from_int(1);
351 T w = exp(-lt);
352 std::vector<T> v(ny), u(v0), un(ny);
353 for (std::size_t i = 0; i < ny; ++i) v[i] = w * v0[i];
354 T cum = w;
355 std::size_t n = 1;
356 const T tol = num_traits<T>::from_double(opt.unif_tol);
357 while ((one - cum) > tol && n < opt.unif_max_terms) {
358 for (std::size_t i = 0; i < ny; ++i) un[i] = u[i] * (one - p[i]);
359 for (std::size_t i = ny; i-- > 1;) un[i] += u[i - 1] * p[i - 1];
360 u = un;
361 w = w * lt / num_traits<T>::from_int(static_cast<int>(n));
362 for (std::size_t i = 0; i < ny; ++i) v[i] += w * u[i];
363 cum += w;
364 ++n;
365 }
366 return v;
367}
368
369/**
370 * Backward pass for the Lagrange multipliers. The equation is linear in r, so
371 * on a grid cell with frozen coefficients it is the action of a matrix
372 * exponential. The generator has non-positive row sums by Jensen, so
373 * uniformization evaluates it without the stiffness an explicit rule suffers
374 * when exp(E log Xi) falls orders of magnitude below E[Xi].
375 */
376template <class T>
377Matrix<T> iv_backward(const Matrix<T>& ge, const Matrix<T>& he, const Matrix<T>& sl,
378 const Matrix<T>& Ye, const std::vector<std::size_t>& obsIdx,
379 const Matrix<T>& obsw, const T& dt, const VariationalOptions<T>& opt) {
380 const std::size_t G = ge.rows(), ny = ge.cols();
381 const T zero = num_traits<T>::from_int(0);
382 Matrix<T> r(G, ny, zero);
383 std::vector<T> v(ny, num_traits<T>::from_int(1));
384 for (std::size_t q = 0; q < obsIdx.size(); ++q) {
385 if (obsIdx[q] + 1 == G) {
386 for (std::size_t y = 0; y < ny; ++y)
387 v[y] = v[y] * (obsw(q, y) > zero ? obsw(q, y) : zero);
388 }
389 }
390 iv_rescale(v);
391 for (std::size_t y = 0; y < ny; ++y) r(G - 1, y) = v[y];
392 std::vector<T> pd(ny), pu(ny);
393 for (std::size_t gi = G - 1; gi-- > 0;) {
394 T lam = zero;
395 for (std::size_t y = 0; y < ny; ++y) {
396 const T gv = ge(gi, y) > zero ? ge(gi, y) : zero;
397 T hv = he(gi, y) * iv_damp(sl(gi, y), Ye(gi, y), opt.floor);
398 if (!(hv > zero)) hv = zero;
399 if (hv > gv) hv = gv;
400 pd[y] = gv;
401 pu[y] = hv;
402 if (gv > lam) lam = gv;
403 }
404 if (lam > zero) {
405 const double lamd = num_traits<T>::to_double(lam);
406 const double dtd = num_traits<T>::to_double(dt);
407 const std::size_t ncell =
408 std::max<std::size_t>(1, static_cast<std::size_t>(std::ceil(lamd * dtd / opt.unifmax)));
409 const T h = dt / num_traits<T>::from_int(static_cast<int>(ncell));
410 std::vector<T> pdn(ny), pun(ny);
411 for (std::size_t y = 0; y < ny; ++y) {
412 pdn[y] = pd[y] / lam;
413 pun[y] = pu[y] / lam;
414 }
415 const T lt = lam * h;
416 for (std::size_t c = 0; c < ncell; ++c) v = iv_back_uniformize(v, pdn, pun, lt, opt);
417 iv_rescale(v);
418 }
419 for (std::size_t q = 0; q < obsIdx.size(); ++q) {
420 if (obsIdx[q] == gi) {
421 for (std::size_t y = 0; y < ny; ++y)
422 v[y] = v[y] * (obsw(q, y) > zero ? obsw(q, y) : zero);
423 iv_rescale(v);
424 }
425 }
426 for (std::size_t y = 0; y < ny; ++y) r(gi, y) = v[y];
427 }
428 return r;
429}
430
431/** Forward master equation of an inhomogeneous pure-birth process. */
432template <class T>
433Matrix<T> iv_forward(const Matrix<T>& nue, const T& dt, const VariationalOptions<T>& opt) {
434 const std::size_t G = nue.rows(), ny = nue.cols();
435 const T zero = num_traits<T>::from_int(0);
436 Matrix<T> q(G, ny, zero);
437 std::vector<T> v(ny, zero);
438 v[0] = num_traits<T>::from_int(1);
439 for (std::size_t y = 0; y < ny; ++y) q(0, y) = v[y];
440 std::vector<T> p(ny);
441 for (std::size_t g = 0; g + 1 < G; ++g) {
442 T lam = zero;
443 for (std::size_t y = 0; y < ny; ++y) {
444 p[y] = nue(g, y) > zero ? nue(g, y) : zero;
445 if (p[y] > lam) lam = p[y];
446 }
447 if (!(lam > zero)) {
448 for (std::size_t y = 0; y < ny; ++y) q(g + 1, y) = v[y];
449 continue;
450 }
451 const double lamd = num_traits<T>::to_double(lam);
452 const double dtd = num_traits<T>::to_double(dt);
453 const std::size_t ncell =
454 std::max<std::size_t>(1, static_cast<std::size_t>(std::ceil(lamd * dtd / opt.unifmax)));
455 const T h = dt / num_traits<T>::from_int(static_cast<int>(ncell));
456 std::vector<T> pn(ny);
457 for (std::size_t y = 0; y < ny; ++y) pn[y] = p[y] / lam;
458 const T lt = lam * h;
459 for (std::size_t c = 0; c < ncell; ++c) v = iv_uniformize(v, pn, lt, opt);
460 T s = zero;
461 for (std::size_t y = 0; y < ny; ++y) {
462 if (v[y] < zero) v[y] = zero;
463 s += v[y];
464 }
465 if (s > zero) {
466 for (std::size_t y = 0; y < ny; ++y) v[y] = v[y] / s;
467 }
468 for (std::size_t y = 0; y < ny; ++y) q(g + 1, y) = v[y];
469 }
470 return q;
471}
472
473/** Trapezoidal integral of a grid function. */
474template <class T>
475T iv_trapz(const std::vector<T>& f, const T& dt) {
476 if (f.size() < 2) return num_traits<T>::from_int(0);
477 T s = num_traits<T>::from_int(0);
478 for (std::size_t i = 0; i < f.size(); ++i) s += f[i];
479 const T half = num_traits<T>::from_int(1) / num_traits<T>::from_int(2);
480 return dt * (s - half * f.front() - half * f.back());
481}
482
483/** KL(Gamma(a,b) || Gamma(a0,b0)) with rate parameterisation. */
484template <class T>
485T iv_kl_gamma(const T& a, const T& b, const T& a0, const T& b0) {
486 using std::log;
487 return (a - a0) * iv_digamma(a) - iv_lgamma(a) + iv_lgamma(a0) + a0 * (log(b) - log(b0)) +
488 a * (b0 - b) / b;
489}
490
491} // namespace detail
492
493/**
494 * Run the variational inference procedure.
495 *
496 * @param spec the inference problem
497 * @param opt solver options; defaults are derived from the specification
498 * @return posterior Gamma parameters, the bound trace and the marginals
499 */
500template <class T>
503 using std::exp;
504 using std::log;
505 const T zero = num_traits<T>::from_int(0);
506 const T one = num_traits<T>::from_int(1);
507 const std::size_t M = spec.x0.rows(), R = spec.x0.cols();
508 const std::size_t narcs = spec.arcs.size(), P = spec.alpha0.size();
509 const std::size_t MR = M * R;
510 const std::size_t K = spec.obsTimes.size();
511
512 if (spec.sched.size() != M || spec.nservers.size() != M)
513 throw InputError("infer_variational: sched and nservers must have one entry per station");
514 if (spec.routeprob.size() != narcs || spec.arcparam.size() != narcs ||
515 spec.arcrate.size() != narcs)
516 throw InputError(
517 "infer_variational: routeprob, arcparam and arcrate must have one entry per "
518 "transition");
519 if (spec.alpha0.size() != spec.beta0.size())
520 throw InputError("infer_variational: alpha0 and beta0 must have the same length");
521 if (spec.obsData.rows() != K || (K > 0 && spec.obsData.cols() != MR))
522 throw InputError("infer_variational: obsData must be K x M*R");
523 if (spec.obsRange.size() != MR)
524 throw InputError("infer_variational: obsRange must have M*R entries");
525 for (std::size_t e = 0; e < narcs; ++e) {
526 if (spec.arcs[e][0] == 0 && spec.arcs[e][1] == 0)
527 throw InputError("infer_variational: a transition cannot be external at both ends");
528 if (spec.arcparam[e] == 0 && !(spec.arcrate[e] > zero))
529 throw InputError(
530 "infer_variational: a transition without a parameter needs a positive known rate");
531 }
532 if (spec.capacity.empty())
533 spec.capacity.assign(MR, num_traits<T>::from_double(std::numeric_limits<double>::infinity()));
534 if (spec.capacity.size() != MR)
535 throw InputError("infer_variational: capacity must have M*R entries");
536
537 const T unobs = VariationalSpec<T>::unobserved();
538 std::vector<std::size_t> src(narcs), dst(narcs), cls(narcs);
539 for (std::size_t e = 0; e < narcs; ++e) {
540 src[e] = spec.arcs[e][0];
541 dst[e] = spec.arcs[e][1];
542 cls[e] = spec.arcs[e][2];
543 }
544
545 Matrix<T> sgnClass(narcs, MR, zero), sgnStat(narcs, M, zero);
546 for (std::size_t e = 0; e < narcs; ++e) {
547 if (dst[e] > 0) {
548 sgnClass(e, (cls[e] - 1) * M + dst[e] - 1) += one;
549 sgnStat(e, dst[e] - 1) += one;
550 }
551 if (src[e] > 0) {
552 sgnClass(e, (cls[e] - 1) * M + src[e] - 1) -= one;
553 sgnStat(e, src[e] - 1) -= one;
554 }
555 }
556
557 std::vector<T> x0v(MR, zero), x0s(M, zero), capStat(M, zero);
558 for (std::size_t m = 0; m < M; ++m) {
559 for (std::size_t r = 0; r < R; ++r) {
560 x0v[r * M + m] = spec.x0(m, r);
561 x0s[m] += spec.x0(m, r);
562 capStat[m] += spec.capacity[r * M + m];
563 }
564 }
565
566 // mean occupancy used to size the truncation and the initial rates
567 std::vector<T> xbar(x0v), xbars(M, zero);
568 for (std::size_t k = 0; k < MR; ++k) {
569 T s = zero;
570 std::size_t n = 0;
571 for (std::size_t q = 0; q < K; ++q) {
572 if (!(spec.obsData(q, k) == unobs)) {
573 s += spec.obsData(q, k);
574 ++n;
575 }
576 }
577 if (n > 0) xbar[k] = s / num_traits<T>::from_int(static_cast<int>(n));
578 }
579 for (std::size_t m = 0; m < M; ++m)
580 for (std::size_t r = 0; r < R; ++r) xbars[m] += xbar[r * M + m];
581
582 if (opt.tmax < 0) {
583 if (K == 0) throw InputError("infer_variational: tmax is required without observations");
584 double t = 0.0;
585 for (std::size_t k = 0; k < K; ++k)
586 t = std::max(t, num_traits<T>::to_double(spec.obsTimes[k]));
587 opt.tmax = t;
588 }
589 if (!(opt.tmax > 0)) throw InputError("infer_variational: tmax must be positive");
590 if (opt.ngrid < 0 && opt.dt < 0) opt.ngrid = 201;
591 if (opt.ngrid < 0) opt.ngrid = static_cast<long>(std::llround(opt.tmax / opt.dt)) + 1;
592 opt.ngrid = std::max<long>(2, opt.ngrid);
593 opt.dt = opt.tmax / static_cast<double>(opt.ngrid - 1);
594
595 if (opt.ymax < 0) {
596 double fmax = 0.0;
597 for (std::size_t e = 0; e < narcs; ++e) {
598 double lam;
599 if (spec.arcparam[e] > 0) {
600 const std::size_t p = spec.arcparam[e] - 1;
601 lam = num_traits<T>::to_double(spec.routeprob[e] * spec.alpha0[p] / spec.beta0[p]);
602 } else {
603 lam = num_traits<T>::to_double(spec.routeprob[e] * spec.arcrate[e]);
604 }
605 double u = 1.0;
606 if (src[e] > 0) {
607 const std::size_t kc = (cls[e] - 1) * M + src[e] - 1;
608 u = num_traits<T>::to_double(detail::iv_ups(xbar[kc], xbars[src[e] - 1],
609 spec.nservers[src[e] - 1],
610 spec.sched[src[e] - 1],
611 spec.capacity[kc], capStat[src[e] - 1]));
612 }
613 fmax = std::max(fmax, lam * u * opt.tmax);
614 }
615 opt.ymax = std::max<long>(
616 20, static_cast<long>(std::ceil(2 * fmax + 5 * std::sqrt(std::max(1.0, fmax)))));
617 }
618 opt.ymax = std::max<long>(2, opt.ymax);
619 if (opt.rate_max < 0)
620 opt.rate_max = opt.rate_cap_factor * static_cast<double>(opt.ymax) / opt.tmax;
621
622 const std::size_t G = static_cast<std::size_t>(opt.ngrid);
623 const std::size_t ymax = static_cast<std::size_t>(opt.ymax);
624 const std::size_t ny = ymax + 1;
625 const std::size_t S = opt.nsamples;
626 const T dt = num_traits<T>::from_double(opt.dt);
627 const T deltaT = num_traits<T>::from_double(opt.delta);
628 const T floorT = num_traits<T>::from_double(opt.floor);
629 const T rateMaxT = num_traits<T>::from_double(opt.rate_max);
630
631 std::vector<T> yvec(ny), tgrid(G);
632 for (std::size_t y = 0; y < ny; ++y) yvec[y] = num_traits<T>::from_int(static_cast<int>(y));
633 for (std::size_t g = 0; g < G; ++g) tgrid[g] = num_traits<T>::from_int(static_cast<int>(g)) * dt;
634
635 std::vector<std::size_t> obsIdx(K, 0);
636 for (std::size_t k = 0; k < K; ++k) {
637 const long idx = std::lround(num_traits<T>::to_double(spec.obsTimes[k]) / opt.dt);
638 obsIdx[k] = static_cast<std::size_t>(std::min<long>(std::max<long>(idx, 0),
639 static_cast<long>(G) - 1));
640 }
641
642 std::vector<int> arcSched(narcs, static_cast<int>(VariationalSched::EXTERNAL));
643 std::vector<T> arcServers(narcs, one);
644 for (std::size_t e = 0; e < narcs; ++e) {
645 if (src[e] > 0) {
646 arcSched[e] = spec.sched[src[e] - 1];
647 arcServers[e] = spec.nservers[src[e] - 1];
648 }
649 }
650
651 std::vector<T> alpha(spec.alpha0), beta(spec.beta0);
652
653 std::vector<Matrix<T>> Y, nu, slack, gexp, hexp;
654 Y.reserve(narcs);
655 nu.reserve(narcs);
656 slack.reserve(narcs);
657 gexp.reserve(narcs);
658 hexp.reserve(narcs);
659
660 // E[lambda_eta] and E[log lambda_eta] under the current Gamma posterior
661 auto rate_mean = [&](std::size_t e) {
662 const std::size_t p = spec.arcparam[e];
663 if (p == 0) return spec.routeprob[e] * spec.arcrate[e];
664 return spec.routeprob[e] * alpha[p - 1] / beta[p - 1];
665 };
666 auto rate_log_mean = [&](std::size_t e) {
667 const std::size_t p = spec.arcparam[e];
668 if (p == 0) return log(spec.routeprob[e] * spec.arcrate[e]);
669 return log(spec.routeprob[e]) + detail::iv_digamma(alpha[p - 1]) - log(beta[p - 1]);
670 };
671
672 for (std::size_t e = 0; e < narcs; ++e) {
673 const T lam = rate_mean(e);
674 T u0 = one;
675 if (src[e] > 0) {
676 const std::size_t kc = (cls[e] - 1) * M + src[e] - 1;
677 u0 = detail::iv_ups(xbar[kc], xbars[src[e] - 1], arcServers[e], arcSched[e],
678 spec.capacity[kc], capStat[src[e] - 1]);
679 }
680 T nu0 = lam * u0;
681 if (nu0 < deltaT) nu0 = deltaT;
682 Matrix<T> nue(G, ny, zero);
683 for (std::size_t g = 0; g < G; ++g)
684 for (std::size_t y = 0; y < ymax; ++y) nue(g, y) = nu0;
685 nu.push_back(nue);
686 Y.push_back(detail::iv_forward(nue, dt, opt));
687 slack.push_back(Matrix<T>(G, ny, zero));
688 gexp.push_back(Matrix<T>(G, ny, zero));
689 hexp.push_back(Matrix<T>(G, ny, zero));
690 }
691
692 /**
693 * Conditional rate moments of one transition, and its observation jumps.
694 * Fills E[Xi | Y^eta=y] and exp(E[log Xi | Y^eta=y]) on the time grid,
695 * both under Q with the transition's own contribution removed.
696 */
697 auto rate_moments = [&](std::size_t e, const std::vector<Matrix<T>>& Ys, Matrix<T>& ge,
698 Matrix<T>& he, Matrix<T>& obsw, bool want_obs) {
699 const T lam = rate_mean(e);
700 const T loglam = rate_log_mean(e);
701 const bool has_origin = src[e] > 0;
702 const std::size_t kclass = has_origin ? (cls[e] - 1) * M + src[e] - 1 : 0;
703 const T sgnOwnClass = has_origin ? sgnClass(e, kclass) : zero;
704 const T sgnOwnStat = has_origin ? sgnStat(e, src[e] - 1) : zero;
705 const T d0 = deltaT / lam;
706 for (std::size_t q = 0; q < K; ++q)
707 for (std::size_t y = 0; y < ny; ++y) obsw(q, y) = one;
708 std::vector<T> accg(ny), acch(ny);
709 Matrix<T> aStore(MR, S, zero);
710 for (std::size_t g = 0; g < G; ++g) {
711 bool hasObs = false;
712 if (want_obs) {
713 for (std::size_t q = 0; q < K; ++q)
714 if (obsIdx[q] == g) hasObs = true;
715 }
716 std::fill(accg.begin(), accg.end(), zero);
717 std::fill(acch.begin(), acch.end(), zero);
718 for (std::size_t s = 0; s < S; ++s) {
719 T a = zero, b = zero;
720 if (has_origin) {
721 a = x0v[kclass];
722 b = x0s[src[e] - 1];
723 for (std::size_t f = 0; f < narcs; ++f) {
724 if (f == e) continue;
725 a += sgnClass(f, kclass) * Ys[f](g, s);
726 b += sgnStat(f, src[e] - 1) * Ys[f](g, s);
727 }
728 }
729 if (hasObs) {
730 for (std::size_t k = 0; k < MR; ++k) {
731 T acc = x0v[k];
732 for (std::size_t f = 0; f < narcs; ++f)
733 if (f != e) acc += sgnClass(f, k) * Ys[f](g, s);
734 aStore(k, s) = acc;
735 }
736 }
737 for (std::size_t y = 0; y < ny; ++y) {
738 T u = one;
739 if (has_origin)
740 u = detail::iv_ups(a + sgnOwnClass * yvec[y], b + sgnOwnStat * yvec[y],
741 arcServers[e], arcSched[e], spec.capacity[kclass],
742 capStat[src[e] - 1]);
743 accg[y] += u;
744 acch[y] += log(u + d0);
745 }
746 }
747 const T Sn = num_traits<T>::from_int(static_cast<int>(S));
748 // E[Xi] and exp(E[log Xi]) of the SAME rate Xi = delta + lam*Ups;
749 // writing the second as exp(E[log lam]) exp(E[log(Ups + delta/E[lam])])
750 // keeps the two consistent wherever Ups is deterministic, which is
751 // what stops the backward equation from developing a gradient away
752 // from the empty-station boundary
753 for (std::size_t y = 0; y < ny; ++y) {
754 ge(g, y) = deltaT + lam * (accg[y] / Sn);
755 he(g, y) = exp(loglam + acch[y] / Sn);
756 }
757 if (hasObs) {
758 for (std::size_t q = 0; q < K; ++q) {
759 if (obsIdx[q] != g) continue;
760 for (std::size_t y = 0; y < ny; ++y) {
761 T acc = zero;
762 for (std::size_t k = 0; k < MR; ++k) {
763 if (spec.obsData(q, k) == unobs) continue;
764 const T range =
765 spec.obsRange[k] > one ? spec.obsRange[k] : one;
766 T ak = zero;
767 for (std::size_t s = 0; s < S; ++s) {
768 const T x = aStore(k, s) + sgnClass(e, k) * yvec[y];
769 T p = zero;
770 if (x == spec.obsData(q, k)) {
771 p = one - spec.epsilon;
772 } else if (!(x < zero) && !(x > spec.obsRange[k])) {
773 p = spec.epsilon / range;
774 }
775 ak += log(floorT + p);
776 }
777 acc += ak / Sn;
778 }
779 obsw(q, y) = exp(acc);
780 }
781 }
782 }
783 }
784 };
785
786 std::vector<T> bound;
787 Matrix<T> alphaTrace(P, opt.iter_max, zero), betaTrace(P, opt.iter_max, zero);
788 bool converged = false;
789 std::size_t iter = 0;
790 std::vector<Matrix<T>> Ys(narcs, Matrix<T>(G, S, zero));
791 Matrix<T> ge(G, ny, zero), he(G, ny, zero), obsw(std::max<std::size_t>(K, 1), ny, zero);
792
793 for (std::size_t it = 1; it <= opt.iter_max; ++it) {
794 iter = it;
795 for (std::size_t e = 0; e < narcs; ++e) {
796 for (std::size_t f = 0; f < narcs; ++f) Ys[f] = detail::iv_sample(Y[f], S, f);
797 rate_moments(e, Ys, ge, he, obsw, true);
798 const Matrix<T> r = detail::iv_backward(ge, he, slack[e], Y[e], obsIdx, obsw, dt, opt);
799
800 // Eq. (15). A vanishing multiplier marks a count the future
801 // observations rule out; the rate there is zero, which is what
802 // keeps the forward pass from placing mass on it.
803 Matrix<T> nue(G, ny, zero), sl(G, ny, zero);
804 for (std::size_t g = 0; g < G; ++g) {
805 for (std::size_t y = 0; y < ymax; ++y) {
806 T val = zero;
807 if (r(g, y) > zero) val = he(g, y) * r(g, y + 1) / r(g, y);
808 if (!(val > zero)) val = zero;
809 if (val > rateMaxT) {
810 const T w = Y[e](g, y) > floorT ? Y[e](g, y) : floorT;
811 sl(g, y) = w * log(val / rateMaxT);
812 val = rateMaxT;
813 }
814 nue(g, y) = val;
815 }
816 }
817 nu[e] = nue;
818 slack[e] = sl;
819 Y[e] = detail::iv_forward(nue, dt, opt);
820 }
821
822 // conjugate Gamma updates: the shape gains the expected number of
823 // firings, the rate the expected exposure time of the station-class
824 // pair that the parameter governs
825 for (std::size_t f = 0; f < narcs; ++f) Ys[f] = detail::iv_sample(Y[f], S, f);
826 std::vector<T> firings(P, zero), exposure(P, zero);
827 std::vector<char> seen(P * MR, 0);
828 for (std::size_t e = 0; e < narcs; ++e) {
829 const std::size_t p = spec.arcparam[e];
830 if (p == 0) continue;
831 // expected number of firings over the horizon, taken from the
832 // marginal itself, which is exact, rather than by quadrature of
833 // the intensity, which a near-deterministic marginal makes
834 // inaccurate
835 T m1 = zero, m0 = zero;
836 for (std::size_t y = 0; y < ny; ++y) {
837 m1 += Y[e](G - 1, y) * yvec[y];
838 m0 += Y[e](0, y) * yvec[y];
839 }
840 firings[p - 1] += m1 - m0;
841 const std::size_t kclass = (cls[e] - 1) * M + src[e] - 1;
842 if (!seen[(p - 1) * MR + kclass]) {
843 seen[(p - 1) * MR + kclass] = 1;
844 std::vector<T> ue(G, zero);
845 const T Sn = num_traits<T>::from_int(static_cast<int>(S));
846 for (std::size_t g = 0; g < G; ++g) {
847 T acc = zero;
848 for (std::size_t s = 0; s < S; ++s) {
849 T a = x0v[kclass], b = x0s[src[e] - 1];
850 for (std::size_t f = 0; f < narcs; ++f) {
851 a += sgnClass(f, kclass) * Ys[f](g, s);
852 b += sgnStat(f, src[e] - 1) * Ys[f](g, s);
853 }
854 acc += detail::iv_ups(a, b, spec.nservers[src[e] - 1],
855 spec.sched[src[e] - 1], spec.capacity[kclass],
856 capStat[src[e] - 1]);
857 }
858 ue[g] = acc / Sn;
859 }
860 exposure[p - 1] += detail::iv_trapz(ue, dt);
861 }
862 }
863 for (std::size_t p = 0; p < P; ++p) {
864 alpha[p] = spec.alpha0[p] + firings[p];
865 beta[p] = spec.beta0[p] + exposure[p];
866 alphaTrace(p, it - 1) = alpha[p];
867 betaTrace(p, it - 1) = beta[p];
868 }
869
870 // the bound is evaluated at the state the iteration ended in, so the
871 // rate moments are recomputed against the updated marginals rather
872 // than reused from the sweep that produced them
873 for (std::size_t e = 0; e < narcs; ++e) {
874 rate_moments(e, Ys, ge, he, obsw, false);
875 gexp[e] = ge;
876 hexp[e] = he;
877 }
878
879 // evidence lower bound: path term, observation term and the divergence
880 // of the rate posteriors from their priors
881 T b = zero;
882 {
883 std::vector<T> acc(G, zero);
884 for (std::size_t e = 0; e < narcs; ++e) {
885 for (std::size_t g = 0; g < G; ++g) {
886 T s = zero;
887 for (std::size_t y = 0; y < ny; ++y) {
888 const T n = nu[e](g, y);
889 T term = n - gexp[e](g, y);
890 if (n > zero) {
891 const T hh = hexp[e](g, y) > floorT ? hexp[e](g, y) : floorT;
892 term -= n * log(n / hh);
893 }
894 s += Y[e](g, y) * term;
895 }
896 acc[g] = s;
897 }
898 b += detail::iv_trapz(acc, dt);
899 }
900 const T Sn = num_traits<T>::from_int(static_cast<int>(S));
901 for (std::size_t k = 0; k < K; ++k) {
902 const std::size_t g = obsIdx[k];
903 T tot = zero;
904 for (std::size_t s = 0; s < S; ++s) {
905 T a = zero;
906 for (std::size_t j = 0; j < MR; ++j) {
907 if (spec.obsData(k, j) == unobs) continue;
908 T x = x0v[j];
909 for (std::size_t f = 0; f < narcs; ++f) x += sgnClass(f, j) * Ys[f](g, s);
910 T p = zero;
911 if (x == spec.obsData(k, j)) {
912 p = one - spec.epsilon;
913 } else if (!(x < zero) && !(x > spec.obsRange[j])) {
914 p = spec.epsilon / (spec.obsRange[j] > one ? spec.obsRange[j] : one);
915 }
916 a += log(floorT + p);
917 }
918 tot += a;
919 }
920 b += tot / Sn;
921 }
922 for (std::size_t p = 0; p < P; ++p)
923 b -= detail::iv_kl_gamma(alpha[p], beta[p], spec.alpha0[p], spec.beta0[p]);
924 }
925 bound.push_back(b);
926
927 // The rate update solves a stationarity condition rather than
928 // maximising the bound in a block, so the bound need not ascend;
929 // convergence is judged on the bound AND on the rate posteriors.
930 if (it > 1) {
931 const double prevb = num_traits<T>::to_double(bound[it - 2]);
932 double crit = std::abs(num_traits<T>::to_double(b) - prevb) / std::max(1.0, std::abs(prevb));
933 for (std::size_t p = 0; p < P; ++p) {
934 const double prev = num_traits<T>::to_double(alphaTrace(p, it - 2) /
935 betaTrace(p, it - 2));
936 const double cur = num_traits<T>::to_double(alpha[p] / beta[p]);
937 crit = std::max(crit, std::abs(cur - prev) / std::max(1e-12, prev));
938 }
939 if (crit <= opt.tol) {
940 converged = true;
941 break;
942 }
943 }
944 }
945
947 out.alpha = alpha;
948 out.beta = beta;
949 out.rates.resize(P);
950 out.mean_service_time.resize(P);
951 for (std::size_t p = 0; p < P; ++p) {
952 out.rates[p] = alpha[p] / beta[p];
953 out.mean_service_time[p] = beta[p] / alpha[p];
954 }
955 out.bound = bound;
956 out.alpha_trace = Matrix<T>(P, iter, zero);
957 out.beta_trace = Matrix<T>(P, iter, zero);
958 for (std::size_t p = 0; p < P; ++p) {
959 for (std::size_t i = 0; i < iter; ++i) {
960 out.alpha_trace(p, i) = alphaTrace(p, i);
961 out.beta_trace(p, i) = betaTrace(p, i);
962 }
963 }
964 out.Y = Y;
965 out.nu = nu;
966 out.tgrid = tgrid;
967 out.iter = iter;
968 out.converged = converged;
969
970 T tailmass = zero;
971 for (std::size_t e = 0; e < narcs; ++e)
972 for (std::size_t g = 0; g < G; ++g)
973 if (Y[e](g, ny - 1) > tailmass) tailmass = Y[e](g, ny - 1);
974 out.tailmass = tailmass;
975
976 out.qlen = Matrix<T>(G, MR, zero);
977 for (std::size_t g = 0; g < G; ++g)
978 for (std::size_t k = 0; k < MR; ++k) out.qlen(g, k) = x0v[k];
979 for (std::size_t e = 0; e < narcs; ++e) {
980 for (std::size_t g = 0; g < G; ++g) {
981 T my = zero;
982 for (std::size_t y = 0; y < ny; ++y) my += Y[e](g, y) * yvec[y];
983 for (std::size_t k = 0; k < MR; ++k) out.qlen(g, k) += my * sgnClass(e, k);
984 }
985 }
986 return out;
987}
988
989} // namespace infer
990} // namespace line
991
992#endif // LINE_API_INFER_INFER_VARIATIONAL_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
Dense matrix and non-owning view.
VariationalResult< T > infer_variational(VariationalSpec< T > spec, VariationalOptions< T > opt=VariationalOptions< T >())
Run the variational inference procedure.
VariationalSched
Service discipline of a station, as seen by the load factor Upsilon.
Number-type abstraction for the templated API port.
Options of infer_variational; a negative box means "derive a default".
double delta
rate added to every feasible transition by the space expansion.
double rate_max
cap on the variational rates; derived from ymax and tmax when negative.
Outcome of infer_variational.
std::vector< Matrix< T > > nu
std::vector< Matrix< T > > Y
Inference problem handed to infer_variational.
std::vector< T > obsRange
(M*R) support size of the uniform contamination.
std::vector< T > arcrate
(T) known rate for transitions with arcparam == 0.
std::vector< T > capacity
(M*R) upper bound on the queue length, empty for none.
std::vector< std::size_t > arcparam
(T) index in 1..P of the rate governing the transition, 0 when known.
Matrix< T > x0
(M x R) initial queue lengths.
Matrix< T > obsData
(K x M*R) observed queue lengths; an unobserved entry is unobserved.
static T unobserved()
Sentinel marking an unobserved entry of obsData.
std::vector< std::array< std::size_t, 3 > > arcs
(T x 3) transitions [i j c]; i==0 external source, j==0 sink.
std::vector< int > sched
(M) discipline codes.
std::vector< T > obsTimes
(K) observation epochs.
std::vector< T > alpha0
(P) Gamma prior shapes and rates.
std::vector< T > nservers
(M) number of servers.
std::vector< T > routeprob
(T) routing probability of each transition.
T epsilon
probability that a reading is faulty.