LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_cftp.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_CFTP_H
6#define LINE_API_PFQN_CFTP_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Perfect stationary state sampling for closed single-class multiserver
12 * product-form networks, by monotone Coupling From The Past.
13 *
14 * Templated port of matlab/src/api/pfqn/pfqn_cftp.m, cross-checked against
15 * jar/src/main/java/jline/api/pfqn/Pfqn_cftp.java. Reference: S. Kijima and
16 * T. Matsui, "Approximate/Perfect Samplers for Closed Jackson Networks",
17 * Winter Simulation Conference 2005.
18 *
19 * The chain moves one adjacent station pair at a time. A single uniform u
20 * encodes both the pair, through lam = 1 + u (M-1) and j = floor(lam), and the
21 * split of the pair's combined occupancy k = x_j + x_{j+1}, through the
22 * fractional part Lambda used as an inverse-CDF argument against
23 *
24 * w(s) proportional to alpha_j(s) alpha_{j+1}(k-s),
25 * log alpha_i(m) = m log L_i - sum_{t=1}^{m} log min(t, S_i).
26 *
27 * That update is monotone with respect to the componentwise partial order on
28 * the population simplex, so running the top state (K,0,...,0) and the bottom
29 * state (0,...,0,K) from -T with a FIXED randomness tape and doubling T until
30 * they coalesce returns a draw from the exact stationary distribution
31 * (Propp-Wilson). The randomness for the steps already simulated must be
32 * REUSED as T doubles, which is why the tape grows at its far end and is
33 * replayed oldest first; re-drawing it would destroy the perfection guarantee.
34 *
35 * The 'approx' method is the rapidly-mixing sampler M_A of the same paper: a
36 * fixed number ceil(M(M-1)/2 log(K/eps)) of updates on uniformly random
37 * DISTINCT (not necessarily adjacent) pairs, from an arbitrary feasible start.
38 * It is not exact; the reference offers it because its running time is
39 * deterministic.
40 *
41 * Log-space weights are kept in double, as in the reference: the split CDF is
42 * compared against a double uniform, so carrying the weights at a higher
43 * precision cannot change the sampled state. The reported mean queue length is
44 * accumulated in the working arithmetic.
45 *
46 * Arithmetic: INEXACT BY CONSTRUCTION. The output is a random state; the
47 * balance functions are formed in the log domain.
48 *
49 * RNG contract: see pfqn_mc_common.h. Comparable to MATLAB only in
50 * distribution, never stream for stream; reproducible within this port only
51 * when the generator is passed in the same state.
52 */
53
54#include <cmath>
55#include <cstddef>
56#include <vector>
57
59#include "line/num/number.h"
60#include "line/util/error.h"
61#include "line/util/matrix.h"
62
63namespace line {
64namespace pfqn {
65
66/** Sentinel for an infinite-server (delay) station, the reference's S = Inf. */
67constexpr int cftp_inf_servers = -1;
68
69/** Which sampler to run. */
70enum class CftpMethod {
71 Cftp, ///< exact, monotone coupling from the past
72 Approx ///< the rapidly-mixing approximate sampler M_A
73};
74
75/** Return value of pfqn_cftp, mirroring [Q, X, T]. */
76template <class T>
77struct CftpResult {
78 std::vector<T> Q; ///< (M) empirical mean queue length
79 Matrix<int> X; ///< (nsamples x M) sampled states, rows sum to K
80 std::vector<long> horizon; ///< (nsamples) coalescence horizon or step count
81};
82
83namespace detail {
84
85/**
86 * Inverse-CDF split of k jobs between stations i and j: the smallest s with
87 * Lambda <= cdf(s). Weights are normalized by their maximum before
88 * exponentiating, exactly as the reference does.
89 */
90inline int cftp_split_index(const std::vector<double>& logL, const Matrix<double>& logfac,
91 std::size_t i, std::size_t j, int k, double Lambda) {
92 std::vector<double> lw(static_cast<std::size_t>(k) + 1);
93 double m = -std::numeric_limits<double>::infinity();
94 for (int s = 0; s <= k; ++s) {
95 const double val = s * logL[i] - logfac(i, static_cast<std::size_t>(s)) +
96 (k - s) * logL[j] - logfac(j, static_cast<std::size_t>(k - s));
97 lw[static_cast<std::size_t>(s)] = val;
98 if (val > m) m = val;
99 }
100 double tot = 0.0;
101 for (int s = 0; s <= k; ++s) {
102 lw[static_cast<std::size_t>(s)] = std::exp(lw[static_cast<std::size_t>(s)] - m);
103 tot += lw[static_cast<std::size_t>(s)];
104 }
105 double c = 0.0;
106 for (int s = 0; s <= k; ++s) {
107 c += lw[static_cast<std::size_t>(s)] / tot;
108 if (Lambda <= c) return s;
109 }
110 return k;
111}
112
113/** One monotone update of an adjacent pair, driven by a single uniform. */
114inline void cftp_monotone_update(std::vector<int>& x, double u, const std::vector<double>& logL,
115 const Matrix<double>& logfac, std::size_t M) {
116 const double lam = 1.0 + u * static_cast<double>(M - 1);
117 std::size_t j = static_cast<std::size_t>(std::floor(lam));
118 if (j > M - 1) j = M - 1;
119 if (j < 1) j = 1;
120 const double Lambda = lam - static_cast<double>(j);
121 const std::size_t a = j - 1, b = j; // 0-based pair (j, j+1)
122 const int k = x[a] + x[b];
123 const int l = cftp_split_index(logL, logfac, a, b, k, Lambda);
124 x[a] = l;
125 x[b] = k - l;
126}
127
128} // namespace detail
129
130/**
131 * @brief Perfect stationary state sampling for closed single-class
132 * multiserver product-form networks, by monotone Coupling From The
133 * Past.
134 *
135 * @param L (M) demands L_i = theta_i / mu_i, strictly positive
136 * @param N total closed population K
137 * @param S (M) servers per station; cftp_inf_servers for a delay.
138 * Empty for all single-server.
139 * @param nsamples number of independent draws
140 * @param method exact CFTP or the approximate sampler
141 * @param rng explicit generator, advanced by the call
142 */
143template <class T>
144CftpResult<T> pfqn_cftp(const std::vector<T>& L, int N, const std::vector<int>& S,
145 std::size_t nsamples, CftpMethod method, McRng& rng) {
147 "pfqn_cftp requires transcendental arithmetic: it draws random states and forms "
148 "the station balance functions in the log domain");
149
150 const std::size_t M = L.size();
151 if (M < 2) throw InputError("pfqn_cftp: at least two stations are required");
152 if (N < 0) throw InputError("pfqn_cftp: negative population");
153 if (nsamples == 0) throw InputError("pfqn_cftp: at least one sample is required");
154 const T zero = num_traits<T>::from_int(0);
155 for (std::size_t i = 0; i < M; ++i)
156 if (!(L[i] > zero)) throw InputError("pfqn_cftp: all demands L must be strictly positive");
157 std::vector<int> Sv(M, 1);
158 if (!S.empty()) {
159 if (S.size() != M) throw InputError("pfqn_cftp: S has the wrong station count");
160 Sv = S;
161 for (std::size_t i = 0; i < M; ++i)
162 if (Sv[i] == 0 || Sv[i] < cftp_inf_servers)
163 throw InputError("pfqn_cftp: server counts must be positive or cftp_inf_servers");
164 }
165 const int K = N;
166
167 // logfac(i, m) = sum_{t=1}^{m} log min(t, S_i), m = 0 .. K
168 Matrix<double> logfac(M, static_cast<std::size_t>(K) + 1, 0.0);
169 std::vector<double> logL(M);
170 for (std::size_t i = 0; i < M; ++i) {
171 logL[i] = num_traits<T>::log_as_double(L[i]);
172 double acc = 0.0;
173 for (int m = 1; m <= K; ++m) {
174 const double cap = Sv[i] == cftp_inf_servers
175 ? static_cast<double>(m)
176 : static_cast<double>(m < Sv[i] ? m : Sv[i]);
177 acc += std::log(cap);
178 logfac(i, static_cast<std::size_t>(m)) = acc;
179 }
180 }
181
182 CftpResult<T> res;
183 res.X = Matrix<int>(nsamples, M, 0);
184 res.horizon.assign(nsamples, 0);
185 res.Q.assign(M, zero);
186
187 std::vector<double> tape;
188 std::vector<int> xU(M), xL(M), x(M);
189 for (std::size_t smp = 0; smp < nsamples; ++smp) {
190 if (method == CftpMethod::Cftp) {
191 tape.clear();
192 long Tback = 1;
193 while (true) {
194 // Extend the tape at its far end; the recent steps keep the
195 // randomness they were already simulated with.
196 while (static_cast<long>(tape.size()) < Tback) tape.push_back(mc_uniform01(rng));
197 xU.assign(M, 0);
198 xU[0] = K;
199 xL.assign(M, 0);
200 xL[M - 1] = K;
201 for (long t = Tback; t >= 1; --t) {
202 const double u = tape[static_cast<std::size_t>(t - 1)];
203 detail::cftp_monotone_update(xU, u, logL, logfac, M);
204 detail::cftp_monotone_update(xL, u, logL, logfac, M);
205 }
206 if (xU == xL) {
207 x = xU;
208 res.horizon[smp] = Tback;
209 break;
210 }
211 Tback *= 2;
212 }
213 } else {
214 const double eps = 1e-2;
215 const long steps = static_cast<long>(std::ceil(
216 static_cast<double>(M * (M - 1)) / 2.0 *
217 std::log(static_cast<double>(K > 0 ? K : 1) / eps)));
218 res.horizon[smp] = steps;
219 x.assign(M, 0);
220 x[0] = K;
221 for (long t = 0; t < steps; ++t) {
222 const std::size_t i = static_cast<std::size_t>(mc_uniform_int(rng, M));
223 std::size_t j = static_cast<std::size_t>(mc_uniform_int(rng, M - 1));
224 if (j >= i) ++j; // uniform over the M-1 stations other than i
225 const int k = x[i] + x[j];
226 const int l = detail::cftp_split_index(logL, logfac, i, j, k, mc_uniform01(rng));
227 x[i] = l;
228 x[j] = k - l;
229 }
230 }
231 for (std::size_t i = 0; i < M; ++i) {
232 res.X(smp, i) = x[i];
233 res.Q[i] += num_traits<T>::from_int(x[i]);
234 }
235 }
236 for (std::size_t i = 0; i < M; ++i)
237 res.Q[i] /= num_traits<T>::from_int(static_cast<long>(nsamples));
238 return res;
239}
240
241/** Reference defaults: single servers, one sample, exact CFTP. */
242template <class T>
243CftpResult<T> pfqn_cftp(const std::vector<T>& L, int N, McRng& rng) {
244 return pfqn_cftp(L, N, std::vector<int>(), static_cast<std::size_t>(1), CftpMethod::Cftp, rng);
245}
246
247} // namespace pfqn
248} // namespace line
249
250#endif // LINE_API_PFQN_CFTP_H
InputError(const std::string &what)
Definition error.h:39
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.
std::uint64_t mc_uniform_int(McRng &g, std::uint64_t n)
Uniform integer on [0, n), unbiased by rejection.
double mc_uniform01(McRng &g)
Uniform deviate on [0,1) with 53 significant bits, as a double.
constexpr int cftp_inf_servers
Sentinel for an infinite-server (delay) station, the reference's S = Inf.
Definition pfqn_cftp.h:67
CftpMethod
Which sampler to run.
Definition pfqn_cftp.h:70
@ Cftp
exact, monotone coupling from the past
Definition pfqn_cftp.h:71
@ Approx
the rapidly-mixing approximate sampler M_A
Definition pfqn_cftp.h:72
CftpResult< T > pfqn_cftp(const std::vector< T > &L, int N, const std::vector< int > &S, std::size_t nsamples, CftpMethod method, McRng &rng)
Perfect stationary state sampling for closed single-class multiserver product-form networks,...
Definition pfqn_cftp.h:144
Number-type abstraction for the templated API port.
Randomness scaffolding shared by the Monte Carlo normalizing-constant estimators (pfqn_mci,...
Return value of pfqn_cftp, mirroring [Q, X, T].
Definition pfqn_cftp.h:77
Matrix< int > X
(nsamples x M) sampled states, rows sum to K
Definition pfqn_cftp.h:79
std::vector< T > Q
(M) empirical mean queue length
Definition pfqn_cftp.h:78
std::vector< long > horizon
(nsamples) coalescence horizon or step count
Definition pfqn_cftp.h:80