LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_mcmc.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_PFQN_MCMC_H
6#define LINE_API_PFQN_MCMC_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Chen-O'Cinneide REGULARIZATION: a Markov chain Monte Carlo estimator of the
12 * class throughputs X(r) = G(N-e_r)/G(N) and of the mean queue lengths Q(i,r) of
13 * a CLOSED multiclass product-form (BCMP, no type changes) network.
14 *
15 * W. Chen, C. A. O'Cinneide, "Towards a Polynomial-Time Randomized Algorithm
16 * for Closed Product-Form Networks", ACM TOMACS 8(3):227-253, 1998.
17 *
18 * Templated port of matlab/src/api/pfqn/pfqn_mcmc.m, cross-checked against
19 * jar/src/main/java/jline/api/pfqn/nc/Pfqn_mcmc.java.
20 *
21 * The three steps of the paper are:
22 *
23 * I. CONSTRUCT THE REGULARIZED NETWORK. Write rho(i,r) for the surrogate
24 * traffic intensity of class r at station i -- here the service demand,
25 * since rho = lambda/mu is a visit ratio over a service rate -- and
26 * rho(r) = sum_i rho(i,r). The regularized network has the same stations,
27 * classes and populations, UNIT service rates at every station, the
28 * processor-sharing discipline, and a routing matrix that depends on the
29 * destination only,
30 *
31 * P*(i->m | class r) = rho(m,r)/rho(r).
32 *
33 * By Theorem 2.1 it is a REVERSIBLE chain with the SAME steady-state
34 * distribution as the original network, and its throughputs satisfy
35 * Theta*(r) = rho(r)*Theta(r).
36 *
37 * II. SIMULATE IT at service-completion epochs. With Y(i,r) the number of
38 * class-r jobs at station i, Y(i) their total and Psi_i(k) = min(s_i,k) the
39 * number of busy servers,
40 *
41 * r(i,r) = Y(i,r)/Y(i) * Psi_i(Y(i)), r(r) = sum_i r(i,r),
42 * r = sum_i Psi_i(Y(i)),
43 *
44 * the next completion is of class r at station i with probability
45 * r(i,r)/r, and the conditional expected time to it is 1/r. Equation (10)
46 * of the paper is the holding-time weighted ratio estimator
47 *
48 * Theta*(r) = sum_t r(r,t)/r(t) / sum_t 1/r(t),
49 *
50 * and the same weights give the time-average queue lengths, which need no
51 * transformation at all because the two networks share their steady state.
52 *
53 * III. TRANSFORM BACK: X(r) = Theta*(r)/rho(r).
54 *
55 * Because P* forgets the station of origin and every station serves at unit
56 * rate, the regularized chain has neither the slowly mixing routing chain nor
57 * the customer-trapping slow station that make the original chain converge
58 * slowly. The paper proves O(N^2*M^3) mixing in two special cases (Section 4)
59 * and reports the general behaviour experimentally (Section 5).
60 *
61 * Delay (infinite-server) demand enters as ONE extra station with s = infinity
62 * and demand Z. Aggregating infinite-server stations that way is exact in the
63 * product form, since their joint term is multinomial in the per-class totals.
64 *
65 * THERE IS NO NORMALIZING CONSTANT HERE, and that is the single most important
66 * thing to know about the method: the estimator is a ratio, G itself never
67 * appears. `pfqn_nc`'s Mcmc arm supplies an BLE lG alongside, which is not
68 * part of the paper and cancels out of every mean value.
69 *
70 * Confidence: the run is split into non-overlapping batches (Schmeiser 1982, 30
71 * by default, the count used in the tables of the paper), the batch means of the
72 * ratio estimator give a standard error, and the intervals are the paper's
73 * two-sigma ones. The estimator is a ratio of correlated averages, so it carries
74 * an O(1/samples) bias on top of the initialization bias; the paper ignores
75 * both, this port additionally discards a warm-up fraction (10% by default).
76 *
77 * Arithmetic: INEXACT BY CONSTRUCTION. The value is a random variable and the
78 * batch-means interval needs a square root, so the estimator is meaningless --
79 * not merely inaccurate -- in an exact field, exactly as the rest of the
80 * `pfqn_nc` estimator ladder is.
81 *
82 * RNG contract: see pfqn_mc_common.h. Comparable to MATLAB only in
83 * distribution, never stream for stream; reproducible within this port only
84 * when the generator is passed in the same state.
85 */
86
87#include <algorithm>
88#include <cmath>
89#include <cstddef>
90#include <limits>
91#include <vector>
92
94#include "line/num/number.h"
95#include "line/util/error.h"
96#include "line/util/matrix.h"
97
98namespace line {
99namespace pfqn {
100
101namespace detail {
102
103/** Sample standard deviation over the batch means, normalized by n-1 as MATLAB's std is. */
104inline double mcmc_sample_std(const std::vector<double>& v) {
105 const std::size_t n = v.size();
106 if (n < 2) return 0.0;
107 double mean = 0.0;
108 for (std::size_t i = 0; i < n; ++i) mean += v[i];
109 mean /= static_cast<double>(n);
110 double acc = 0.0;
111 for (std::size_t i = 0; i < n; ++i) {
112 const double d = v[i] - mean;
113 acc += d * d;
114 }
115 return std::sqrt(acc / static_cast<double>(n - 1));
116}
117
118} // namespace detail
119
120/** Estimates of pfqn_mcmc together with their batch-means intervals. */
121template <class T>
123 std::vector<T> X; ///< (R) throughput estimates G(N-e_r)/G(N)
124 Matrix<T> Q; ///< (M x R) mean queue lengths at the queueing stations
125 std::vector<T> Xse; ///< (R) batch-means standard error of X
126 std::vector<T> Xlo; ///< (R) lower end of the two-sigma interval for X
127 std::vector<T> Xhi; ///< (R) upper end of the two-sigma interval for X
128 Matrix<T> Qse; ///< (M x R) batch-means standard error of Q
129 Matrix<T> Qlo; ///< (M x R) lower end of the two-sigma interval for Q
130 Matrix<T> Qhi; ///< (M x R) upper end of the two-sigma interval for Q
131 std::size_t batches = 0; ///< batches the run was split into
132 std::size_t samples = 0; ///< completions simulated after warm-up
133 std::size_t burnin = 0; ///< completions discarded as warm-up
134};
135
136/** Schmeiser (1982), the batch count used in the tables of the paper. */
137inline constexpr std::size_t MCMC_DEFAULT_BATCHES = 30;
138/** Warm-up fraction discarded before accumulation starts. */
139inline constexpr double MCMC_DEFAULT_BURNIN = 0.1;
140
141/**
142 * @brief Chen-O'Cinneide REGULARIZATION: a Markov chain Monte Carlo estimator
143 * of the class throughputs X(r) = G(N-e_r)/G(N) and of the mean queue
144 * lengths Q(i,r) of a CLOSED multiclass product-form (BCMP, no type
145 * changes) network.
146 *
147 * @param L (M x R) per-class service demands at the M queueing stations
148 * @param N (R) closed population vector; finite and integer
149 * @param Z (R) aggregated think times; empty for a model with no delay
150 * @param s (M) servers per station, infinite for an infinite server;
151 * empty means all stations single-server
152 * @param samples service completions to simulate after warm-up
153 * @param nbatches batches the run is split into for the confidence intervals
154 * @param burnin warm-up fraction discarded before accumulation starts
155 * @param rng explicit generator, advanced by the call
156 */
157template <class T>
158McmcResult<T> pfqn_mcmc(const Matrix<T>& L, const std::vector<int>& N, const std::vector<T>& Z,
159 const std::vector<double>& s, std::size_t samples, std::size_t nbatches,
160 double burnin, McRng& rng) {
162 "pfqn_mcmc requires transcendental arithmetic: it is a Monte Carlo estimator "
163 "whose value is a random variable and whose batch-means interval needs a "
164 "square root");
165
166 const std::size_t M = L.empty() ? 0 : L.rows();
167 const std::size_t R = N.size();
168 if (!L.empty() && L.cols() != R)
169 throw InputError("pfqn_mcmc: L and N disagree on the class count");
170 if (!Z.empty() && Z.size() != R) throw InputError("pfqn_mcmc: Z has the wrong length");
171 if (!s.empty() && s.size() != M)
172 throw InputError("pfqn_mcmc: the server count vector has the wrong length");
173
174 const T zero = num_traits<T>::from_int(0);
175 McmcResult<T> res;
176 res.X.assign(R, zero);
177 res.Xse.assign(R, zero);
178 res.Xlo.assign(R, zero);
179 res.Xhi.assign(R, zero);
180 res.Q = Matrix<T>(M, R, zero);
181 res.Qse = Matrix<T>(M, R, zero);
182 res.Qlo = Matrix<T>(M, R, zero);
183 res.Qhi = Matrix<T>(M, R, zero);
184
185 long Ntot = 0;
186 for (std::size_t r = 0; r < R; ++r) {
187 // The chain lives on the integer lattice sum_i Y(i,r) = N(r); the caller has
188 // already resolved the population to an int, so a fractional one cannot reach
189 // here, but a NEGATIVE entry is the open-class marker and has no state space.
190 if (N[r] < 0)
191 throw InputError("pfqn_mcmc requires a closed model, but the population vector "
192 "marks an open class");
193 Ntot += N[r];
194 }
195 if (Ntot == 0) return res;
196
197 // ---- Step I: the regularized network ---------------------------------------------
198 // Only the surrogate traffic intensities rho(i,r) enter the product form, and scaling
199 // a whole class column by a constant leaves the steady-state distribution unchanged,
200 // so the demands are used as they are.
201 bool hasDelay = false;
202 for (std::size_t r = 0; r < R && !Z.empty(); ++r)
203 if (num_traits<T>::to_double(Z[r]) > 0.0) hasDelay = true;
204 const std::size_t Mx = hasDelay ? M + 1 : M;
205
206 Matrix<T> rho(Mx, R, zero);
207 std::vector<double> svec(Mx, 1.0);
208 for (std::size_t i = 0; i < M; ++i) {
209 svec[i] = s.empty() ? 1.0 : s[i];
210 for (std::size_t r = 0; r < R; ++r) {
211 const double v = num_traits<T>::to_double(L(i, r));
212 rho(i, r) = (std::isfinite(v) && v > 0.0) ? L(i, r) : zero;
213 }
214 }
215 if (hasDelay) {
216 svec[M] = std::numeric_limits<double>::infinity();
217 for (std::size_t r = 0; r < R; ++r) rho(M, r) = Z[r];
218 }
219
220 std::vector<T> rhoTot(R, zero);
221 for (std::size_t r = 0; r < R; ++r) {
222 for (std::size_t i = 0; i < Mx; ++i) rhoTot[r] += rho(i, r);
223 if (N[r] > 0 && num_traits<T>::to_double(rhoTot[r]) <= 0.0)
224 throw InputError("pfqn_mcmc: a class has a positive population but no demand "
225 "anywhere in the network");
226 }
227
228 // Routing of the regularized network, P*(m|r) = rho(m,r)/rho(r), held as one column of
229 // cumulative probabilities per class.
230 Matrix<double> Pstar(Mx, R, 0.0);
231 Matrix<double> cumP(Mx, R, 0.0);
232 for (std::size_t r = 0; r < R; ++r) {
233 const double den = std::max(num_traits<T>::to_double(rhoTot[r]),
234 std::numeric_limits<double>::min());
235 double acc = 0.0;
236 for (std::size_t i = 0; i < Mx; ++i) {
237 Pstar(i, r) = num_traits<T>::to_double(rho(i, r)) / den;
238 acc += Pstar(i, r);
239 cumP(i, r) = acc;
240 }
241 cumP(Mx - 1, r) = 1.0; // guard the last bin against a floating-point shortfall
242 }
243
244 if (nbatches == 0) nbatches = 1;
245 if (samples == 0) samples = 1;
246 if (!(burnin >= 0.0)) burnin = 0.0;
247 if (burnin > 0.9) burnin = 0.9;
248 const std::size_t batchLen = std::max<std::size_t>(1, samples / nbatches);
249 samples = batchLen * nbatches;
250 const std::size_t nburn = static_cast<std::size_t>(std::llround(burnin * samples));
251
252 // Initial state: spread each class over the stations it can occupy in the proportions
253 // P*(.|r), by largest remainder. That is the marginal the regularized network would
254 // have with no queueing, so it costs nothing and starts the chain far closer to
255 // stationarity than a single-station state.
256 std::vector<std::vector<long> > Y(Mx, std::vector<long>(R, 0));
257 std::vector<double> Ytot(Mx, 0.0);
258 for (std::size_t r = 0; r < R; ++r) {
259 if (N[r] == 0) continue;
260 std::vector<double> target(Mx, 0.0);
261 long placed = 0;
262 for (std::size_t i = 0; i < Mx; ++i) {
263 target[i] = N[r] * Pstar(i, r);
264 Y[i][r] = static_cast<long>(std::floor(target[i]));
265 placed += Y[i][r];
266 }
267 // Largest remainder: hand each remaining unit to the biggest residue still
268 // outstanding. Once a station is topped up its residue target-Y drops by one, so
269 // it cannot win twice, which is the 'descend' sort of the reference.
270 for (long k = 0; k < N[r] - placed; ++k) {
271 std::size_t best = 0;
272 double bestRem = -std::numeric_limits<double>::infinity();
273 for (std::size_t i = 0; i < Mx; ++i) {
274 const double rem = target[i] - static_cast<double>(Y[i][r]);
275 if (rem > bestRem) {
276 bestRem = rem;
277 best = i;
278 }
279 }
280 ++Y[best][r];
281 }
282 }
283 for (std::size_t i = 0; i < Mx; ++i) {
284 double tot = 0.0;
285 for (std::size_t r = 0; r < R; ++r) tot += static_cast<double>(Y[i][r]);
286 Ytot[i] = tot;
287 }
288
289 // ---- Step II: simulate at service-completion epochs -------------------------------
290 Matrix<double> xnum(nbatches, R, 0.0); // sum_t r(r,t)/r(t) within the batch
291 std::vector<Matrix<double> > qnum(nbatches, Matrix<double>(Mx, R, 0.0));
292 std::vector<double> den(nbatches, 0.0); // sum_t 1/r(t) within the batch
293 std::vector<double> Psi(Mx, 0.0), cPsi(Mx, 0.0), rvec(R, 0.0), cY(R, 0.0);
294 double w = 0.0;
295 bool stale = true;
296 const std::size_t horizon = nburn + samples;
297 for (std::size_t t = 1; t <= horizon; ++t) {
298 if (stale) {
299 // (8)-(9): busy servers, per-class completion rates, total rate
300 double totPsi = 0.0;
301 for (std::size_t i = 0; i < Mx; ++i) {
302 Psi[i] = std::min(svec[i], Ytot[i]);
303 totPsi += Psi[i];
304 cPsi[i] = totPsi;
305 }
306 for (std::size_t r = 0; r < R; ++r) rvec[r] = 0.0;
307 for (std::size_t i = 0; i < Mx; ++i) {
308 if (Ytot[i] <= 0.0) continue;
309 const double rw = Psi[i] / Ytot[i];
310 for (std::size_t r = 0; r < R; ++r)
311 if (Y[i][r] != 0) rvec[r] += rw * static_cast<double>(Y[i][r]);
312 }
313 w = 1.0 / totPsi;
314 stale = false;
315 }
316 if (t > nburn) {
317 const std::size_t b = (t - nburn - 1) / batchLen;
318 den[b] += w;
319 for (std::size_t r = 0; r < R; ++r) xnum(b, r) += w * rvec[r];
320 Matrix<double>& qb = qnum[b];
321 for (std::size_t i = 0; i < Mx; ++i)
322 for (std::size_t r = 0; r < R; ++r)
323 if (Y[i][r] != 0) qb(i, r) += w * static_cast<double>(Y[i][r]);
324 }
325 // Pick the completing station with probability Psi(i)/r, then the completing class
326 // within it with probability Y(i,r)/Y(i); the product is the r(i,r)/r of the
327 // paper, since sum_r Y(i,r)/Y(i)*Psi(i) = Psi(i).
328 std::size_t i = Mx;
329 {
330 const double u = mc_uniform01(rng) * cPsi[Mx - 1];
331 for (std::size_t k = 0; k < Mx; ++k)
332 if (cPsi[k] >= u) {
333 i = k;
334 break;
335 }
336 if (i == Mx)
337 for (std::size_t k = Mx; k-- > 0;)
338 if (Psi[k] > 0.0) {
339 i = k;
340 break;
341 }
342 }
343 double acc = 0.0;
344 for (std::size_t r = 0; r < R; ++r) {
345 acc += static_cast<double>(Y[i][r]);
346 cY[r] = acc;
347 }
348 std::size_t cls = R;
349 {
350 const double u = mc_uniform01(rng) * cY[R - 1];
351 for (std::size_t k = 0; k < R; ++k)
352 if (cY[k] >= u) {
353 cls = k;
354 break;
355 }
356 if (cls == R)
357 for (std::size_t k = R; k-- > 0;)
358 if (Y[i][k] > 0) {
359 cls = k;
360 break;
361 }
362 }
363 // Route it. A self-transition leaves the state, hence the rates and the weight,
364 // unchanged: skipping the recomputation is the saving described at the end of
365 // Section 2 of the paper.
366 const double u = mc_uniform01(rng);
367 std::size_t m = Mx;
368 for (std::size_t k = 0; k < Mx; ++k)
369 if (cumP(k, cls) >= u) {
370 m = k;
371 break;
372 }
373 if (m < Mx && m != i) {
374 --Y[i][cls];
375 ++Y[m][cls];
376 Ytot[i] -= 1.0;
377 Ytot[m] += 1.0;
378 stale = true;
379 }
380 }
381
382 // ---- Step III: back to the original network ---------------------------------------
383 // Theta(r) = Theta*(r)/rho(r) by (7) and (11); the queue lengths transfer unchanged,
384 // the two networks sharing their steady-state distribution.
385 double denTot = 0.0;
386 for (std::size_t b = 0; b < nbatches; ++b) denTot += den[b];
387 for (std::size_t r = 0; r < R; ++r) {
388 double num = 0.0;
389 for (std::size_t b = 0; b < nbatches; ++b) num += xnum(b, r);
390 res.X[r] = num_traits<T>::from_double((num / denTot) /
391 num_traits<T>::to_double(rhoTot[r]));
392 }
393 for (std::size_t i = 0; i < M; ++i)
394 for (std::size_t r = 0; r < R; ++r) {
395 double num = 0.0;
396 for (std::size_t b = 0; b < nbatches; ++b) num += qnum[b](i, r);
397 res.Q(i, r) = num_traits<T>::from_double(num / denTot);
398 }
399
400 // Batch-means standard error and the two-sigma interval of the paper.
401 if (nbatches > 1) {
402 std::vector<double> v(nbatches, 0.0);
403 for (std::size_t r = 0; r < R; ++r) {
404 const double rt = num_traits<T>::to_double(rhoTot[r]);
405 for (std::size_t b = 0; b < nbatches; ++b) v[b] = (xnum(b, r) / den[b]) / rt;
406 res.Xse[r] = num_traits<T>::from_double(detail::mcmc_sample_std(v) /
407 std::sqrt(static_cast<double>(nbatches)));
408 }
409 for (std::size_t i = 0; i < M; ++i)
410 for (std::size_t r = 0; r < R; ++r) {
411 for (std::size_t b = 0; b < nbatches; ++b) v[b] = qnum[b](i, r) / den[b];
413 detail::mcmc_sample_std(v) / std::sqrt(static_cast<double>(nbatches)));
414 }
415 }
416 const T two = num_traits<T>::from_int(2);
417 for (std::size_t r = 0; r < R; ++r) {
418 res.Xlo[r] = T(res.X[r] - two * res.Xse[r]);
419 res.Xhi[r] = T(res.X[r] + two * res.Xse[r]);
420 }
421 for (std::size_t i = 0; i < M; ++i)
422 for (std::size_t r = 0; r < R; ++r) {
423 res.Qlo(i, r) = T(res.Q(i, r) - two * res.Qse(i, r));
424 res.Qhi(i, r) = T(res.Q(i, r) + two * res.Qse(i, r));
425 }
426 res.batches = nbatches;
427 res.samples = samples;
428 res.burnin = nburn;
429 return res;
430}
431
432} // namespace pfqn
433} // namespace line
434
435#endif // LINE_API_PFQN_MCMC_H
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
bool empty() const
Definition matrix.h:92
The exception types the port throws.
Dense matrix and non-owning view.
std::mt19937_64 McRng
The generator type every Monte Carlo entry point in this tree accepts.
double mc_uniform01(McRng &g)
Uniform deviate on [0,1) with 53 significant bits, as a double.
constexpr double MCMC_DEFAULT_BURNIN
Warm-up fraction discarded before accumulation starts.
Definition pfqn_mcmc.h:139
constexpr std::size_t MCMC_DEFAULT_BATCHES
Schmeiser (1982), the batch count used in the tables of the paper.
Definition pfqn_mcmc.h:137
McmcResult< T > pfqn_mcmc(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const std::vector< double > &s, std::size_t samples, std::size_t nbatches, double burnin, McRng &rng)
Chen-O'Cinneide REGULARIZATION: a Markov chain Monte Carlo estimator of the class throughputs X(r) = ...
Definition pfqn_mcmc.h:158
Number-type abstraction for the templated API port.
Randomness scaffolding shared by the Monte Carlo normalizing-constant estimators (pfqn_mci,...
Estimates of pfqn_mcmc together with their batch-means intervals.
Definition pfqn_mcmc.h:122
std::size_t batches
batches the run was split into
Definition pfqn_mcmc.h:131
Matrix< T > Qse
(M x R) batch-means standard error of Q
Definition pfqn_mcmc.h:128
std::vector< T > Xlo
(R) lower end of the two-sigma interval for X
Definition pfqn_mcmc.h:126
std::vector< T > X
(R) throughput estimates G(N-e_r)/G(N)
Definition pfqn_mcmc.h:123
std::size_t burnin
completions discarded as warm-up
Definition pfqn_mcmc.h:133
std::vector< T > Xse
(R) batch-means standard error of X
Definition pfqn_mcmc.h:125
Matrix< T > Qlo
(M x R) lower end of the two-sigma interval for Q
Definition pfqn_mcmc.h:129
std::vector< T > Xhi
(R) upper end of the two-sigma interval for X
Definition pfqn_mcmc.h:127
std::size_t samples
completions simulated after warm-up
Definition pfqn_mcmc.h:132
Matrix< T > Qhi
(M x R) upper end of the two-sigma interval for Q
Definition pfqn_mcmc.h:130
Matrix< T > Q
(M x R) mean queue lengths at the queueing stations
Definition pfqn_mcmc.h:124