LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_nc_prob.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_PROB_H
6#define LINE_SOLVERS_NC_SOLVER_NC_PROB_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The state-probability half of the SolverNC class surface: ports of
12 * `solver_nc_marg.m`, `solver_nc_margaggr.m`, `solver_nc_joint.m`,
13 * `solver_nc_jointaggr.m` and `solver_nc_jointaggr_ld.m`, with the five
14 * `@@SolverNC/getProb*` entry points on top of them.
15 *
16 * WHAT THIS ADDS THAT NOTHING ELSE IN THE TREE HAS. These are EXACT
17 * product-form state probabilities. `solver_mva_prob.h` also answers
18 * `getProbAggr`, but by its own account it fits a binomial to the means
19 * (Schmidt 1997); here the probability is a ratio of normalizing constants and
20 * is the model's own, to the last bit.
21 *
22 * THE IDENTITY THEY ALL USE. For a station i holding the per-class vector n_i,
23 *
24 * Pr[n_i] = F_i(n_i) G_{-i}(N - n_i) / G(N)
25 *
26 * where F_i is the station's own balance function evaluated at n_i, G_{-i} is
27 * the constant of the network with station i deleted, and G is the constant of
28 * the whole model. Each factor is another `pfqn_ncld` call on a load-dependent
29 * lattice, so the whole family is three or four constants per station and
30 * nothing else.
31 *
32 * NO STATE PACKAGE: THE INPUT IS THE MARGINAL VECTOR. The reference reaches the
33 * per-class counts through `State.toMarginal(sn, ist, state{isf})`, and
34 * `getProbAggr` gets there by encoding the user's per-class vector with
35 * `State.fromMarginal` first -- a round trip whose only product is the vector
36 * the user already supplied. This port takes that vector directly. The
37 * consequence is precise and is enforced rather than hidden: two branches of
38 * `solver_nc_marg` read state a marginal does not carry, and both are refused
39 * by name (see `solver_nc_prob`).
40 *
41 * ARITHMETIC. Every probability is a difference of logarithms of normalizing
42 * constants, exponentiated once; a non-transcendental backend is refused by
43 * name, as in `solver_nc.h`.
44 */
45
46#include <algorithm>
47#include <cmath>
48#include <cstddef>
49#include <functional>
50#include <limits>
51#include <string>
52#include <vector>
53
64#include "line/util/error.h"
65#include "line/util/matrix.h"
66
67namespace line {
68namespace nc {
69
70/**
71 * A state, as this port expresses it: `nir[i][r]` jobs of class r at station i.
72 *
73 * This is `State.toMarginal`'s second output and `State.fromMarginal`'s input,
74 * i.e. the only part of the reference's state encoding these analyzers use. A
75 * NEGATIVE entry is the reference's "ignore this station" flag and is honoured.
76 */
77using MarginalState = std::vector<std::vector<int>>;
78
79namespace detail {
80
81/** The load-dependent lattice `mu` the probability analyzers all build. */
82template <class T>
83Matrix<T> prob_mu(const qn::NetworkStruct<T>& sn, std::size_t Ntot) {
84 const std::size_t M = sn.nstations;
85 const std::size_t w = std::max<std::size_t>(1, Ntot);
87 for (std::size_t i = 0; i < M; ++i) {
88 const double S = sn.stations[i].nservers;
89 for (std::size_t n = 1; n <= w; ++n)
90 mu(i, n - 1) = num_traits<T>::from_double(
91 std::isinf(S) ? static_cast<double>(n)
92 : std::min<double>(static_cast<double>(n), S));
93 }
94 return mu;
95}
96
97/** `nivec * sn.chains'`: the per-chain totals of a per-class vector. */
98template <class T>
99std::vector<int> to_chain(const qn::NetworkStruct<T>& sn, const std::vector<int>& nir) {
100 std::vector<int> nc(sn.nchains, 0);
101 for (std::size_t c = 0; c < sn.nchains; ++c)
102 for (std::size_t k : sn.inchain[c]) nc[c] += nir[k - 1];
103 return nc;
104}
105
106/** One row of a matrix, as the 1 x R matrix `pfqn_ncld` wants. */
107template <class T>
108Matrix<T> row_of(const Matrix<T>& A, std::size_t i) {
110 for (std::size_t j = 0; j < A.cols(); ++j) r(0, j) = A(i, j);
111 return r;
112}
113
114/** Every row but one (MATLAB's `A(setdiff(1:M,i),:)`). */
115template <class T>
116Matrix<T> drop_row(const Matrix<T>& A, std::size_t i) {
117 Matrix<T> r(A.rows() - 1, A.cols(), num_traits<T>::from_int(0));
118 std::size_t o = 0;
119 for (std::size_t k = 0; k < A.rows(); ++k) {
120 if (k == i) continue;
121 for (std::size_t j = 0; j < A.cols(); ++j) r(o, j) = A(k, j);
122 ++o;
123 }
124 return r;
125}
126
127/** `sn.rates` reciprocated, zero where the pair is disabled. */
128template <class T>
129Matrix<T> service_times(const qn::NetworkStruct<T>& sn) {
130 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
131 Matrix<T> ST(sn.nstations, sn.nclasses, zero);
132 for (std::size_t i = 0; i < sn.nstations; ++i)
133 for (std::size_t r = 0; r < sn.nclasses; ++r)
134 if (!sn.disabled[i][r] && sn.rates(i, r) != zero) ST(i, r) = T(one / sn.rates(i, r));
135 return ST;
136}
137
138/**
139 * `V(i,k)` as the probability analyzers build it: the visit of class k at
140 * station i within ITS OWN chain, unnormalized.
141 *
142 * The reference indexes `sn.visits{c}(ist,k)` with a STATION index while
143 * `sn.visits` is indexed by stateful node; the two coincide on every model NC
144 * accepts today, since a Cache is refused and no other node is stateful. This
145 * port uses the stateful index, which is what the field is.
146 */
147template <class T>
148Matrix<T> class_visits(const qn::NetworkStruct<T>& sn) {
149 const T zero = num_traits<T>::from_int(0);
150 Matrix<T> V(sn.nstations, sn.nclasses, zero);
151 for (std::size_t c = 0; c < sn.nchains; ++c)
152 for (std::size_t i = 0; i < sn.nstations; ++i) {
153 const std::size_t sf = sn.stateful_of_station(i + 1) - 1;
154 for (std::size_t k : sn.inchain[c]) V(i, k - 1) = sn.visits[c](sf, k - 1);
155 }
156 return V;
157}
158
159/** The total closed population, refusing an open model the way these do. */
160template <class T>
161std::size_t closed_total(const qn::NetworkStruct<T>& sn, const char* who) {
162 double t = 0.0;
163 for (const qn::JobClass& c : sn.classes) {
164 if (std::isinf(c.population))
165 throw UnsupportedError(std::string(who) +
166 ": the state probability is defined on a CLOSED network, and "
167 "this model has an open class");
168 t += c.population;
169 }
170 return static_cast<std::size_t>(std::llround(t));
171}
172
173/** Validate a marginal against the model, so a bad index is not a wrong number. */
174template <class T>
175void check_marginal(const qn::NetworkStruct<T>& sn, const MarginalState& nir, const char* who) {
176 if (nir.size() != sn.nstations)
177 throw InputError(std::string(who) + ": the marginal state has " +
178 std::to_string(nir.size()) + " stations, the model has " +
179 std::to_string(sn.nstations));
180 for (const std::vector<int>& row : nir)
181 if (row.size() != sn.nclasses)
182 throw InputError(std::string(who) +
183 ": every station's marginal must give one count per class");
184}
185
186} // namespace detail
187
188/** What the marginal analyzers return: one probability per station. */
189template <class T>
191 std::vector<T> P; ///< (M) probability that station i holds its given vector
192 std::vector<T> logP; ///< (M) the same, in logs
193 double lG = 0.0; ///< the log normalizing constant that normalized them
194};
195
196/**
197 * Port of `solver_nc_margaggr.m`.
198 *
199 * The purely AGGREGATE marginal: the station's balance function evaluated at
200 * the per-class vector, times the constant of the network without it. It reads
201 * nothing but the marginal, so it carries no discipline restriction at all --
202 * which is why `getProbAggr`, `getProbMarg` and `getProbSysAggr` are
203 * unrestricted while `getProb` is not.
204 *
205 * @param sn the refreshed struct
206 * @param opt solver controls
207 * @param nir the state; a station whose row has a NEGATIVE entry is skipped and
208 * reported as probability zero, which is the reference's flag
209 * @param lG a precomputed log normalizing constant; NaN to compute one
210 */
211template <class T>
213 const MarginalState& nir, double lG) {
214 NcMargResult<T> out;
215 if constexpr (!num_traits<T>::has_transcendental) {
216 (void)sn; (void)opt; (void)nir; (void)lG;
217 throw UnsupportedError(
218 "solver_nc_margaggr: the state probability is exp(lF_i + lG_{-i} - lG), a difference "
219 "of logarithms of normalizing constants, and needs transcendental arithmetic");
220 } else {
221 const T zero = num_traits<T>::from_int(0);
222 const std::size_t M = sn.nstations, K = sn.nclasses;
223 detail::check_marginal(sn, nir, "solver_nc_margaggr");
224 const std::size_t Ntot = detail::closed_total(sn, "solver_nc_margaggr");
225
227 const Matrix<T> ST = detail::service_times(sn);
228 const Matrix<T> V = detail::class_visits(sn);
229 const Matrix<T> mu = detail::prob_mu(sn, Ntot);
230 std::vector<int> Nchain(sn.nchains, 0);
231 for (std::size_t c = 0; c < sn.nchains; ++c)
232 Nchain[c] = static_cast<int>(std::llround(d.Nchain[c]));
233
234 const Matrix<T> Zc(1, sn.nchains, zero);
235 const Matrix<T> Zk(1, K, zero);
236 // The method travels as a NAME. pfqn_ncld resolves it where the
237 // reference does, past the degenerate returns, so the per-station
238 // factors below -- one station, no think time, a closed form no
239 // algorithm name selects -- never have to recognize a load-INDEPENDENT
240 // NC method such as 'ls'. See pfqn_ncld.h.
241 const std::string& pm = opt.method;
242 pfqn::NcOptions nopt;
243 nopt.samples = opt.samples;
244 nopt.seed = opt.seed;
245 nopt.tol = opt.tol;
246 const T atol = num_traits<T>::from_double(opt.tol);
247 if (std::isnan(lG))
248 lG = pfqn::pfqn_ncld(d.Lchain, Nchain, Zc, mu, pm, atol, nopt).lG;
249 out.lG = lG;
250
251 out.P.assign(M, zero);
252 out.logP.assign(M, zero);
253 for (std::size_t i = 0; i < M; ++i) {
254 bool ignore = false;
255 for (int v : nir[i])
256 if (v < 0) ignore = true;
257 if (ignore) continue; // MATLAB sets NaN here and then Pr(isnan)=0
258 const std::vector<int> nc = detail::to_chain(sn, nir[i]);
259 std::vector<int> Nrest(sn.nchains, 0);
260 for (std::size_t c = 0; c < sn.nchains; ++c) Nrest[c] = Nchain[c] - nc[c];
261 const double lG_minus_i =
262 M > 1 ? pfqn::pfqn_ncld(detail::drop_row(d.Lchain, i), Nrest, Zc,
263 detail::drop_row(mu, i), pm, atol, nopt)
264 .lG
265 : 0.0;
266 Matrix<T> Fi(1, K, zero);
267 for (std::size_t r = 0; r < K; ++r) Fi(0, r) = T(ST(i, r) * V(i, r));
268 const double lF_i =
269 pfqn::pfqn_ncld(Fi, nir[i], Zk, detail::row_of(mu, i), pm, atol, nopt).lG;
270 const double lp = lF_i + lG_minus_i - lG;
272 out.P[i] = num_traits<T>::from_double(std::exp(lp));
273 }
274 return out;
275 }
276}
277
278/**
279 * Port of `solver_nc_marg.m`: the DETAILED marginal, which weighs the station's
280 * internal arrangement and therefore depends on its discipline.
281 *
282 * TWO BRANCHES ARE REFUSED BY NAME because a per-class marginal cannot carry
283 * what they read, and answering them from the default arrangement would be a
284 * fabricated number:
285 *
286 * SIRO wants the CLASS OF THE JOB IN SERVICE (`sivec`), whose term is
287 * log(n_ci / sum n). A marginal says how many jobs of each class are
288 * present, not which one holds the server.
289 * PS and INF want the PHASE-LEVEL occupancy (`kirvec`) when service is not
290 * exponential. Under exponential service kirvec IS the marginal, and
291 * that case is computed exactly.
292 *
293 * FCFS additionally carries the reference's own preconditions -- exponential
294 * service, and identical mean service time across the classes -- without which
295 * the station is not product-form and the reference errors out.
296 */
297template <class T>
299 const MarginalState& nir, double lG) {
300 NcMargResult<T> out;
301 if constexpr (!num_traits<T>::has_transcendental) {
302 (void)sn; (void)opt; (void)nir; (void)lG;
303 throw UnsupportedError(
304 "solver_nc_marg: the state probability is exp(lF_i + lG_{-i} - lG), a difference of "
305 "logarithms of normalizing constants, and needs transcendental arithmetic");
306 } else {
307 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
308 const std::size_t M = sn.nstations, K = sn.nclasses;
309 detail::check_marginal(sn, nir, "solver_nc_marg");
310 const std::size_t Ntot = detail::closed_total(sn, "solver_nc_marg");
311
313 const Matrix<T> ST = detail::service_times(sn);
314 const Matrix<T> V = detail::class_visits(sn);
315 const Matrix<T> mu = detail::prob_mu(sn, Ntot);
316 std::vector<int> Nchain(sn.nchains, 0);
317 for (std::size_t c = 0; c < sn.nchains; ++c)
318 Nchain[c] = static_cast<int>(std::llround(d.Nchain[c]));
319
320 const Matrix<T> Zc(1, sn.nchains, zero);
321 const Matrix<T> Zk(1, K, zero);
322 // The method travels as a NAME. pfqn_ncld resolves it where the
323 // reference does, past the degenerate returns, so the per-station
324 // factors below -- one station, no think time, a closed form no
325 // algorithm name selects -- never have to recognize a load-INDEPENDENT
326 // NC method such as 'ls'. See pfqn_ncld.h.
327 const std::string& pm = opt.method;
328 pfqn::NcOptions nopt;
329 nopt.samples = opt.samples;
330 nopt.seed = opt.seed;
331 nopt.tol = opt.tol;
332 const T atol = num_traits<T>::from_double(opt.tol);
333 if (std::isnan(lG)) lG = pfqn::pfqn_ncld(d.Lchain, Nchain, Zc, mu, pm, atol, nopt).lG;
334 out.lG = lG;
335
336 // A station is exponential in class r when its service law has one phase.
337 const auto is_exponential = [&](std::size_t i, std::size_t r) {
338 if (sn.disabled[i][r]) return true;
339 return sn.service[i][r].D0.rows() <= 1;
340 };
341
342 out.P.assign(M, zero);
343 out.logP.assign(M, zero);
344 for (std::size_t i = 0; i < M; ++i) {
345 bool ignore = false;
346 for (int v : nir[i])
347 if (v < 0) ignore = true;
348 if (ignore) continue;
349 const std::vector<int> nc = detail::to_chain(sn, nir[i]);
350 std::vector<int> Nrest(sn.nchains, 0);
351 for (std::size_t c = 0; c < sn.nchains; ++c) Nrest[c] = Nchain[c] - nc[c];
352 const double lG_minus_i =
353 M > 1 ? pfqn::pfqn_ncld(detail::drop_row(d.Lchain, i), Nrest, Zc,
354 detail::drop_row(mu, i), pm, atol, nopt)
355 .lG
356 : 0.0;
357
358 long ntot_i = 0;
359 for (int v : nir[i]) ntot_i += v;
360 // sum_{n=1}^{|n_i|} log mu_i(n), the load-dependent denominator
361 double lmu = 0.0;
362 for (long n = 1; n <= ntot_i && n <= static_cast<long>(mu.cols()); ++n)
363 lmu += std::log(num_traits<T>::to_double(mu(i, static_cast<std::size_t>(n - 1))));
364
365 double lF_i = 0.0;
366 const qn::SchedStrategy sc = sn.stations[i].sched;
367 if (sc == qn::SchedStrategy::FCFS) {
368 double stmax = 0.0;
369 for (std::size_t r = 0; r < K; ++r) {
370 if (sn.disabled[i][r]) continue;
371 if (!is_exponential(i, r))
372 throw UnsupportedError(
373 "solver_nc_marg: the product-form state probability requires "
374 "exponential service times at FCFS nodes, and this station's class " +
375 std::to_string(r + 1) + " is not exponential");
376 stmax = std::max(stmax, num_traits<T>::to_double(ST(i, r)));
377 }
378 for (std::size_t r = 0; r < K; ++r) {
379 if (sn.disabled[i][r] || nir[i][r] == 0) continue;
380 if (std::fabs(num_traits<T>::to_double(ST(i, r)) - stmax) >
382 throw UnsupportedError(
383 "solver_nc_marg: the product-form state probability requires "
384 "identical service times across classes at FCFS nodes, and this "
385 "station's class " + std::to_string(r + 1) + " differs");
386 }
387 if (ntot_i > 0) {
388 // REFERENCE DEFECT, corrected here: the reference writes
389 // `sum(nirvec .* log(V(ist,r)))` with `r` left over from the
390 // validation loop above it, so EVERY class is weighted by the
391 // LAST class's visit ratio. The intended term is each class's
392 // own visit, which is what the identity Pr[n] ~ prod_r V_ir^{n_ir}
393 // requires and what every other branch of this file uses.
394 for (std::size_t r = 0; r < K; ++r) {
395 if (nir[i][r] == 0) continue;
396 const double v = num_traits<T>::to_double(V(i, r));
397 if (!(v > 0.0))
398 throw NumericError(
399 "solver_nc_marg: class " + std::to_string(r + 1) +
400 " holds jobs at a station it never visits");
401 lF_i += static_cast<double>(nir[i][r]) * std::log(v);
402 }
403 lF_i -= lmu;
404 }
405 } else if (sc == qn::SchedStrategy::SIRO) {
406 throw UnsupportedError(
407 "solver_nc_marg: the SIRO branch weighs the state by log(n_ci / sum n), which "
408 "needs the CLASS OF THE JOB IN SERVICE; a per-class marginal does not carry "
409 "it. Use getProbAggr, whose aggregate marginal has no such dependency");
410 } else if (sc == qn::SchedStrategy::PS || sc == qn::SchedStrategy::INF) {
411 for (std::size_t r = 0; r < K; ++r) {
412 if (sn.disabled[i][r]) continue;
413 if (!is_exponential(i, r))
414 throw UnsupportedError(
415 "solver_nc_marg: a non-exponential service law at a " +
416 std::string(sc == qn::SchedStrategy::PS ? "PS" : "delay") +
417 " station makes the balance function depend on the PHASE-LEVEL "
418 "occupancy, which a per-class marginal does not carry");
419 if (nir[i][r] == 0) continue;
420 const double w = num_traits<T>::to_double(T(V(i, r) * ST(i, r)));
421 if (!(w > 0.0))
422 throw NumericError(
423 "solver_nc_marg: class " + std::to_string(r + 1) +
424 " holds jobs at a station whose demand for it is zero");
425 lF_i += static_cast<double>(nir[i][r]) * std::log(w);
426 for (int q = 2; q <= nir[i][r]; ++q) lF_i -= std::log(static_cast<double>(q));
427 }
428 for (long q = 2; q <= ntot_i; ++q) lF_i += std::log(static_cast<double>(q));
429 lF_i -= lmu;
430 }
431 // Any other discipline leaves lF_i at zero, as the reference's
432 // switch does when no case matches.
433 (void)one;
434
435 const double lp = lF_i + lG_minus_i - lG;
437 out.P[i] = num_traits<T>::from_double(std::exp(lp));
438 }
439 return out;
440 }
441}
442
443/**
444 * Port of `solver_nc_joint.m`: the probability of the WHOLE system state.
445 *
446 * The per-station factor is the chain-level balance function corrected by the
447 * class-within-chain split `lg0_i - lG0_i`, which is what turns a chain-level
448 * constant into a class-level one.
449 */
450template <class T>
452 const MarginalState& nir, double* lG_out) {
453 if constexpr (!num_traits<T>::has_transcendental) {
454 (void)sn; (void)opt; (void)nir; (void)lG_out;
455 throw UnsupportedError(
456 "solver_nc_joint: the joint state probability is a difference of logarithms of "
457 "normalizing constants and needs transcendental arithmetic");
458 } else {
459 const T zero = num_traits<T>::from_int(0);
460 const std::size_t M = sn.nstations, K = sn.nclasses;
461 detail::check_marginal(sn, nir, "solver_nc_joint");
462 const std::size_t Ntot = detail::closed_total(sn, "solver_nc_joint");
463
465 const Matrix<T> ST = detail::service_times(sn);
466 const Matrix<T> mu = detail::prob_mu(sn, Ntot);
467 std::vector<int> Nchain(sn.nchains, 0);
468 for (std::size_t c = 0; c < sn.nchains; ++c)
469 Nchain[c] = static_cast<int>(std::llround(d.Nchain[c]));
470
471 const Matrix<T> Zc(1, sn.nchains, zero);
472 const Matrix<T> Zk(1, K, zero);
473 // The method travels as a NAME. pfqn_ncld resolves it where the
474 // reference does, past the degenerate returns, so the per-station
475 // factors below -- one station, no think time, a closed form no
476 // algorithm name selects -- never have to recognize a load-INDEPENDENT
477 // NC method such as 'ls'. See pfqn_ncld.h.
478 const std::string& pm = opt.method;
479 pfqn::NcOptions nopt;
480 nopt.samples = opt.samples;
481 nopt.seed = opt.seed;
482 nopt.tol = opt.tol;
483 const T atol = num_traits<T>::from_double(opt.tol);
484 const double lG = pfqn::pfqn_ncld(d.Lchain, Nchain, Zc, mu, pm, atol, nopt).lG;
485 if (lG_out != nullptr) *lG_out = lG;
486
487 double lPr = 0.0;
488 for (std::size_t i = 0; i < M; ++i) {
489 const std::vector<int> nc = detail::to_chain(sn, nir[i]);
490 const Matrix<T> mui = detail::row_of(mu, i);
491 const double lF_i =
492 pfqn::pfqn_ncld(detail::row_of(d.Lchain, i), nc, Zc, mui, pm, atol, nopt).lG;
493 Matrix<T> g0(1, K, zero);
494 for (std::size_t r = 0; r < K; ++r) g0(0, r) = T(ST(i, r) * d.alpha(i, r));
495 const double lg0_i = pfqn::pfqn_ncld(g0, nir[i], Zk, mui, pm, atol, nopt).lG;
496 const double lG0_i =
497 pfqn::pfqn_ncld(detail::row_of(d.STchain, i), nc, Zc, mui, pm, atol, nopt).lG;
498 lPr += lF_i + (lg0_i - lG0_i);
499 }
500 return num_traits<T>::from_double(std::exp(lPr - lG));
501 }
502}
503
504/**
505 * Port of `solver_nc_jointaggr.m`: the aggregate joint.
506 *
507 * The reference takes its constant from `pfqn_ncld` under `method='exact'` and
508 * from `solver_nc` otherwise, noting in a comment that the second is "unclear
509 * ... as it doesn't consider the transformation to ld model". Both paths are
510 * reproduced, because the choice changes the answer and the caller's method is
511 * what selects it.
512 */
513template <class T>
515 const MarginalState& nir, double* lG_out) {
516 if constexpr (!num_traits<T>::has_transcendental) {
517 (void)sn; (void)opt; (void)nir; (void)lG_out;
518 throw UnsupportedError(
519 "solver_nc_jointaggr: the joint state probability is a difference of logarithms of "
520 "normalizing constants and needs transcendental arithmetic");
521 } else {
522 const T zero = num_traits<T>::from_int(0);
523 const std::size_t M = sn.nstations, K = sn.nclasses;
524 detail::check_marginal(sn, nir, "solver_nc_jointaggr");
525 const std::size_t Ntot = detail::closed_total(sn, "solver_nc_jointaggr");
526
528 const Matrix<T> mu = detail::prob_mu(sn, Ntot);
529 std::vector<int> Nchain(sn.nchains, 0);
530 for (std::size_t c = 0; c < sn.nchains; ++c)
531 Nchain[c] = static_cast<int>(std::llround(d.Nchain[c]));
532
533 const Matrix<T> Zc(1, sn.nchains, zero);
534 const Matrix<T> Zk(1, K, zero);
535 // The method travels as a NAME. pfqn_ncld resolves it where the
536 // reference does, past the degenerate returns, so the per-station
537 // factors below -- one station, no think time, a closed form no
538 // algorithm name selects -- never have to recognize a load-INDEPENDENT
539 // NC method such as 'ls'. See pfqn_ncld.h.
540 const std::string& pm = opt.method;
541 pfqn::NcOptions nopt;
542 nopt.samples = opt.samples;
543 nopt.seed = opt.seed;
544 nopt.tol = opt.tol;
545 const T atol = num_traits<T>::from_double(opt.tol);
546
547 double lG;
548 Matrix<T> ST;
549 if (opt.method == "exact") {
550 lG = pfqn::pfqn_ncld(d.Lchain, Nchain, Zc, mu, pm, atol, nopt).lG;
551 ST = detail::service_times(sn);
552 } else {
553 const NcSolution<T> s = solver_nc(sn, opt);
554 lG = s.sol.lG;
555 ST = s.STeff;
556 }
557 if (lG_out != nullptr) *lG_out = lG;
558
559 // V is cellsum(sn.visits) here, summed over chains, not the per-chain
560 // class visit the detailed marginal uses.
561 Matrix<T> V(M, K, zero);
562 for (std::size_t c = 0; c < sn.nchains; ++c)
563 for (std::size_t i = 0; i < M; ++i) {
564 const std::size_t sf = sn.stateful_of_station(i + 1) - 1;
565 for (std::size_t r = 0; r < K; ++r) V(i, r) = T(V(i, r) + sn.visits[c](sf, r));
566 }
567
568 double lPr = 0.0;
569 for (std::size_t i = 0; i < M; ++i) {
570 const std::vector<int> nc = detail::to_chain(sn, nir[i]);
571 bool any = false;
572 for (int v : nc)
573 if (v > 0) any = true;
574 if (!any) continue;
575 Matrix<T> Fi(1, K, zero);
576 for (std::size_t r = 0; r < K; ++r) Fi(0, r) = T(ST(i, r) * V(i, r));
577 lPr += pfqn::pfqn_ncld(Fi, nir[i], Zk, detail::row_of(mu, i), pm, atol, nopt).lG;
578 }
579 return num_traits<T>::from_double(std::exp(lPr - lG));
580 }
581}
582
583/**
584 * Port of `solver_nc_jointaggr_ld.m`: the load-dependent joint.
585 *
586 * Identical to `solver_nc_joint` except that the chain demand comes straight
587 * from `sn_get_demands_chain` rather than being rebuilt, which is what makes it
588 * the load-dependent variant in the reference.
589 */
590template <class T>
592 const MarginalState& nir, double* lG_out) {
593 return solver_nc_joint(sn, opt, nir, lG_out);
594}
595
596// ---------------------------------------------------------------------------
597// The @@SolverNC class surface
598// ---------------------------------------------------------------------------
599
600/**
601 * Port of `@@SolverNC/getProb.m`: the DETAILED state probability at one station.
602 *
603 * RETURNS THE LOG PROBABILITY, NOT THE PROBABILITY, DESPITE THE NAME. The value
604 * is therefore NEGATIVE and is not in [0,1]; `exp()` recovers the probability.
605 * On Delay(1)+PS(2) at N=3 with two jobs at the queue this returns
606 * -1.15267950993839, and the probability is exp of it, 0.31578947368.
607 *
608 * This is a DELIBERATE reproduction of the reference, not an oversight.
609 * `solver_nc_marg.m` returns `lPr` as its first output and
610 * `@@SolverNC/getProb.m` passes it through under the name `Pnir` without
611 * exponentiating. The port originally returned the probability and exposed the
612 * log separately; the user ruled for strict bug-for-bug parity with MATLAB over
613 * the safer API, and this is that decision. See register row N8, which records
614 * the argument on both sides.
615 *
616 * `getProbAggr` is unaffected and DOES return a probability, so the two
617 * accessors disagree in kind -- another reason the reference behaviour is
618 * surprising rather than merely unusual. Use `solver_nc_marg` directly for a
619 * result carrying both `P` and `logP`.
620 *
621 * @param ist 1-based station index
622 * @param nir the per-class occupancy at that station; other stations are left
623 * at the reference's "ignore" flag, since the detailed marginal is
624 * reported per station and only this one is asked for
625 * @param sn the refreshed network struct
626 * @param opt SolverNC's options
627 * @return the LOG of the probability
628 */
629template <class T>
631 const std::vector<int>& nir) {
632 if (ist == 0 || ist > sn.nstations)
633 throw InputError("getProb: station index out of range");
634 MarginalState st(sn.nstations, std::vector<int>(sn.nclasses, -1));
635 st[ist - 1] = nir;
636 return solver_nc_marg(sn, opt, st,
637 std::numeric_limits<double>::quiet_NaN())
638 .logP[ist - 1];
639}
640
641/** Port of `@@SolverNC/getProbAggr.m`: the AGGREGATE probability at one station. */
642template <class T>
644 std::size_t ist, const std::vector<int>& nir, double lG) {
645 if (ist == 0 || ist > sn.nstations)
646 throw InputError("getProbAggr: station index out of range");
647 MarginalState st(sn.nstations, std::vector<int>(sn.nclasses, -1));
648 st[ist - 1] = nir;
649 return solver_nc_margaggr(sn, opt, st, lG).P[ist - 1];
650}
651
652/** The marginal queue-length distribution and its logarithm. */
653template <class T>
655 std::vector<T> P; ///< P[n] = Pr[n jobs at the station], n = 0..sum(N)
656 std::vector<T> logP;
657};
658
659/**
660 * Port of `@@SolverNC/getProbMarg.m`: the TOTAL queue-length distribution.
661 *
662 * Two routes, as in the reference. Under `method='comom'` the whole vector
663 * comes from one `pfqn_procomom` solve; otherwise every total n is written as a
664 * sum over the per-class partitions of n and each partition goes through the
665 * aggregate marginal. The normalizing constant is computed ONCE and reused
666 * across the partitions, which is the reference's caching and matters here:
667 * the enumeration is otherwise quadratic in constants.
668 */
669template <class T>
671 const NcSolverOptions& opt, std::size_t ist) {
673 if constexpr (!num_traits<T>::has_transcendental) {
674 (void)sn; (void)opt; (void)ist;
675 throw UnsupportedError(
676 "getProbMarg: the queue-length distribution is assembled from exponentiated "
677 "log-constants and needs transcendental arithmetic");
678 } else {
679 const T zero = num_traits<T>::from_int(0);
680 if (ist == 0 || ist > sn.nstations)
681 throw InputError("getProbMarg: station index out of range");
682 const std::size_t K = sn.nclasses;
683 const std::size_t Ntot = detail::closed_total(sn, "getProbMarg");
684
685 std::vector<int> N(K, 0);
686 for (std::size_t r = 0; r < K; ++r)
687 N[r] = static_cast<int>(std::llround(sn.classes[r].population));
688
689 out.P.assign(Ntot + 1, zero);
690 out.logP.assign(Ntot + 1, num_traits<T>::from_double(
691 -std::numeric_limits<double>::infinity()));
692
693 if (opt.method == "comom") {
694 // The fast path: pfqn_procomom returns the whole per-station
695 // queue-length vector in one solve, on the Seidmann-reduced model.
697 const std::size_t M = sn.nstations, C = sn.nchains;
698 Matrix<T> Lms(M, C, zero);
699 Matrix<T> Ztot(1, C, zero);
700 std::vector<std::size_t> queueStations;
701 for (std::size_t i = 0; i < M; ++i) {
702 const double S = sn.stations[i].nservers;
703 if (std::isinf(S)) {
704 for (std::size_t c = 0; c < C; ++c) Ztot(0, c) += d.Lchain(i, c);
705 } else {
706 queueStations.push_back(i);
707 const T cs = num_traits<T>::from_double(S);
708 for (std::size_t c = 0; c < C; ++c) {
709 Lms(i, c) = T(d.Lchain(i, c) / cs);
710 Ztot(0, c) += T(d.Lchain(i, c) * num_traits<T>::from_double(S - 1.0) / cs);
711 }
712 }
713 }
714 const auto pos = std::find(queueStations.begin(), queueStations.end(), ist - 1);
715 if (pos == queueStations.end()) {
716 // A DELAY station has no row in the procomom result. The
717 // reference warns and FALLS BACK to the enumeration under
718 // method='default' rather than refusing, so this does too.
719 NcSolverOptions fallback = opt;
720 fallback.method = "default";
721 return solver_nc_getprob_marg(sn, fallback, ist);
722 }
723 Matrix<T> Lq(queueStations.size(), C, zero);
724 for (std::size_t a = 0; a < queueStations.size(); ++a)
725 for (std::size_t c = 0; c < C; ++c) Lq(a, c) = Lms(queueStations[a], c);
726 std::vector<int> Nchain(C, 0);
727 std::size_t sumNchain = 0;
728 for (std::size_t c = 0; c < C; ++c) {
729 Nchain[c] = static_cast<int>(std::llround(d.Nchain[c]));
730 sumNchain += static_cast<std::size_t>(Nchain[c]);
731 }
732 std::vector<T> Zv(C, zero);
733 for (std::size_t c = 0; c < C; ++c) Zv[c] = Ztot(0, c);
734 const Matrix<T> Pr = pfqn::pfqn_procomom(Lq, Nchain, Zv).Pr;
735 const std::size_t row = static_cast<std::size_t>(pos - queueStations.begin());
736 const std::size_t len = std::min(sumNchain + 1, Ntot + 1);
737 for (std::size_t n = 0; n < len && n < Pr.cols(); ++n) {
738 out.P[n] = Pr(row, n);
739 if (num_traits<T>::to_double(out.P[n]) > 0.0)
741 std::log(num_traits<T>::to_double(out.P[n])));
742 }
743 return out;
744 }
745
746 // The enumeration. lG is computed once and handed to every call.
748 const Matrix<T> mu = detail::prob_mu(sn, Ntot);
749 std::vector<int> Nchain(sn.nchains, 0);
750 for (std::size_t c = 0; c < sn.nchains; ++c)
751 Nchain[c] = static_cast<int>(std::llround(d.Nchain[c]));
752 const double lG =
753 pfqn::pfqn_ncld(d.Lchain, Nchain, Matrix<T>(1, sn.nchains, zero), mu,
754 opt.method,
756 .lG;
757
758 std::vector<int> part(K, 0);
759 for (std::size_t n = 0; n <= Ntot; ++n) {
760 double acc = 0.0;
761 bool any = false;
762 // enumerate the compositions of n into K parts with part_r <= N_r
763 std::function<void(std::size_t, int)> rec = [&](std::size_t r, int left) {
764 if (r + 1 == K) {
765 if (left > N[r]) return;
766 part[r] = left;
767 const T p = solver_nc_getprob_aggr(sn, opt, ist, part, lG);
768 const double v = num_traits<T>::to_double(p);
769 if (v > 0.0) {
770 acc += v;
771 any = true;
772 }
773 return;
774 }
775 const int hi = std::min(left, N[r]);
776 for (int v = 0; v <= hi; ++v) {
777 part[r] = v;
778 rec(r + 1, left - v);
779 }
780 };
781 if (K == 0) continue;
782 rec(0, static_cast<int>(n));
783 if (any) {
784 out.P[n] = num_traits<T>::from_double(acc);
785 out.logP[n] = num_traits<T>::from_double(std::log(acc));
786 }
787 }
788 // THE REFERENCE RENORMALIZES, and so must this: `getProbMarg.m` divides
789 // the whole vector by its sum whenever that sum misses one by more than
790 // 1e-10. The gap is the normalizing constant's own error -- on the
791 // default (`cub`) route lG is an approximation, and the per-partition
792 // constants inherit it -- so without this the marginal law of a station
793 // is not a distribution. The 1e-10 dead band is the reference's too: it
794 // leaves a vector already summing to one bit-for-bit alone rather than
795 // perturbing it by a division.
796 double total = 0.0;
797 for (std::size_t n = 0; n <= Ntot; ++n) total += num_traits<T>::to_double(out.P[n]);
798 if (total > 0.0 && std::fabs(total - 1.0) > 1e-10) {
799 const double ltotal = std::log(total);
800 for (std::size_t n = 0; n <= Ntot; ++n) {
801 out.P[n] = num_traits<T>::from_double(num_traits<T>::to_double(out.P[n]) / total);
802 if (num_traits<T>::to_double(out.P[n]) > 0.0)
803 out.logP[n] =
805 }
806 }
807 return out;
808 }
809}
810
811/** Port of `@@SolverNC/getProbSys.m`. */
812template <class T>
814 const MarginalState& nir) {
815 return solver_nc_joint(sn, opt, nir, nullptr);
816}
817
818/** Port of `@@SolverNC/getProbSysAggr.m`. */
819template <class T>
821 const MarginalState& nir) {
822 return solver_nc_jointaggr(sn, opt, nir, nullptr);
823}
824
825namespace detail {
826
827/**
828 * The permanent identity supplies one n_i! per queueing station and none per
829 * infinite server. A multiserver or load-dependent station has neither, so it
830 * is refused BY NAME rather than approximated.
831 */
832template <class T>
833void check_jointmarg_supported(const qn::NetworkStruct<T>& sn) {
834 for (const qn::JobClass& c : sn.classes)
835 if (std::isinf(c.population))
836 throw UnsupportedError(
837 "solver_nc_jointmarg: getProbSysMarg requires a closed model: the joint law of "
838 "the total queue lengths is not defined when a class has an infinite population");
839 for (std::size_t i = 0; i < sn.nstations; ++i) {
840 if (!sn.stations[i].lldscaling.empty())
841 throw UnsupportedError(
842 "solver_nc_jointmarg: getProbSysMarg does not support load-dependent stations "
843 "(sn.lldscaling is set at station " + std::to_string(i + 1) + "): the permanent "
844 "identity supplies exactly one n_i! per queueing station");
845 const double S = sn.stations[i].nservers;
846 if (std::isfinite(S) && S > 1.0)
847 throw UnsupportedError(
848 "solver_nc_jointmarg: getProbSysMarg does not support the multiserver station " +
849 std::to_string(i + 1) + " (" + std::to_string(static_cast<int>(S)) +
850 " servers): the permanent identity supplies exactly one n_i! per queueing "
851 "station");
852 }
853}
854
855} // namespace detail
856
857/**
858 * Joint probability that station i holds `nvec[i]` jobs IN TOTAL, all classes
859 * summed out.
860 *
861 * This is NOT `solver_nc_jointaggr`, which fixes the per-class population of
862 * every station: each state here is the SUM of jointaggr over the whole fibre
863 * of per-class tables with these row sums, and that fibre grows
864 * combinatorially. `pfqn_jointmarg` evaluates the sum in closed form as a
865 * permanent of the demand matrix replicated once per job.
866 *
867 * @param nvec (M) per-station total job counts
868 * @param engine "exact" (default), "spm", "bethe", "heur", "huberlaw" or
869 * "adapart"; see pfqn_jointmarg for what each guarantees
870 * @param lG_out receives the log normalizing constant when not null
871 */
872template <class T>
874 const std::vector<int>& nvec, const std::string& engine = "exact",
875 double* lG_out = nullptr) {
876 const std::size_t M = sn.nstations;
877 if (nvec.size() != M)
878 throw InputError("solver_nc_jointmarg: the occupancy vector has " +
879 std::to_string(nvec.size()) + " entries but the model has " +
880 std::to_string(M) + " stations");
881 detail::check_jointmarg_supported(sn);
882
884 const T zero = num_traits<T>::from_int(0);
885 Matrix<T> Lchain = d.Lchain;
886 for (std::size_t i = 0; i < Lchain.rows(); ++i)
887 for (std::size_t c = 0; c < Lchain.cols(); ++c)
888 if (!std::isfinite(num_traits<T>::to_double(Lchain(i, c)))) Lchain(i, c) = zero;
889 std::vector<int> Nchain(sn.nchains, 0);
890 for (std::size_t c = 0; c < sn.nchains; ++c)
891 Nchain[c] = static_cast<int>(std::llround(d.Nchain[c]));
892
893 std::vector<std::size_t> infset;
894 for (std::size_t i = 0; i < M; ++i)
895 if (std::isinf(sn.stations[i].nservers)) infset.push_back(i);
896
897 // G does not depend on how the delay stations are split: they aggregate by
898 // the multinomial theorem, so it is taken with the infinite-server rows
899 // summed into the think time.
900 std::vector<bool> isinf(M, false);
901 for (std::size_t k = 0; k < infset.size(); ++k) isinf[infset[k]] = true;
902 std::size_t nq = 0;
903 for (std::size_t i = 0; i < M; ++i)
904 if (!isinf[i]) ++nq;
905 Matrix<T> Lq(nq, sn.nchains, zero);
906 std::size_t a = 0;
907 for (std::size_t i = 0; i < M; ++i) {
908 if (isinf[i]) continue;
909 for (std::size_t c = 0; c < sn.nchains; ++c) Lq(a, c) = Lchain(i, c);
910 ++a;
911 }
912 Matrix<T> Z;
913 if (!infset.empty()) {
914 Z = Matrix<T>(1, sn.nchains, zero);
915 for (std::size_t k = 0; k < infset.size(); ++k)
916 for (std::size_t c = 0; c < sn.nchains; ++c) Z(0, c) += Lchain(infset[k], c);
917 }
918 const pfqn::NcResult<T> ca = pfqn::pfqn_ca(Lq, Nchain, Z);
919 if (lG_out != nullptr) *lG_out = num_traits<T>::to_double(ca.lG);
920
921 return pfqn::pfqn_jointmarg(nvec, Lchain, Nchain, infset, ca.G, engine,
922 static_cast<std::uint64_t>(opt.seed));
923}
924
925/**
926 * Port of `@@SolverNC/getProbSysMarg.m`.
927 *
928 * Compare with `solver_nc_getprob_sys_aggr`, which fixes the PER-CLASS
929 * population of every station and is a product form; each value returned here
930 * is the sum of that one over every per-class table with these row sums.
931 */
932template <class T>
934 const std::vector<int>& nvec, const std::string& engine = "exact") {
935 return solver_nc_jointmarg(sn, opt, nvec, engine, nullptr);
936}
937
938} // namespace nc
939} // namespace line
940
941#endif // LINE_SOLVERS_NC_SOLVER_NC_PROB_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
NumericError(const std::string &what)
Definition error.h:45
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
What refreshProcessRepresentations and refreshLST compute FROM a distribution: the (D0,...
The exception types the port throws.
Dense matrix and non-owning view.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
T solver_nc_jointmarg(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const std::vector< int > &nvec, const std::string &engine="exact", double *lG_out=nullptr)
Joint probability that station i holds nvec[i] jobs IN TOTAL, all classes summed out.
T solver_nc_jointaggr(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const MarginalState &nir, double *lG_out)
Port of solver_nc_jointaggr.m: the aggregate joint.
T solver_nc_getprob_sys_marg(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const std::vector< int > &nvec, const std::string &engine="exact")
Port of @@SolverNC/getProbSysMarg.m.
T solver_nc_getprob_sys_aggr(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const MarginalState &nir)
Port of @@SolverNC/getProbSysAggr.m.
T solver_nc_joint(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const MarginalState &nir, double *lG_out)
Port of solver_nc_joint.m: the probability of the WHOLE system state.
NcSolution< T > solver_nc(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc.m.
Definition solver_nc.h:142
T solver_nc_getprob_sys(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const MarginalState &nir)
Port of @@SolverNC/getProbSys.m.
NcQueueLengthDist< T > solver_nc_getprob_marg(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, std::size_t ist)
Port of @@SolverNC/getProbMarg.m: the TOTAL queue-length distribution.
NcMargResult< T > solver_nc_margaggr(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const MarginalState &nir, double lG)
Port of solver_nc_margaggr.m.
NcMargResult< T > solver_nc_marg(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const MarginalState &nir, double lG)
Port of solver_nc_marg.m: the DETAILED marginal, which weighs the station's internal arrangement and ...
std::vector< std::vector< int > > MarginalState
A state, as this port expresses it: nir[i][r] jobs of class r at station i.
T solver_nc_getprob(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, std::size_t ist, const std::vector< int > &nir)
Port of @@SolverNC/getProb.m: the DETAILED state probability at one station.
T solver_nc_jointaggr_ld(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, const MarginalState &nir, double *lG_out)
Port of solver_nc_jointaggr_ld.m: the load-dependent joint.
T solver_nc_getprob_aggr(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, std::size_t ist, const std::vector< int > &nir, double lG)
Port of @@SolverNC/getProbAggr.m: the AGGREGATE probability at one station.
ProcomomResult< T > pfqn_procomom(const Matrix< T > &L, const std::vector< int > &N, const std::vector< T > &Z, const T &atol)
Marginal queue-length distributions of every station.
NcldResult< T > pfqn_ncld(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const Matrix< T > &mu, const std::string &method_name, const T &atol, const NcOptions &nopt)
Normalizing constant of a LOAD-DEPENDENT closed network: the dispatcher.
Definition pfqn_ncld.h:167
NcResult< T > pfqn_ca(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z)
Convolution algorithm for the exact normalizing constant of a closed product-form network (Buzen 1973...
Definition pfqn_ca.h:120
T pfqn_jointmarg(const std::vector< int > &n, const Matrix< T > &L, const std::vector< int > &N, const std::vector< std::size_t > &infset, const T &G, const std::string &engine="exact", std::uint64_t seed=0)
Joint probability of the per-station TOTAL queue lengths.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Controls and result shape shared by the normalizing-constant analyzers.
A queueing network and its refreshed NetworkStruct.
Convolution algorithm for the exact normalizing constant of a closed product-form network (Buzen 1973...
Joint probability of the per-station TOTAL queue lengths.
Normalizing constant of a LOAD-DEPENDENT closed network: the dispatcher.
ProCoMoM: marginal queue-length probabilities of a closed multiclass product-form network by the clas...
Chain aggregation and de-aggregation.
Port of solver_nc.m: the load-INDEPENDENT normalizing-constant analyzer.
Port of solver_ncld.m: the LOAD-DEPENDENT normalizing-constant analyzer.
The chain-level view of a layer, as sn_get_demands_chain returns it.
Definition sn_chain.h:46
std::vector< double > Nchain
(C) population, infinite for an open chain
Definition sn_chain.h:51
Matrix< T > alpha
(M x K) class share of its chain's visits at a station
Definition sn_chain.h:50
Matrix< T > STchain
(M x C) mean service time
Definition sn_chain.h:48
Matrix< T > Lchain
(M x C) demand
Definition sn_chain.h:47
static constexpr double FineTol
Definition lang_types.h:760
What the marginal analyzers return: one probability per station.
std::vector< T > P
(M) probability that station i holds its given vector
double lG
the log normalizing constant that normalized them
std::vector< T > logP
(M) the same, in logs
The marginal queue-length distribution and its logarithm.
std::vector< T > P
P[n] = Pr[n jobs at the station], n = 0..sum(N).
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
Matrix< T > STeff
the service times of the last pass, MATLAB's STeff
Definition nc_types.h:116
Controls, defaulting to SolverOptions('NC') in the reference.
Definition nc_types.h:33
The options fields compute_norm_const reads beyond the method itself.
Definition pfqn_nc.h:215
unsigned long seed
SolverOptions('NC').seed.
Definition pfqn_nc.h:217
std::size_t samples
SolverOptions('NC').samples.
Definition pfqn_nc.h:216
double tol
handed to pfqn_comomrm
Definition pfqn_nc.h:218
Return value of the normalizing-constant family, mirroring Ret.pfqnNc.
Definition pfqn_ca.h:44
T G
normalizing constant in the requested arithmetic
Definition pfqn_ca.h:45
double lG
log of the constant, always a double and always finite
Definition pfqn_ca.h:46
One job class of the network.
double population
infinite for an open class