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