LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_pas_is.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_PAS_IS_H
6#define LINE_API_PFQN_PAS_IS_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Importance-sampling estimate of the normalizing constant of a single
12 * communicating class of a cyclic two-station pass-and-swap (P&S) network with
13 * swap graph H.
14 *
15 * Templated port of matlab/src/api/pfqn/pfqn_pas_is.m together with its
16 * placement-order helper matlab/src/api/pfqn/pas_placement.m, cross-checked
17 * against jar/src/main/java/jline/api/pfqn/nc/Pfqn_pas_is.java.
18 *
19 * Model. Two order-independent stations (1 upstream, 2 downstream) hold all N
20 * jobs. With a non-empty swap graph the ordered-state chain is reducible and
21 * the recurrent communicating class D is the set of orderings that are
22 * non-decreasing with respect to H (Comte and Dorsman, 2021). Writing Phi_m
23 * for the balanced-fairness balance function,
24 *
25 * G_C = sum_{c in D} sum_{k=0}^{ell} Phi_1(c_{1..k}) Phi_2(c_{ell..k+1}),
26 * Phi_m(q) = prod_{p=1}^{|q|} 1 / mu_m(n(q_{1..p})), n(.) = prefix counts,
27 *
28 * which depends on the ordering only through the counts reached at each
29 * position; that is the order-independence property.
30 *
31 * Auto-normalized IS. Orderings are drawn from D by placing, at each step, a
32 * uniformly random placement-order-minimal present class. The SAME samples
33 * feed numerator and denominator: with xi = 1 the estimate is G_C, and with
34 * xi = (number of class-r jobs in the prefix) the ratio is the mean class-r
35 * queue length at station 1. Auto-normalized IS is consistent but biased at
36 * finite sample count for the ratio Q, while G_C itself is unbiased.
37 *
38 * Deviation from the reference, deliberate: 1/p(c) is accumulated as the
39 * product of the integer branching factors rather than as exp(-sum log na).
40 * Same number, no log/exp round trip.
41 *
42 * Arithmetic: INEXACT BY CONSTRUCTION. The output is a random variable, so no
43 * arithmetic makes it exact; the gate is also required by lG = log(G).
44 *
45 * RNG contract: see pfqn_mc_common.h. Comparable to MATLAB only in
46 * distribution, never stream for stream; reproducible within this port only
47 * when the generator is passed in the same state.
48 */
49
50#include <cstddef>
51#include <functional>
52#include <vector>
53
55#include "line/num/number.h"
56#include "line/util/error.h"
57#include "line/util/matrix.h"
58
59namespace line {
60namespace pfqn {
61
62/** Return value of pfqn_pas_is / pfqn_oi_is, mirroring [G, lG, Q]. */
63template <class T>
65 T G; ///< estimate of the communicating-class normalizing constant
66 double lG; ///< log of the estimate
67 Matrix<T> Q; ///< (2 x R) mean per-class queue length, Q(1,:) = N - Q(0,:)
68};
69
70/**
71 * The OI rank rate of a station as a function of the per-class COUNT vector:
72 * the svcRateFun of an OI / P&S node. The argument is the (R) vector of job
73 * counts of the prefix, exactly the `occ` row the MATLAB handles receive.
74 * OI property P1 makes mu permutation-invariant, i.e. a function of the
75 * counts; it is NOT in general a function of the support alone (an INF
76 * station has mu(n) = sum_r n_r sigma_r).
77 */
78template <class T>
79using OiRateFun = std::function<T(const std::vector<int>&)>;
80
81/**
82 * matlab/src/api/pfqn/pas_placement.m: transitive closure of the "must
83 * precede" relation. P(i,j) is true iff class i must be placed before class j.
84 * An empty or all-zero H yields the all-false closure, i.e. no constraint.
85 */
87 if (H.empty()) return Matrix<int>();
88 const std::size_t R = H.rows();
89 if (H.cols() != R) throw InputError("pas_placement: H must be square");
90 Matrix<int> P(R, R, 0);
91 for (std::size_t i = 0; i < R; ++i)
92 for (std::size_t j = 0; j < R; ++j) P(i, j) = H(i, j) != 0 ? 1 : 0;
93 for (std::size_t it = 0; it < R; ++it) {
94 Matrix<int> Pn(R, R, 0);
95 bool changed = false;
96 for (std::size_t i = 0; i < R; ++i)
97 for (std::size_t j = 0; j < R; ++j) {
98 int v = P(i, j);
99 if (!v)
100 for (std::size_t k = 0; k < R && !v; ++k)
101 if (P(i, k) && H(k, j) != 0) v = 1;
102 Pn(i, j) = v;
103 if (v != P(i, j)) changed = true;
104 }
105 P = Pn;
106 if (!changed) break;
107 }
108 return P;
109}
110
111/**
112 * @brief Importance-sampling estimate of the normalizing constant of a single
113 * communicating class of a cyclic two-station pass-and-swap (P&S)
114 * network with swap graph H. Templated port of
115 * matlab/src/api/pfqn/pfqn_pas_is.m together with its placement-order
116 * helper matlab/src/api/pfqn/pas_placement.m, cross-checked against
117 * jar/src/main/java/jline/api/pfqn/nc/Pfqn_pas_is.java.
118 *
119 * @param N (R) closed population vector
120 * @param mu the two OI rank-rate functions, station 1 then station 2
121 * @param H (R x R) swap-graph adjacency; empty or all zero for pure OI
122 * @param samples number of importance samples
123 * @param rng explicit generator, advanced by the call
124 * @param want_qlen estimate the per-class queue lengths as well as the
125 * constant. False estimates ONLY G: the prefix-count matrix is neither
126 * allocated nor written and its coefficients are not accumulated, and Q
127 * comes back zero. The ordering is drawn from the same stream either
128 * way, so G is unchanged to the last bit -- this is for the callers that
129 * want G(N - e_r) and read nothing else from it.
130 */
131template <class T>
132PasIsResult<T> pfqn_pas_is(const std::vector<int>& N, const std::vector<OiRateFun<T>>& mu,
133 const Matrix<int>& H, std::size_t samples, McRng& rng,
134 bool want_qlen = true) {
136 "pfqn_pas_is requires transcendental arithmetic: it is a Monte Carlo estimator, "
137 "inexact by construction, and reports the log of its own estimate");
138
139 const std::size_t R = N.size();
140 if (mu.size() != 2)
141 throw InputError(
142 "pfqn_pas_is models a two-station pass-and-swap tandem: mu must have exactly two rate "
143 "functions");
144 for (int n : N)
145 if (n < 0) throw InputError("pfqn_pas_is: negative population");
146 if (!H.empty() && (H.rows() != R || H.cols() != R))
147 throw InputError("pfqn_pas_is: H must be a (R x R) swap-graph adjacency matrix");
148 if (samples == 0) throw InputError("pfqn_pas_is: at least one sample is required");
149
150 const T zero = num_traits<T>::from_int(0);
151 const T one = num_traits<T>::from_int(1);
152
153 PasIsResult<T> res;
154 res.Q = Matrix<T>(2, R, zero);
155 long ell_l = 0;
156 for (int n : N) ell_l += n;
157 if (ell_l == 0) {
158 res.G = one;
159 res.lG = 0.0;
160 return res;
161 }
162 const std::size_t ell = static_cast<std::size_t>(ell_l);
163
164 const Matrix<int> P = pas_placement(H);
165
166 // accum[0] carries xi = 1; accum[1 + r] carries xi = n_{1,r}. Without the
167 // queue lengths only the first coefficient exists, and cnt1 -- an O(ell R)
168 // write per sample that nothing else reads -- is not allocated at all.
169 std::vector<T> accum(want_qlen ? R + 1 : 1, zero);
170 std::vector<int> x(R), occ(R), occ2(R), avail(R);
171 std::vector<std::size_t> c(ell);
172 std::vector<T> Phi1(ell + 1), Phi2cut(ell + 1);
173 Matrix<T> cnt1(want_qlen ? ell + 1 : 0, want_qlen ? R : 0, zero);
174
175 for (std::size_t s = 0; s < samples; ++s) {
176 // ---- draw a feasible ordering from D ------------------------------
177 x = N;
178 T invp = one;
179 for (std::size_t p = 0; p < ell; ++p) {
180 std::size_t na = 0;
181 for (std::size_t j = 0; j < R; ++j) {
182 if (x[j] <= 0) continue;
183 bool blocked = false;
184 if (!P.empty())
185 for (std::size_t i = 0; i < R && !blocked; ++i)
186 if (x[i] > 0 && P(i, j)) blocked = true;
187 if (!blocked) avail[na++] = j;
188 }
189 if (na == 0)
190 throw NumericError(
191 "pfqn_pas_is: swap graph induces no feasible ordering (cyclic placement "
192 "order)");
193 const std::size_t pick = avail[mc_uniform_int(rng, na)];
194 c[p] = pick;
195 invp *= num_traits<T>::from_int(static_cast<long>(na));
196 x[pick] -= 1;
197 }
198
199 // ---- prefix balance at station 1, and the per-class prefix counts ---
200 occ.assign(R, 0);
201 Phi1[0] = one;
202 if (want_qlen)
203 for (std::size_t r = 0; r < R; ++r) cnt1(0, r) = zero;
204 T phi = one;
205 for (std::size_t k = 0; k < ell; ++k) {
206 const std::size_t cls = c[k];
207 occ[cls] += 1;
208 const T rate = mu[0](occ);
209 if (rate == zero) throw NumericError("pfqn_pas_is: zero rank rate at station 1");
210 phi /= rate;
211 Phi1[k + 1] = phi;
212 if (want_qlen)
213 for (std::size_t r = 0; r < R; ++r)
214 cnt1(k + 1, r) = num_traits<T>::from_int(occ[r]);
215 }
216
217 // ---- reversed-suffix balance at station 2 --------------------------
218 occ2.assign(R, 0);
219 phi = one;
220 Phi2cut[ell] = one;
221 for (std::size_t k = ell; k >= 1; --k) {
222 occ2[c[k - 1]] += 1;
223 const T rate = mu[1](occ2);
224 if (rate == zero) throw NumericError("pfqn_pas_is: zero rank rate at station 2");
225 phi /= rate;
226 Phi2cut[k - 1] = phi;
227 }
228
229 // ---- split convolution over every cut ------------------------------
230 for (std::size_t k = 0; k <= ell; ++k) {
231 const T w2 = k >= ell ? one : Phi2cut[k];
232 const T w = Phi1[k] * w2;
233 accum[0] += w * invp;
234 if (want_qlen && k > 0)
235 for (std::size_t r = 0; r < R; ++r) accum[1 + r] += w * cnt1(k, r) * invp;
236 }
237 }
238
239 const T ns = num_traits<T>::from_int(static_cast<long>(samples));
240 res.G = accum[0] / ns;
242 if (want_qlen)
243 for (std::size_t r = 0; r < R; ++r) {
244 const T q1 = res.G > zero ? (accum[1 + r] / ns) / res.G : zero;
245 res.Q(0, r) = q1;
246 res.Q(1, r) = num_traits<T>::from_int(N[r]) - q1;
247 }
248 return res;
249}
250
251/** Reference default of 1e4 samples. */
252template <class T>
253PasIsResult<T> pfqn_pas_is(const std::vector<int>& N, const std::vector<OiRateFun<T>>& mu,
254 const Matrix<int>& H, McRng& rng, bool want_qlen = true) {
255 return pfqn_pas_is(N, mu, H, static_cast<std::size_t>(10000), rng, want_qlen);
256}
257
258} // namespace pfqn
259} // namespace line
260
261#endif // LINE_API_PFQN_PAS_IS_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
NumericError(const std::string &what)
Definition error.h:45
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.
PasPlacement< T > pas_placement(const Matrix< T > &H)
Precedence closure of a swap graph.
PasIsResult< T > pfqn_pas_is(const std::vector< int > &N, const std::vector< OiRateFun< T > > &mu, const Matrix< int > &H, std::size_t samples, McRng &rng, bool want_qlen=true)
Importance-sampling estimate of the normalizing constant of a single communicating class of a cyclic ...
std::function< T(const std::vector< int > &)> OiRateFun
The OI rank rate of a station as a function of the per-class COUNT vector: the svcRateFun of an OI / ...
Definition pfqn_pas_is.h:79
Number-type abstraction for the templated API port.
Randomness scaffolding shared by the Monte Carlo normalizing-constant estimators (pfqn_mci,...
Return value of pfqn_pas_is / pfqn_oi_is, mirroring [G, lG, Q].
Definition pfqn_pas_is.h:64
double lG
log of the estimate
Definition pfqn_pas_is.h:66
T G
estimate of the communicating-class normalizing constant
Definition pfqn_pas_is.h:65
Matrix< T > Q
(2 x R) mean per-class queue length, Q(1,:) = N - Q(0,:)
Definition pfqn_pas_is.h:67