LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_nc_oi.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_NC_SOLVER_NC_OI_H
6#define LINE_SOLVERS_NC_SOLVER_NC_OI_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Order-independent (OI) and pass-and-swap (P&S) normalizing-constant analysis.
12 *
13 * Ports `nc_is_oi_model.m`, `nc_is_pas_model.m`, `solver_nc_oi_analyzer.m` and
14 * `solver_nc_pas_is_analyzer.m`.
15 *
16 * An OI station is a load-dependent server whose total rate mu(n) is invariant
17 * under permutations of the ordered microstate, so it is a function of the
18 * per-class count vector alone. Such a station is product-form under balanced
19 * fairness (Bonald and Proutiere 2003) and the whole network's constant is
20 * assembled on the population lattice: `pfqn_ncoi` evaluates the OI stations
21 * together with the aggregated delay, and every ordinary BCMP station is folded
22 * in by lattice convolution of its load-dependent weight table. Mean queue
23 * lengths come from the OI functional-server identity of `pfqn_oi_fnc` (Casale,
24 * QEST 2006), E[f(n_i)] = G^+/G - 1 with G^+ the convolution of that station's
25 * FNC balance function against the full-network table.
26 *
27 * A P&S station with a NON-EMPTY swap graph is not order-independent: the
28 * ordered-state chain is reducible (Comte and Dorsman 2021) and only the
29 * recurrent communicating class carries a product form. Its constant has no
30 * exact lattice recursion here and is estimated by the auto-normalized
31 * importance sampler `pfqn_pas_is`, which reduces to `pfqn_oi_is` when the swap
32 * graph is empty.
33 *
34 * THE TWO ANALYZERS ARE NOT INTERCHANGEABLE and the dispatch order decides
35 * which a model gets: a pure-OI tandem on 'default'/'exact' is caught by the
36 * exact analyzer first, and only a genuine swap graph (or an explicit 'is' /
37 * 'sampling') reaches the sampler.
38 *
39 * Arithmetic: the OI analyzer needs `log` for lG and `exp`/`lgamma` for the
40 * BCMP weight table, so it is guarded on `has_transcendental`. The sampler needs
41 * a random stream and is guarded the same way through `pfqn_pas_is`.
42 *
43 * NOTE ON PRECISION AT `Real<N>`. `oi_ld_table` accumulates its weight in a
44 * DOUBLE logarithm, exactly as the reference's `gammaln`/`exp` do, so the BCMP
45 * factor is double precision whatever T is. Everything else -- the balanced-
46 * fairness fill, the lattice convolution, the FNC identity -- is field
47 * arithmetic in T. A higher-precision run therefore gains nothing on the BCMP
48 * stations; the table would have to be built as an exact multinomial product to
49 * change that, which is a different algorithm from the reference's.
50 */
51
52#include <cmath>
53#include <functional>
54#include <map>
55#include <memory>
56#include <vector>
57
65#include "line/util/error.h"
66
67namespace line {
68namespace nc {
69
70namespace detail {
71
72/** Column-major lattice descriptor for 0 <= n <= N, as `oi_lattice`. */
73inline void oi_lattice(const std::vector<int>& N, std::vector<std::size_t>& shp,
74 std::vector<std::size_t>& stride, std::size_t& total) {
75 const std::size_t R = N.size();
76 shp.assign(R, 0);
77 stride.assign(R, 1);
78 total = 1;
79 for (std::size_t d = 0; d < R; ++d) shp[d] = static_cast<std::size_t>(N[d]) + 1;
80 for (std::size_t d = 1; d < R; ++d) stride[d] = stride[d - 1] * shp[d - 1];
81 for (std::size_t d = 0; d < R; ++d) total *= shp[d];
82}
83
84/** Decode a 0-based linear lattice index to its count vector, as `oi_sub`. */
85inline std::vector<int> oi_sub(std::size_t i, const std::vector<std::size_t>& shp) {
86 std::vector<int> n(shp.size(), 0);
87 std::size_t li = i;
88 for (std::size_t d = 0; d < shp.size(); ++d) {
89 n[d] = static_cast<int>(li % shp[d]);
90 li /= shp[d];
91 }
92 return n;
93}
94
95/** Flatten a count vector to its 0-based lattice index. */
96inline std::size_t oi_idx(const std::vector<int>& n, const std::vector<std::size_t>& stride) {
97 std::size_t i = 0;
98 for (std::size_t d = 0; d < n.size(); ++d) i += stride[d] * static_cast<std::size_t>(n[d]);
99 return i;
100}
101
102/**
103 * The canonical ordered microstate holding n_r copies of class r, as
104 * `oi_microstate`: any ordering evaluates to the same rate at an OI station.
105 */
106inline std::vector<std::size_t> oi_microstate(const std::vector<int>& n) {
107 std::vector<std::size_t> c;
108 for (std::size_t r = 0; r < n.size(); ++r)
109 for (int a = 0; a < n[r]; ++a) c.push_back(r + 1);
110 return c;
111}
112
113/** Count vectors kept per memoized rank rate; a miss past it just recomputes. */
114const std::size_t OI_RANK_RATE_MEMO_LIMIT = 1u << 17;
115
116/**
117 * An OI rank rate on a per-class COUNT vector, MEMOIZED on that vector.
118 *
119 * Only the importance sampler should use this. It calls the rate 2(ell+1) times
120 * per sampled ordering and walks the same prefix occupancies over and over, so
121 * rebuilding the microstate -- and re-evaluating the balance function on it --
122 * dominated the estimator. The value depends on nothing but the counts, so the
123 * memo returns what the rebuild would have returned, bit for bit. The exact
124 * enumerating paths visit each count vector ONCE and must keep calling
125 * `oi_microstate` directly: there a memo is pure loss.
126 */
127template <class T>
128inline pfqn::OiRateFun<T> oi_rank_rate(
129 const std::function<T(const std::vector<std::size_t>&)>& f) {
130 std::shared_ptr<std::map<std::vector<int>, T> > memo(new std::map<std::vector<int>, T>());
131 return [f, memo](const std::vector<int>& n) -> T {
132 const typename std::map<std::vector<int>, T>::const_iterator it = memo->find(n);
133 if (it != memo->end()) return it->second;
134 const T v = f(oi_microstate(n));
135 if (memo->size() < OI_RANK_RATE_MEMO_LIMIT) (*memo)[n] = v;
136 return v;
137 };
138}
139
140/**
141 * Forward balanced-fairness fill of an OI balance function, as `oi_phi`:
142 * Phi(0) = 1 and Phi(n) = (1/mu(n)) sum_{r : n_r > 0} Phi(n - e_r).
143 */
144template <class T>
145std::vector<T> oi_phi(const std::function<T(const std::vector<int>&)>& oirate,
146 const std::vector<int>& N) {
147 std::vector<std::size_t> shp, stride;
148 std::size_t total = 0;
149 oi_lattice(N, shp, stride, total);
150 const std::size_t R = shp.size();
151 std::vector<T> Phi(total, num_traits<T>::from_int(0));
152 for (std::size_t i = 0; i < total; ++i) {
153 const std::vector<int> n = oi_sub(i, shp);
154 int tot = 0;
155 for (int v : n) tot += v;
156 if (tot == 0) {
157 Phi[i] = num_traits<T>::from_int(1);
158 continue;
159 }
160 T s = num_traits<T>::from_int(0);
161 for (std::size_t r = 0; r < R; ++r)
162 if (n[r] > 0) s += Phi[i - stride[r]];
163 Phi[i] = T(s / oirate(n));
164 }
165 return Phi;
166}
167
168/**
169 * The BCMP load-dependent weight table of one station, as `oi_ld_table`:
170 * W(n) = |n|!/prod(n_r!) prod_r D_r^{n_r} / prod_{k=1}^{|n|} beta(k), with
171 * beta(k) = min(k, c). A class with zero demand contributes nothing, so any
172 * lattice point that places a job there has weight zero.
173 */
174template <class T>
175std::vector<T> oi_ld_table(const std::vector<T>& Dq, double c,
176 const std::vector<std::size_t>& shp, std::size_t total) {
177 const std::size_t R = shp.size();
178 std::vector<T> W(total, num_traits<T>::from_int(0));
179 const double cc = (std::isfinite(c) && c > 0.0) ? c : 1.0;
180 for (std::size_t i = 0; i < total; ++i) {
181 const std::vector<int> n = oi_sub(i, shp);
182 int tot = 0;
183 for (int v : n) tot += v;
184 double logf = std::lgamma(static_cast<double>(tot) + 1.0);
185 bool ok = true;
186 for (std::size_t r = 0; r < R; ++r)
187 if (n[r] > 0) {
188 const double d = num_traits<T>::to_double(Dq[r]);
189 if (!(d > 0.0)) {
190 ok = false;
191 break;
192 }
193 logf += n[r] * std::log(d) - std::lgamma(static_cast<double>(n[r]) + 1.0);
194 }
195 if (!ok) continue;
196 for (int k = 1; k <= tot; ++k)
197 logf -= std::log(std::min(static_cast<double>(k), cc));
198 W[i] = num_traits<T>::from_double(std::exp(logf));
199 }
200 return W;
201}
202
203/** Lattice convolution C(m) = sum_{0<=a<=m} A(a) B(m-a), as `oi_conv`. */
204template <class T>
205std::vector<T> oi_conv(const std::vector<T>& A, const std::vector<T>& B,
206 const std::vector<std::size_t>& shp,
207 const std::vector<std::size_t>& stride, std::size_t total) {
208 const std::size_t R = shp.size();
209 std::vector<std::vector<int>> subs(total);
210 for (std::size_t i = 0; i < total; ++i) subs[i] = oi_sub(i, shp);
211 std::vector<T> C(total, num_traits<T>::from_int(0));
212 for (std::size_t i = 0; i < total; ++i) {
213 T acc = num_traits<T>::from_int(0);
214 for (std::size_t j = 0; j <= i; ++j) {
215 bool le = true;
216 for (std::size_t d = 0; d < R && le; ++d)
217 if (subs[j][d] > subs[i][d]) le = false;
218 if (!le) continue;
219 std::size_t off = 0;
220 for (std::size_t d = 0; d < R; ++d)
221 off += stride[d] * static_cast<std::size_t>(subs[i][d] - subs[j][d]);
222 acc += T(A[j] * B[off]);
223 }
224 C[i] = acc;
225 }
226 return C;
227}
228
229/** G^+ = sum_{0<=b<=N} Psi(b) G(N-b), as `oi_fnc_mean`. */
230template <class T>
231T oi_fnc_mean(const std::vector<T>& Psi, const std::vector<T>& Gfull,
232 const std::vector<std::size_t>& shp, const std::vector<std::size_t>& stride,
233 std::size_t total) {
234 const T zero = num_traits<T>::from_int(0);
235 T val = zero;
236 for (std::size_t i = 0; i < total; ++i) {
237 if (Psi[i] == zero) continue;
238 const std::vector<int> b = oi_sub(i, shp);
239 std::size_t off = 0;
240 for (std::size_t d = 0; d < shp.size(); ++d)
241 off += stride[d] * (shp[d] - 1 - static_cast<std::size_t>(b[d]));
242 val += T(Psi[i] * Gfull[off]);
243 }
244 return val;
245}
246
247/** Per-class visits normalized to the reference station, as both analyzers do. */
248template <class T>
249Matrix<T> oi_visits(const qn::NetworkStruct<T>& sn) {
250 const T zero = num_traits<T>::from_int(0);
251 const std::size_t M = sn.nstations, K = sn.nclasses;
252 Matrix<T> V(M, K, zero);
253 for (std::size_t r = 0; r < K; ++r) {
254 std::size_t chain = 0;
255 for (std::size_t c = 0; c < sn.nchains; ++c)
256 if (sn.chains[c][r]) chain = c + 1;
257 if (chain == 0) continue;
258 for (std::size_t i = 0; i < M; ++i)
259 V(i, r) = sn.visits[chain - 1](sn.stateful_of_station(i + 1) - 1, r);
260 const T vref = V(sn.classes[r].refstat - 1, r);
261 if (vref > zero)
262 for (std::size_t i = 0; i < M; ++i) V(i, r) = T(V(i, r) / vref);
263 }
264 return V;
265}
266
267/** True when the FCFS / SIRO rates at a station vary across populated classes. */
268template <class T>
269bool class_dependent_fcfs_rate(const qn::NetworkStruct<T>& sn, std::size_t i) {
270 double lo = 0.0, hi = 0.0;
271 bool any = false;
272 for (std::size_t r = 0; r < sn.nclasses; ++r) {
273 if (!(sn.classes[r].population > 0.0)) continue;
274 const double v = num_traits<T>::to_double(sn.rates(i, r));
275 if (!std::isfinite(v)) continue;
276 if (!any) {
277 lo = hi = v;
278 any = true;
279 } else {
280 lo = std::min(lo, v);
281 hi = std::max(hi, v);
282 }
283 }
284 return any && (hi - lo) > 1e-9 * hi;
285}
286
287} // namespace detail
288
289/**
290 * Port of `nc_is_oi_model.m`: a closed network with at least one OI station and
291 * nothing but BCMP product-form stations besides.
292 *
293 * The OI requirement is what keeps a pure-BCMP network on the ordinary (faster)
294 * normalizing-constant path rather than the lattice one.
295 */
296template <class T>
298 using qn::SchedStrategy;
299 for (const qn::JobClass& c : sn.classes)
300 if (std::isinf(c.population)) return false;
301 bool hasOI = false;
302 for (std::size_t i = 0; i < sn.nstations; ++i) {
303 const SchedStrategy s = sn.stations[i].sched;
304 if (s == SchedStrategy::INF) continue;
305 if (s == SchedStrategy::PAS || s == SchedStrategy::OI) {
306 if (!qn::station_swap_graph_is_zero(sn, i + 1)) return false; // genuine P&S
307 hasOI = true;
308 } else if (s == SchedStrategy::PS || s == SchedStrategy::LCFSPR ||
309 s == SchedStrategy::SIRO || s == SchedStrategy::FCFS) {
310 if ((s == SchedStrategy::FCFS || s == SchedStrategy::SIRO) &&
311 detail::class_dependent_fcfs_rate(sn, i))
312 return false;
313 } else {
314 return false; // an unsupported (non-product-form) station
315 }
316 }
317 return hasOI;
318}
319
320/**
321 * Port of `nc_is_pas_model.m`: a closed two-station OI / P&S tandem.
322 *
323 * The swap graph may be empty, since an OI queue is exactly the P&S
324 * specialization with a zero graph and `pfqn_pas_is` reduces to `pfqn_oi_is`
325 * there; the exact analyzer is what keeps a pure-OI tandem off this path on
326 * 'default' and 'exact'.
327 */
328template <class T>
330 using qn::SchedStrategy;
331 for (const qn::JobClass& c : sn.classes)
332 if (std::isinf(c.population)) return false;
333 if (sn.nstations != 2) return false;
334 for (std::size_t i = 0; i < sn.nstations; ++i) {
335 const SchedStrategy s = sn.stations[i].sched;
336 if (s != SchedStrategy::PAS && s != SchedStrategy::OI) return false;
337 if (!sn.stations[i].svc_rate_fun) return false;
338 }
339 return true;
340}
341
342/**
343 * Port of `solver_nc_oi_analyzer.m`.
344 *
345 * @param sn the refreshed struct
346 * @param opt solver controls; unused, the analyzer is exact and has no tuning
347 */
348template <class T>
350 using qn::SchedStrategy;
351 (void)opt;
352 NcSolution<T> out;
353 if constexpr (!num_traits<T>::has_transcendental) {
354 (void)sn;
355 throw UnsupportedError(
356 "solver_nc_oi_analyzer: the BCMP weight table is formed as exp(lgamma(...)) and lG as "
357 "log(G); this backend has no transcendental arithmetic");
358 } else {
359 const T zero = num_traits<T>::from_int(0);
360 const std::size_t M = sn.nstations, K = sn.nclasses;
361
362 // The OI rank rates are indexed by RAW class, so a chain that merges
363 // several classes has no rate to evaluate.
364 for (std::size_t c = 0; c < sn.nchains; ++c)
365 if (sn.inchain[c].size() > 1)
366 throw UnsupportedError(
367 "solver_nc_oi: requires one class per chain (no class switching)");
368 std::vector<int> N(K, 0);
369 for (std::size_t r = 0; r < K; ++r) {
370 if (std::isinf(sn.classes[r].population))
371 throw UnsupportedError("solver_nc_oi: requires a closed queueing network");
372 N[r] = static_cast<int>(std::llround(sn.classes[r].population));
373 }
374
375 std::vector<bool> isOI(M, false), isINF(M, false), isQ(M, false);
376 for (std::size_t i = 0; i < M; ++i) {
377 const SchedStrategy s = sn.stations[i].sched;
378 if (s == SchedStrategy::INF) {
379 isINF[i] = true;
380 } else if (s == SchedStrategy::PAS || s == SchedStrategy::OI) {
382 throw UnsupportedError(
383 "solver_nc_oi: supports OI stations only (PAS with a non-empty swap graph "
384 "is not order-independent)");
385 isOI[i] = true;
386 if (!sn.stations[i].svc_rate_fun)
387 throw UnsupportedError(
388 "solver_nc_oi: an OI station has no service rate function; set it via "
389 "setServiceRateFunction");
390 } else if (s == SchedStrategy::PS || s == SchedStrategy::LCFSPR ||
391 s == SchedStrategy::FCFS || s == SchedStrategy::SIRO) {
392 isQ[i] = true;
393 if ((s == SchedStrategy::FCFS || s == SchedStrategy::SIRO) &&
394 detail::class_dependent_fcfs_rate(sn, i))
395 throw UnsupportedError(
396 "solver_nc_oi: a station has class-dependent FCFS/SIRO rates and is not "
397 "product form; class-independent rates are required");
398 } else {
399 throw UnsupportedError(
400 "solver_nc_oi: supports only INF (delay), OI, PS, LCFS-PR, SIRO and "
401 "class-independent FCFS stations");
402 }
403 }
404
405 const Matrix<T> V = detail::oi_visits(sn);
406
407 // Delays aggregate into Z; every other BCMP queue keeps its own demand.
408 std::vector<T> Z(K, zero);
409 Matrix<T> D(M, K, zero);
410 for (std::size_t i = 0; i < M; ++i)
411 for (std::size_t r = 0; r < K; ++r) {
412 const double mu = num_traits<T>::to_double(sn.rates(i, r));
413 if (!std::isfinite(mu) || mu == 0.0) continue;
414 const T st = T(V(i, r) / sn.rates(i, r));
415 if (isINF[i]) Z[r] += st;
416 if (isQ[i]) D(i, r) = st;
417 }
418
419 for (std::size_t i = 0; i < M; ++i)
420 if (isOI[i])
421 for (std::size_t r = 0; r < K; ++r)
422 if (N[r] > 0 &&
423 std::fabs(num_traits<T>::to_double(V(i, r)) - 1.0) > 1e-9)
424 throw UnsupportedError(
425 "solver_nc_oi: requires unit per-class visits at every OI station");
426
427 std::vector<std::size_t> oiList, qList;
428 for (std::size_t i = 0; i < M; ++i) {
429 if (isOI[i]) oiList.push_back(i + 1);
430 if (isQ[i]) qList.push_back(i + 1);
431 }
432 std::vector<pfqn::OiRate<T>> rates;
433 for (std::size_t m = 0; m < oiList.size(); ++m) {
434 const std::function<T(const std::vector<std::size_t>&)> f =
435 sn.stations[oiList[m] - 1].svc_rate_fun;
436 rates.push_back(
437 [f](const std::vector<int>& n) { return f(detail::oi_microstate(n)); });
438 }
439
440 std::vector<std::size_t> shp, stride;
441 std::size_t total = 0;
442 detail::oi_lattice(N, shp, stride, total);
443
444 // The core table over the lattice: OI stations plus the aggregated delay.
445 std::vector<T> Gfull(total, zero);
446 for (std::size_t i = 0; i < total; ++i)
447 Gfull[i] = pfqn::pfqn_ncoi(Z, detail::oi_sub(i, shp), rates).G;
448
449 for (std::size_t i : qList) {
450 std::vector<T> Dq(K, zero);
451 for (std::size_t r = 0; r < K; ++r) Dq[r] = D(i - 1, r);
452 const std::vector<T> Wq =
453 detail::oi_ld_table(Dq, sn.stations[i - 1].nservers, shp, total);
454 Gfull = detail::oi_conv(Gfull, Wq, shp, stride, total);
455 }
456
457 const T G = Gfull[total - 1];
459
460 std::vector<T> X(K, zero);
461 for (std::size_t r = 0; r < K; ++r)
462 if (N[r] > 0) {
463 std::vector<int> Nr = N;
464 --Nr[r];
465 X[r] = T(Gfull[detail::oi_idx(Nr, stride)] / G);
466 }
467
468 Matrix<T> Q(M, K, zero), Tp(M, K, zero), R(M, K, zero), U(M, K, zero);
469 const T one = num_traits<T>::from_int(1);
470 for (std::size_t m = 0; m < oiList.size(); ++m) {
471 const std::size_t i = oiList[m];
472 const std::vector<T> Phi = detail::oi_phi<T>(rates[m], N);
473 for (std::size_t r = 0; r < K; ++r) {
474 if (N[r] == 0) continue;
476 Phi, N, [r](const std::vector<int>& n) {
477 return num_traits<T>::from_int(n[r]);
478 });
479 Q(i - 1, r) =
480 T(detail::oi_fnc_mean(fr.Psi, Gfull, shp, stride, total) / G - one);
481 }
482 }
483 for (std::size_t i : qList) {
484 std::vector<T> Dq(K, zero);
485 for (std::size_t r = 0; r < K; ++r) Dq[r] = D(i - 1, r);
486 const std::vector<T> Wq =
487 detail::oi_ld_table(Dq, sn.stations[i - 1].nservers, shp, total);
488 for (std::size_t r = 0; r < K; ++r) {
489 if (N[r] == 0) continue;
491 Wq, N, [r](const std::vector<int>& n) {
492 return num_traits<T>::from_int(n[r]);
493 });
494 Q(i - 1, r) =
495 T(detail::oi_fnc_mean(fr.Psi, Gfull, shp, stride, total) / G - one);
496 }
497 }
498 for (std::size_t i = 0; i < M; ++i)
499 if (isINF[i])
500 for (std::size_t r = 0; r < K; ++r) {
501 const double mu = num_traits<T>::to_double(sn.rates(i, r));
502 if (!std::isfinite(mu) || mu == 0.0) continue;
503 Q(i, r) = T(X[r] * V(i, r) / sn.rates(i, r));
504 }
505
506 for (std::size_t i = 0; i < M; ++i)
507 for (std::size_t r = 0; r < K; ++r) Tp(i, r) = T(X[r] * V(i, r));
508 for (std::size_t i = 0; i < M; ++i)
509 if (isINF[i])
510 for (std::size_t r = 0; r < K; ++r) U(i, r) = Q(i, r); // INF convention
511 for (std::size_t i : qList) {
512 const double c_raw = sn.stations[i - 1].nservers;
513 const double c = (std::isfinite(c_raw) && c_raw > 0.0) ? c_raw : 1.0;
514 for (std::size_t r = 0; r < K; ++r)
515 U(i - 1, r) = T(X[r] * D(i - 1, r) / num_traits<T>::from_double(c));
516 }
517 for (std::size_t m = 0; m < oiList.size(); ++m) {
518 // IN-SERVICE utilization E[sir_r]/c, the exact CTMC / LDES
519 // convention: sir_r counts the class-r jobs receiving a strictly
520 // positive rank rate, which is a function of the count vector, so
521 // its mean is read off the same functional-server identity. It
522 // coincides with the offered-load form T/mu(e_r)/c only when a job
523 // engages a single server.
524 const std::size_t i = oiList[m];
525 const double s_raw = sn.stations[i - 1].nservers;
526 const double s = (std::isfinite(s_raw) && s_raw > 0.0) ? s_raw : 1.0;
527 const std::vector<T> Phi = detail::oi_phi<T>(rates[m], N);
528 const pfqn::OiInsvcResult<T> ins = pfqn::pfqn_oi_insvc<T>(rates[m], N);
529 for (std::size_t r = 0; r < K; ++r) {
530 if (N[r] == 0) continue;
532 Phi, N, [&ins, &stride, r](const std::vector<int>& n) {
533 return ins.g(detail::oi_idx(n, stride), r);
534 });
535 U(i - 1, r) =
536 T((detail::oi_fnc_mean(fr.Psi, Gfull, shp, stride, total) / G - one) /
538 }
539 }
540 for (std::size_t i = 0; i < M; ++i)
541 for (std::size_t r = 0; r < K; ++r)
542 if (Tp(i, r) > zero) R(i, r) = T(Q(i, r) / Tp(i, r));
543
544 std::vector<T> C(K, zero);
545 for (std::size_t r = 0; r < K; ++r)
546 if (X[r] > zero) C[r] = T(num_traits<T>::from_int(N[r]) / X[r]);
547
548 out.sol.Q = Q;
549 out.sol.U = U;
550 out.sol.R = R;
551 out.sol.Tp = Tp;
552 out.sol.X = X;
553 out.sol.C = C;
554 out.sol.iter = 1;
555 out.sol.method = "oi";
556 out.actualmethod = "oi";
557 return out;
558 }
559}
560
561/**
562 * Port of `solver_nc_pas_is_analyzer.m`.
563 *
564 * @param sn the refreshed struct
565 * @param opt solver controls; `samples` and `seed` reach `pfqn_pas_is`
566 */
567template <class T>
569 const NcSolverOptions& opt) {
570 using qn::SchedStrategy;
571 NcSolution<T> out;
572 if constexpr (!num_traits<T>::has_transcendental) {
573 (void)sn;
574 (void)opt;
575 throw UnsupportedError(
576 "solver_nc_pas_is_analyzer: the auto-normalized importance sampler draws from a "
577 "continuous proposal and reports lG = log(G); this backend has no transcendental "
578 "arithmetic");
579 } else {
580 const T zero = num_traits<T>::from_int(0);
581 const std::size_t M = sn.nstations, K = sn.nclasses;
582
583 for (std::size_t c = 0; c < sn.nchains; ++c)
584 if (sn.inchain[c].size() > 1)
585 throw UnsupportedError(
586 "solver_nc_pas_is: requires one class per chain (no class switching)");
587 for (const qn::JobClass& c : sn.classes)
588 if (std::isinf(c.population))
589 throw UnsupportedError("solver_nc_pas_is: requires a closed queueing network");
590 if (M != 2)
591 throw UnsupportedError(
592 "solver_nc_pas_is: models a two-station pass-and-swap tandem");
593 std::vector<int> N(K, 0);
594 for (std::size_t r = 0; r < K; ++r)
595 N[r] = static_cast<int>(std::llround(sn.classes[r].population));
596
597 std::vector<std::function<T(const std::vector<std::size_t>&)>> svc(M);
598 std::vector<Matrix<T>> swapG(M);
599 for (std::size_t i = 0; i < M; ++i) {
600 const SchedStrategy s = sn.stations[i].sched;
601 if (s != SchedStrategy::PAS && s != SchedStrategy::OI)
602 throw UnsupportedError(
603 "solver_nc_pas_is: requires both stations to be OI/PAS");
604 svc[i] = sn.stations[i].svc_rate_fun;
605 if (!svc[i])
606 throw UnsupportedError(
607 "solver_nc_pas_is: an OI/PAS station has no service rate function; set it via "
608 "setServiceRateFunction");
609 swapG[i] = qn::station_swap_graph(sn, i + 1);
610 }
611
612 std::vector<pfqn::OiRateFun<T>> mu(M);
613 for (std::size_t i = 0; i < M; ++i) mu[i] = detail::oi_rank_rate<T>(svc[i]);
614
615 // The stored graph is the raw class-compatibility graph; the estimator
616 // needs the global placement-order DAG that defines the recurrent
617 // communicating class, derived from the P&S dynamics on the one-job-per-
618 // class instance. Station 2 is the reversed suffix inside pfqn_pas_is,
619 // so a single global order suffices.
620 //
621 // pas_swap2order hands its rate functions an ORDERED microstate (a list
622 // of 1-based class indices), where pfqn_pas_is hands its own a support
623 // indicator; both reach the same svcRateFun, so only the adapter differs.
624 std::vector<pfqn::PasRateFun<T>> murates(M);
625 for (std::size_t i = 0; i < M; ++i) {
626 const std::function<T(const std::vector<std::size_t>&)> f = svc[i];
627 murates[i] = [f](const std::vector<int>& c) {
628 std::vector<std::size_t> mc(c.size());
629 for (std::size_t a = 0; a < c.size(); ++a)
630 mc[a] = static_cast<std::size_t>(c[a]);
631 return f(mc);
632 };
633 }
634 const Matrix<T> H = pfqn::pas_swap2order<T>(swapG, murates, std::vector<int>(K, 1));
635 Matrix<int> Hi(H.rows(), H.cols(), 0);
636 for (std::size_t a = 0; a < H.rows(); ++a)
637 for (std::size_t b = 0; b < H.cols(); ++b)
638 Hi(a, b) = static_cast<int>(std::llround(num_traits<T>::to_double(H(a, b))));
639
640 const Matrix<T> V = detail::oi_visits(sn);
641 for (std::size_t i = 0; i < M; ++i)
642 for (std::size_t r = 0; r < K; ++r)
643 if (N[r] > 0 && std::fabs(num_traits<T>::to_double(V(i, r)) - 1.0) > 1e-9)
644 throw UnsupportedError(
645 "solver_nc_pas_is: requires unit per-class visits at both stations");
646
647 const std::size_t samples = opt.samples > 0 ? opt.samples : 10000;
648 const unsigned seed = opt.seed > 0 ? static_cast<unsigned>(opt.seed) : 23456u;
649
650 pfqn::McRng g0(seed);
651 const pfqn::PasIsResult<T> res = pfqn::pfqn_pas_is<T>(N, mu, Hi, samples, g0);
652 out.sol.lG = num_traits<T>::log_as_double(res.G);
653
654 Matrix<T> Q(M, K, zero), Tp(M, K, zero), R(M, K, zero), U(M, K, zero);
655 for (std::size_t i = 0; i < M; ++i)
656 for (std::size_t r = 0; r < K; ++r) Q(i, r) = res.Q(i, r);
657
658 // Common random numbers across the N and N-e_r runs: each call restarts
659 // the stream from the same seed, which is what makes the ratio far less
660 // noisy than two independent estimates would be.
661 std::vector<T> X(K, zero);
662 for (std::size_t r = 0; r < K; ++r)
663 if (N[r] > 0) {
664 std::vector<int> Nr = N;
665 --Nr[r];
666 pfqn::McRng gr_rng(seed);
667 // Only the constant is read here, so this run skips the
668 // prefix-count coefficients: same stream, same G, none of the
669 // O(ell R) per-sample bookkeeping behind the queue lengths.
670 const pfqn::PasIsResult<T> gr =
671 pfqn::pfqn_pas_is<T>(Nr, mu, Hi, samples, gr_rng, false);
672 if (res.G > zero) X[r] = T(gr.G / res.G);
673 }
674
675 for (std::size_t i = 0; i < M; ++i)
676 for (std::size_t r = 0; r < K; ++r) Tp(i, r) = T(X[r] * V(i, r));
677 for (std::size_t i = 0; i < M; ++i) {
678 const double s_raw = sn.stations[i].nservers;
679 const double s = (std::isfinite(s_raw) && s_raw > 0.0) ? s_raw : 1.0;
680 for (std::size_t r = 0; r < K; ++r) {
681 if (N[r] == 0) continue;
682 std::vector<int> er(K, 0);
683 er[r] = 1;
684 const T muR = mu[i](er); // rank rate with only class r present
685 if (muR > zero)
686 U(i, r) = T(Tp(i, r) / muR / num_traits<T>::from_double(s));
687 }
688 }
689 for (std::size_t i = 0; i < M; ++i)
690 for (std::size_t r = 0; r < K; ++r)
691 if (Tp(i, r) > zero) R(i, r) = T(Q(i, r) / Tp(i, r));
692
693 std::vector<T> C(K, zero);
694 for (std::size_t r = 0; r < K; ++r)
695 if (X[r] > zero) C[r] = T(num_traits<T>::from_int(N[r]) / X[r]);
696
697 out.sol.Q = Q;
698 out.sol.U = U;
699 out.sol.R = R;
700 out.sol.Tp = Tp;
701 out.sol.X = X;
702 out.sol.C = C;
703 out.sol.iter = 1;
704 out.sol.method = "is";
705 out.actualmethod = "is";
706 return out;
707 }
708}
709
710} // namespace nc
711} // namespace line
712
713#endif // LINE_SOLVERS_NC_SOLVER_NC_OI_H
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.
The exception types the port throws.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
bool nc_is_oi_model(const qn::NetworkStruct< T > &sn)
Port of nc_is_oi_model.m: a closed network with at least one OI station and nothing but BCMP product-...
NcSolution< T > solver_nc_pas_is_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_pas_is_analyzer.m.
bool nc_is_pas_model(const qn::NetworkStruct< T > &sn)
Port of nc_is_pas_model.m: a closed two-station OI / P&S tandem.
NcSolution< T > solver_nc_oi_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_oi_analyzer.m.
std::mt19937_64 McRng
The generator type every Monte Carlo entry point in this tree accepts.
NcResult< T > pfqn_ncoi(const std::vector< T > &Z, const std::vector< int > &N, const std::vector< OiRate< T > > &mu, const Matrix< T > &visits)
Normalizing constant of a closed network of ORDER-INDEPENDENT (OI) / pass-and-swap stations with empt...
Definition pfqn_ncoi.h:153
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 ...
Matrix< T > pas_swap2order(const std::vector< Matrix< T > > &swap, const std::vector< PasRateFun< T > > &listRate, const std::vector< int > &N0=std::vector< int >())
Placement-order DAG of a two-station pass-and-swap tandem.
OiFncResult< T > pfqn_oi_fnc(const std::vector< T > &Phi, const std::vector< int > &N, const std::function< T(const std::vector< int > &)> &f)
Order-independent (OI) functional server: the balance function Psi and the rate mu_f of an auxiliary ...
Definition pfqn_oi_fnc.h:76
OiInsvcResult< T > pfqn_oi_insvc(const std::function< T(const std::vector< int > &)> &oirate, const std::vector< int > &N)
Conditional mean number of IN-SERVICE jobs per class at an order-independent station.
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
Matrix< T > station_swap_graph(const NetworkStruct< T > &sn, std::size_t ist)
The swap graph of a PAS / OI station, with the defaults refreshLocalVars.m installs applied.
bool station_swap_graph_is_zero(const NetworkStruct< T > &sn, std::size_t ist)
True when the station's materialized swap graph is entirely zero.
Controls and result shape shared by the normalizing-constant analyzers.
A queueing network and its refreshed NetworkStruct.
Global placement-order DAG of a closed two-station pass-and-swap tandem.
Normalizing constant of a closed network of ORDER-INDEPENDENT (OI) / pass-and-swap stations with empt...
Order-independent (OI) functional server: the balance function Psi and the rate mu_f of an auxiliary ...
Conditional mean number of IN-SERVICE jobs per class at an order-independent station.
Importance-sampling estimate of the normalizing constant of a single communicating class of a cyclic ...
The [Q,U,R,T,C,X,lG] of the reference, plus the algorithm that ran.
Definition nc_types.h:113
mva::MvaSolution< T > sol
Definition nc_types.h:114
std::string actualmethod
Definition nc_types.h:115
Controls, defaulting to SolverOptions('NC') in the reference.
Definition nc_types.h:33
Return value of pfqn_oi_fnc, mirroring [muf, Psi, mu] flattened.
Definition pfqn_oi_fnc.h:58
std::vector< T > Psi
column-major over the lattice
Definition pfqn_oi_fnc.h:59
Return value of pfqn_oi_insvc, mirroring [g, Xi, Phi].
Matrix< T > g
(prod(N+1) x R) E[sir_r | n]
Return value of pfqn_pas_is / pfqn_oi_is, mirroring [G, lG, Q].
Definition pfqn_pas_is.h:64
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
One job class of the network.
double population
infinite for an open class