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 const pfqn::NcldMethod pm = detail::ncld_pfqn_method(opt.method);
237 pfqn::NcOptions nopt;
238 nopt.samples = opt.samples;
239 nopt.seed = opt.seed;
240 nopt.tol = opt.tol;
241 const T atol = num_traits<T>::from_double(opt.tol);
242 if (std::isnan(lG))
243 lG = pfqn::pfqn_ncld(d.Lchain, Nchain, Zc, mu, pm, atol, nopt).lG;
244 out.lG = lG;
245
246 out.P.assign(M, zero);
247 out.logP.assign(M, zero);
248 for (std::size_t i = 0; i < M; ++i) {
249 bool ignore = false;
250 for (int v : nir[i])
251 if (v < 0) ignore = true;
252 if (ignore) continue; // MATLAB sets NaN here and then Pr(isnan)=0
253 const std::vector<int> nc = detail::to_chain(sn, nir[i]);
254 std::vector<int> Nrest(sn.nchains, 0);
255 for (std::size_t c = 0; c < sn.nchains; ++c) Nrest[c] = Nchain[c] - nc[c];
256 const double lG_minus_i =
257 M > 1 ? pfqn::pfqn_ncld(detail::drop_row(d.Lchain, i), Nrest, Zc,
258 detail::drop_row(mu, i), pm, atol, nopt)
259 .lG
260 : 0.0;
261 Matrix<T> Fi(1, K, zero);
262 for (std::size_t r = 0; r < K; ++r) Fi(0, r) = T(ST(i, r) * V(i, r));
263 const double lF_i =
264 pfqn::pfqn_ncld(Fi, nir[i], Zk, detail::row_of(mu, i), pm, atol, nopt).lG;
265 const double lp = lF_i + lG_minus_i - lG;
267 out.P[i] = num_traits<T>::from_double(std::exp(lp));
268 }
269 return out;
270 }
271}
272
273/**
274 * Port of `solver_nc_marg.m`: the DETAILED marginal, which weighs the station's
275 * internal arrangement and therefore depends on its discipline.
276 *
277 * TWO BRANCHES ARE REFUSED BY NAME because a per-class marginal cannot carry
278 * what they read, and answering them from the default arrangement would be a
279 * fabricated number:
280 *
281 * SIRO wants the CLASS OF THE JOB IN SERVICE (`sivec`), whose term is
282 * log(n_ci / sum n). A marginal says how many jobs of each class are
283 * present, not which one holds the server.
284 * PS and INF want the PHASE-LEVEL occupancy (`kirvec`) when service is not
285 * exponential. Under exponential service kirvec IS the marginal, and
286 * that case is computed exactly.
287 *
288 * FCFS additionally carries the reference's own preconditions -- exponential
289 * service, and identical mean service time across the classes -- without which
290 * the station is not product-form and the reference errors out.
291 */
292template <class T>
294 const MarginalState& nir, double lG) {
295 NcMargResult<T> out;
296 if constexpr (!num_traits<T>::has_transcendental) {
297 (void)sn; (void)opt; (void)nir; (void)lG;
298 throw UnsupportedError(
299 "solver_nc_marg: the state probability is exp(lF_i + lG_{-i} - lG), a difference of "
300 "logarithms of normalizing constants, and needs transcendental arithmetic");
301 } else {
302 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
303 const std::size_t M = sn.nstations, K = sn.nclasses;
304 detail::check_marginal(sn, nir, "solver_nc_marg");
305 const std::size_t Ntot = detail::closed_total(sn, "solver_nc_marg");
306
308 const Matrix<T> ST = detail::service_times(sn);
309 const Matrix<T> V = detail::class_visits(sn);
310 const Matrix<T> mu = detail::prob_mu(sn, Ntot);
311 std::vector<int> Nchain(sn.nchains, 0);
312 for (std::size_t c = 0; c < sn.nchains; ++c)
313 Nchain[c] = static_cast<int>(std::llround(d.Nchain[c]));
314
315 const Matrix<T> Zc(1, sn.nchains, zero);
316 const Matrix<T> Zk(1, K, zero);
317 const pfqn::NcldMethod pm = detail::ncld_pfqn_method(opt.method);
318 pfqn::NcOptions nopt;
319 nopt.samples = opt.samples;
320 nopt.seed = opt.seed;
321 nopt.tol = opt.tol;
322 const T atol = num_traits<T>::from_double(opt.tol);
323 if (std::isnan(lG)) lG = pfqn::pfqn_ncld(d.Lchain, Nchain, Zc, mu, pm, atol, nopt).lG;
324 out.lG = lG;
325
326 // A station is exponential in class r when its service law has one phase.
327 const auto is_exponential = [&](std::size_t i, std::size_t r) {
328 if (sn.disabled[i][r]) return true;
329 return sn.service[i][r].D0.rows() <= 1;
330 };
331
332 out.P.assign(M, zero);
333 out.logP.assign(M, zero);
334 for (std::size_t i = 0; i < M; ++i) {
335 bool ignore = false;
336 for (int v : nir[i])
337 if (v < 0) ignore = true;
338 if (ignore) continue;
339 const std::vector<int> nc = detail::to_chain(sn, nir[i]);
340 std::vector<int> Nrest(sn.nchains, 0);
341 for (std::size_t c = 0; c < sn.nchains; ++c) Nrest[c] = Nchain[c] - nc[c];
342 const double lG_minus_i =
343 M > 1 ? pfqn::pfqn_ncld(detail::drop_row(d.Lchain, i), Nrest, Zc,
344 detail::drop_row(mu, i), pm, atol, nopt)
345 .lG
346 : 0.0;
347
348 long ntot_i = 0;
349 for (int v : nir[i]) ntot_i += v;
350 // sum_{n=1}^{|n_i|} log mu_i(n), the load-dependent denominator
351 double lmu = 0.0;
352 for (long n = 1; n <= ntot_i && n <= static_cast<long>(mu.cols()); ++n)
353 lmu += std::log(num_traits<T>::to_double(mu(i, static_cast<std::size_t>(n - 1))));
354
355 double lF_i = 0.0;
356 const qn::SchedStrategy sc = sn.stations[i].sched;
357 if (sc == qn::SchedStrategy::FCFS) {
358 double stmax = 0.0;
359 for (std::size_t r = 0; r < K; ++r) {
360 if (sn.disabled[i][r]) continue;
361 if (!is_exponential(i, r))
362 throw UnsupportedError(
363 "solver_nc_marg: the product-form state probability requires "
364 "exponential service times at FCFS nodes, and this station's class " +
365 std::to_string(r + 1) + " is not exponential");
366 stmax = std::max(stmax, num_traits<T>::to_double(ST(i, r)));
367 }
368 for (std::size_t r = 0; r < K; ++r) {
369 if (sn.disabled[i][r] || nir[i][r] == 0) continue;
370 if (std::fabs(num_traits<T>::to_double(ST(i, r)) - stmax) >
372 throw UnsupportedError(
373 "solver_nc_marg: the product-form state probability requires "
374 "identical service times across classes at FCFS nodes, and this "
375 "station's class " + std::to_string(r + 1) + " differs");
376 }
377 if (ntot_i > 0) {
378 // REFERENCE DEFECT, corrected here: the reference writes
379 // `sum(nirvec .* log(V(ist,r)))` with `r` left over from the
380 // validation loop above it, so EVERY class is weighted by the
381 // LAST class's visit ratio. The intended term is each class's
382 // own visit, which is what the identity Pr[n] ~ prod_r V_ir^{n_ir}
383 // requires and what every other branch of this file uses.
384 for (std::size_t r = 0; r < K; ++r) {
385 if (nir[i][r] == 0) continue;
386 const double v = num_traits<T>::to_double(V(i, r));
387 if (!(v > 0.0))
388 throw NumericError(
389 "solver_nc_marg: class " + std::to_string(r + 1) +
390 " holds jobs at a station it never visits");
391 lF_i += static_cast<double>(nir[i][r]) * std::log(v);
392 }
393 lF_i -= lmu;
394 }
395 } else if (sc == qn::SchedStrategy::SIRO) {
396 throw UnsupportedError(
397 "solver_nc_marg: the SIRO branch weighs the state by log(n_ci / sum n), which "
398 "needs the CLASS OF THE JOB IN SERVICE; a per-class marginal does not carry "
399 "it. Use getProbAggr, whose aggregate marginal has no such dependency");
400 } else if (sc == qn::SchedStrategy::PS || sc == qn::SchedStrategy::INF) {
401 for (std::size_t r = 0; r < K; ++r) {
402 if (sn.disabled[i][r]) continue;
403 if (!is_exponential(i, r))
404 throw UnsupportedError(
405 "solver_nc_marg: a non-exponential service law at a " +
406 std::string(sc == qn::SchedStrategy::PS ? "PS" : "delay") +
407 " station makes the balance function depend on the PHASE-LEVEL "
408 "occupancy, which a per-class marginal does not carry");
409 if (nir[i][r] == 0) continue;
410 const double w = num_traits<T>::to_double(T(V(i, r) * ST(i, r)));
411 if (!(w > 0.0))
412 throw NumericError(
413 "solver_nc_marg: class " + std::to_string(r + 1) +
414 " holds jobs at a station whose demand for it is zero");
415 lF_i += static_cast<double>(nir[i][r]) * std::log(w);
416 for (int q = 2; q <= nir[i][r]; ++q) lF_i -= std::log(static_cast<double>(q));
417 }
418 for (long q = 2; q <= ntot_i; ++q) lF_i += std::log(static_cast<double>(q));
419 lF_i -= lmu;
420 }
421 // Any other discipline leaves lF_i at zero, as the reference's
422 // switch does when no case matches.
423 (void)one;
424
425 const double lp = lF_i + lG_minus_i - lG;
427 out.P[i] = num_traits<T>::from_double(std::exp(lp));
428 }
429 return out;
430 }
431}
432
433/**
434 * Port of `solver_nc_joint.m`: the probability of the WHOLE system state.
435 *
436 * The per-station factor is the chain-level balance function corrected by the
437 * class-within-chain split `lg0_i - lG0_i`, which is what turns a chain-level
438 * constant into a class-level one.
439 */
440template <class T>
442 const MarginalState& nir, double* lG_out) {
443 if constexpr (!num_traits<T>::has_transcendental) {
444 (void)sn; (void)opt; (void)nir; (void)lG_out;
445 throw UnsupportedError(
446 "solver_nc_joint: the joint state probability is a difference of logarithms of "
447 "normalizing constants and needs transcendental arithmetic");
448 } else {
449 const T zero = num_traits<T>::from_int(0);
450 const std::size_t M = sn.nstations, K = sn.nclasses;
451 detail::check_marginal(sn, nir, "solver_nc_joint");
452 const std::size_t Ntot = detail::closed_total(sn, "solver_nc_joint");
453
455 const Matrix<T> ST = detail::service_times(sn);
456 const Matrix<T> mu = detail::prob_mu(sn, Ntot);
457 std::vector<int> Nchain(sn.nchains, 0);
458 for (std::size_t c = 0; c < sn.nchains; ++c)
459 Nchain[c] = static_cast<int>(std::llround(d.Nchain[c]));
460
461 const Matrix<T> Zc(1, sn.nchains, zero);
462 const Matrix<T> Zk(1, K, zero);
463 const pfqn::NcldMethod pm = detail::ncld_pfqn_method(opt.method);
464 pfqn::NcOptions nopt;
465 nopt.samples = opt.samples;
466 nopt.seed = opt.seed;
467 nopt.tol = opt.tol;
468 const T atol = num_traits<T>::from_double(opt.tol);
469 const double lG = pfqn::pfqn_ncld(d.Lchain, Nchain, Zc, mu, pm, atol, nopt).lG;
470 if (lG_out != nullptr) *lG_out = lG;
471
472 double lPr = 0.0;
473 for (std::size_t i = 0; i < M; ++i) {
474 const std::vector<int> nc = detail::to_chain(sn, nir[i]);
475 const Matrix<T> mui = detail::row_of(mu, i);
476 const double lF_i =
477 pfqn::pfqn_ncld(detail::row_of(d.Lchain, i), nc, Zc, mui, pm, atol, nopt).lG;
478 Matrix<T> g0(1, K, zero);
479 for (std::size_t r = 0; r < K; ++r) g0(0, r) = T(ST(i, r) * d.alpha(i, r));
480 const double lg0_i = pfqn::pfqn_ncld(g0, nir[i], Zk, mui, pm, atol, nopt).lG;
481 const double lG0_i =
482 pfqn::pfqn_ncld(detail::row_of(d.STchain, i), nc, Zc, mui, pm, atol, nopt).lG;
483 lPr += lF_i + (lg0_i - lG0_i);
484 }
485 return num_traits<T>::from_double(std::exp(lPr - lG));
486 }
487}
488
489/**
490 * Port of `solver_nc_jointaggr.m`: the aggregate joint.
491 *
492 * The reference takes its constant from `pfqn_ncld` under `method='exact'` and
493 * from `solver_nc` otherwise, noting in a comment that the second is "unclear
494 * ... as it doesn't consider the transformation to ld model". Both paths are
495 * reproduced, because the choice changes the answer and the caller's method is
496 * what selects it.
497 */
498template <class T>
500 const MarginalState& nir, double* lG_out) {
501 if constexpr (!num_traits<T>::has_transcendental) {
502 (void)sn; (void)opt; (void)nir; (void)lG_out;
503 throw UnsupportedError(
504 "solver_nc_jointaggr: the joint state probability is a difference of logarithms of "
505 "normalizing constants and needs transcendental arithmetic");
506 } else {
507 const T zero = num_traits<T>::from_int(0);
508 const std::size_t M = sn.nstations, K = sn.nclasses;
509 detail::check_marginal(sn, nir, "solver_nc_jointaggr");
510 const std::size_t Ntot = detail::closed_total(sn, "solver_nc_jointaggr");
511
513 const Matrix<T> mu = detail::prob_mu(sn, Ntot);
514 std::vector<int> Nchain(sn.nchains, 0);
515 for (std::size_t c = 0; c < sn.nchains; ++c)
516 Nchain[c] = static_cast<int>(std::llround(d.Nchain[c]));
517
518 const Matrix<T> Zc(1, sn.nchains, zero);
519 const Matrix<T> Zk(1, K, zero);
520 const pfqn::NcldMethod pm = detail::ncld_pfqn_method(opt.method);
521 pfqn::NcOptions nopt;
522 nopt.samples = opt.samples;
523 nopt.seed = opt.seed;
524 nopt.tol = opt.tol;
525 const T atol = num_traits<T>::from_double(opt.tol);
526
527 double lG;
528 Matrix<T> ST;
529 if (opt.method == "exact") {
530 lG = pfqn::pfqn_ncld(d.Lchain, Nchain, Zc, mu, pm, atol, nopt).lG;
531 ST = detail::service_times(sn);
532 } else {
533 const NcSolution<T> s = solver_nc(sn, opt);
534 lG = s.sol.lG;
535 ST = s.STeff;
536 }
537 if (lG_out != nullptr) *lG_out = lG;
538
539 // V is cellsum(sn.visits) here, summed over chains, not the per-chain
540 // class visit the detailed marginal uses.
541 Matrix<T> V(M, K, zero);
542 for (std::size_t c = 0; c < sn.nchains; ++c)
543 for (std::size_t i = 0; i < M; ++i) {
544 const std::size_t sf = sn.stateful_of_station(i + 1) - 1;
545 for (std::size_t r = 0; r < K; ++r) V(i, r) = T(V(i, r) + sn.visits[c](sf, r));
546 }
547
548 double lPr = 0.0;
549 for (std::size_t i = 0; i < M; ++i) {
550 const std::vector<int> nc = detail::to_chain(sn, nir[i]);
551 bool any = false;
552 for (int v : nc)
553 if (v > 0) any = true;
554 if (!any) continue;
555 Matrix<T> Fi(1, K, zero);
556 for (std::size_t r = 0; r < K; ++r) Fi(0, r) = T(ST(i, r) * V(i, r));
557 lPr += pfqn::pfqn_ncld(Fi, nir[i], Zk, detail::row_of(mu, i), pm, atol, nopt).lG;
558 }
559 return num_traits<T>::from_double(std::exp(lPr - lG));
560 }
561}
562
563/**
564 * Port of `solver_nc_jointaggr_ld.m`: the load-dependent joint.
565 *
566 * Identical to `solver_nc_joint` except that the chain demand comes straight
567 * from `sn_get_demands_chain` rather than being rebuilt, which is what makes it
568 * the load-dependent variant in the reference.
569 */
570template <class T>
572 const MarginalState& nir, double* lG_out) {
573 return solver_nc_joint(sn, opt, nir, lG_out);
574}
575
576// ---------------------------------------------------------------------------
577// The @@SolverNC class surface
578// ---------------------------------------------------------------------------
579
580/**
581 * Port of `@@SolverNC/getProb.m`: the DETAILED state probability at one station.
582 *
583 * RETURNS THE LOG PROBABILITY, NOT THE PROBABILITY, DESPITE THE NAME. The value
584 * is therefore NEGATIVE and is not in [0,1]; `exp()` recovers the probability.
585 * On Delay(1)+PS(2) at N=3 with two jobs at the queue this returns
586 * -1.15267950993839, and the probability is exp of it, 0.31578947368.
587 *
588 * This is a DELIBERATE reproduction of the reference, not an oversight.
589 * `solver_nc_marg.m` returns `lPr` as its first output and
590 * `@@SolverNC/getProb.m` passes it through under the name `Pnir` without
591 * exponentiating. The port originally returned the probability and exposed the
592 * log separately; the user ruled for strict bug-for-bug parity with MATLAB over
593 * the safer API, and this is that decision. See register row N8, which records
594 * the argument on both sides.
595 *
596 * `getProbAggr` is unaffected and DOES return a probability, so the two
597 * accessors disagree in kind -- another reason the reference behaviour is
598 * surprising rather than merely unusual. Use `solver_nc_marg` directly for a
599 * result carrying both `P` and `logP`.
600 *
601 * @param ist 1-based station index
602 * @param nir the per-class occupancy at that station; other stations are left
603 * at the reference's "ignore" flag, since the detailed marginal is
604 * reported per station and only this one is asked for
605 * @param sn the refreshed network struct
606 * @param opt SolverNC's options
607 * @return the LOG of the probability
608 */
609template <class T>
611 const std::vector<int>& nir) {
612 if (ist == 0 || ist > sn.nstations)
613 throw InputError("getProb: station index out of range");
614 MarginalState st(sn.nstations, std::vector<int>(sn.nclasses, -1));
615 st[ist - 1] = nir;
616 return solver_nc_marg(sn, opt, st,
617 std::numeric_limits<double>::quiet_NaN())
618 .logP[ist - 1];
619}
620
621/** Port of `@@SolverNC/getProbAggr.m`: the AGGREGATE probability at one station. */
622template <class T>
624 std::size_t ist, const std::vector<int>& nir, double lG) {
625 if (ist == 0 || ist > sn.nstations)
626 throw InputError("getProbAggr: station index out of range");
627 MarginalState st(sn.nstations, std::vector<int>(sn.nclasses, -1));
628 st[ist - 1] = nir;
629 return solver_nc_margaggr(sn, opt, st, lG).P[ist - 1];
630}
631
632/** The marginal queue-length distribution and its logarithm. */
633template <class T>
635 std::vector<T> P; ///< P[n] = Pr[n jobs at the station], n = 0..sum(N)
636 std::vector<T> logP;
637};
638
639/**
640 * Port of `@@SolverNC/getProbMarg.m`: the TOTAL queue-length distribution.
641 *
642 * Two routes, as in the reference. Under `method='comom'` the whole vector
643 * comes from one `pfqn_procomom` solve; otherwise every total n is written as a
644 * sum over the per-class partitions of n and each partition goes through the
645 * aggregate marginal. The normalizing constant is computed ONCE and reused
646 * across the partitions, which is the reference's caching and matters here:
647 * the enumeration is otherwise quadratic in constants.
648 */
649template <class T>
651 const NcSolverOptions& opt, std::size_t ist) {
653 if constexpr (!num_traits<T>::has_transcendental) {
654 (void)sn; (void)opt; (void)ist;
655 throw UnsupportedError(
656 "getProbMarg: the queue-length distribution is assembled from exponentiated "
657 "log-constants and needs transcendental arithmetic");
658 } else {
659 const T zero = num_traits<T>::from_int(0);
660 if (ist == 0 || ist > sn.nstations)
661 throw InputError("getProbMarg: station index out of range");
662 const std::size_t K = sn.nclasses;
663 const std::size_t Ntot = detail::closed_total(sn, "getProbMarg");
664
665 std::vector<int> N(K, 0);
666 for (std::size_t r = 0; r < K; ++r)
667 N[r] = static_cast<int>(std::llround(sn.classes[r].population));
668
669 out.P.assign(Ntot + 1, zero);
670 out.logP.assign(Ntot + 1, num_traits<T>::from_double(
671 -std::numeric_limits<double>::infinity()));
672
673 if (opt.method == "comom") {
674 // The fast path: pfqn_procomom returns the whole per-station
675 // queue-length vector in one solve, on the Seidmann-reduced model.
677 const std::size_t M = sn.nstations, C = sn.nchains;
678 Matrix<T> Lms(M, C, zero);
679 Matrix<T> Ztot(1, C, zero);
680 std::vector<std::size_t> queueStations;
681 for (std::size_t i = 0; i < M; ++i) {
682 const double S = sn.stations[i].nservers;
683 if (std::isinf(S)) {
684 for (std::size_t c = 0; c < C; ++c) Ztot(0, c) += d.Lchain(i, c);
685 } else {
686 queueStations.push_back(i);
687 const T cs = num_traits<T>::from_double(S);
688 for (std::size_t c = 0; c < C; ++c) {
689 Lms(i, c) = T(d.Lchain(i, c) / cs);
690 Ztot(0, c) += T(d.Lchain(i, c) * num_traits<T>::from_double(S - 1.0) / cs);
691 }
692 }
693 }
694 const auto pos = std::find(queueStations.begin(), queueStations.end(), ist - 1);
695 if (pos == queueStations.end()) {
696 // A DELAY station has no row in the procomom result. The
697 // reference warns and FALLS BACK to the enumeration under
698 // method='default' rather than refusing, so this does too.
699 NcSolverOptions fallback = opt;
700 fallback.method = "default";
701 return solver_nc_getprob_marg(sn, fallback, ist);
702 }
703 Matrix<T> Lq(queueStations.size(), C, zero);
704 for (std::size_t a = 0; a < queueStations.size(); ++a)
705 for (std::size_t c = 0; c < C; ++c) Lq(a, c) = Lms(queueStations[a], c);
706 std::vector<int> Nchain(C, 0);
707 std::size_t sumNchain = 0;
708 for (std::size_t c = 0; c < C; ++c) {
709 Nchain[c] = static_cast<int>(std::llround(d.Nchain[c]));
710 sumNchain += static_cast<std::size_t>(Nchain[c]);
711 }
712 std::vector<T> Zv(C, zero);
713 for (std::size_t c = 0; c < C; ++c) Zv[c] = Ztot(0, c);
714 const Matrix<T> Pr = pfqn::pfqn_procomom(Lq, Nchain, Zv).Pr;
715 const std::size_t row = static_cast<std::size_t>(pos - queueStations.begin());
716 const std::size_t len = std::min(sumNchain + 1, Ntot + 1);
717 for (std::size_t n = 0; n < len && n < Pr.cols(); ++n) {
718 out.P[n] = Pr(row, n);
719 if (num_traits<T>::to_double(out.P[n]) > 0.0)
721 std::log(num_traits<T>::to_double(out.P[n])));
722 }
723 return out;
724 }
725
726 // The enumeration. lG is computed once and handed to every call.
728 const Matrix<T> mu = detail::prob_mu(sn, Ntot);
729 std::vector<int> Nchain(sn.nchains, 0);
730 for (std::size_t c = 0; c < sn.nchains; ++c)
731 Nchain[c] = static_cast<int>(std::llround(d.Nchain[c]));
732 const double lG =
733 pfqn::pfqn_ncld(d.Lchain, Nchain, Matrix<T>(1, sn.nchains, zero), mu,
734 detail::ncld_pfqn_method(opt.method),
736 .lG;
737
738 std::vector<int> part(K, 0);
739 for (std::size_t n = 0; n <= Ntot; ++n) {
740 double acc = 0.0;
741 bool any = false;
742 // enumerate the compositions of n into K parts with part_r <= N_r
743 std::function<void(std::size_t, int)> rec = [&](std::size_t r, int left) {
744 if (r + 1 == K) {
745 if (left > N[r]) return;
746 part[r] = left;
747 const T p = solver_nc_getprob_aggr(sn, opt, ist, part, lG);
748 const double v = num_traits<T>::to_double(p);
749 if (v > 0.0) {
750 acc += v;
751 any = true;
752 }
753 return;
754 }
755 const int hi = std::min(left, N[r]);
756 for (int v = 0; v <= hi; ++v) {
757 part[r] = v;
758 rec(r + 1, left - v);
759 }
760 };
761 if (K == 0) continue;
762 rec(0, static_cast<int>(n));
763 if (any) {
764 out.P[n] = num_traits<T>::from_double(acc);
765 out.logP[n] = num_traits<T>::from_double(std::log(acc));
766 }
767 }
768 // THE REFERENCE RENORMALIZES, and so must this: `getProbMarg.m` divides
769 // the whole vector by its sum whenever that sum misses one by more than
770 // 1e-10. The gap is the normalizing constant's own error -- on the
771 // default (`cub`) route lG is an approximation, and the per-partition
772 // constants inherit it -- so without this the marginal law of a station
773 // is not a distribution. The 1e-10 dead band is the reference's too: it
774 // leaves a vector already summing to one bit-for-bit alone rather than
775 // perturbing it by a division.
776 double total = 0.0;
777 for (std::size_t n = 0; n <= Ntot; ++n) total += num_traits<T>::to_double(out.P[n]);
778 if (total > 0.0 && std::fabs(total - 1.0) > 1e-10) {
779 const double ltotal = std::log(total);
780 for (std::size_t n = 0; n <= Ntot; ++n) {
781 out.P[n] = num_traits<T>::from_double(num_traits<T>::to_double(out.P[n]) / total);
782 if (num_traits<T>::to_double(out.P[n]) > 0.0)
783 out.logP[n] =
785 }
786 }
787 return out;
788 }
789}
790
791/** Port of `@@SolverNC/getProbSys.m`. */
792template <class T>
794 const MarginalState& nir) {
795 return solver_nc_joint(sn, opt, nir, nullptr);
796}
797
798/** Port of `@@SolverNC/getProbSysAggr.m`. */
799template <class T>
801 const MarginalState& nir) {
802 return solver_nc_jointaggr(sn, opt, nir, nullptr);
803}
804
805namespace detail {
806
807/**
808 * The permanent identity supplies one n_i! per queueing station and none per
809 * infinite server. A multiserver or load-dependent station has neither, so it
810 * is refused BY NAME rather than approximated.
811 */
812template <class T>
813void check_jointmarg_supported(const qn::NetworkStruct<T>& sn) {
814 for (const qn::JobClass& c : sn.classes)
815 if (std::isinf(c.population))
816 throw UnsupportedError(
817 "solver_nc_jointmarg: getProbSysMarg requires a closed model: the joint law of "
818 "the total queue lengths is not defined when a class has an infinite population");
819 for (std::size_t i = 0; i < sn.nstations; ++i) {
820 if (!sn.stations[i].lldscaling.empty())
821 throw UnsupportedError(
822 "solver_nc_jointmarg: getProbSysMarg does not support load-dependent stations "
823 "(sn.lldscaling is set at station " + std::to_string(i + 1) + "): the permanent "
824 "identity supplies exactly one n_i! per queueing station");
825 const double S = sn.stations[i].nservers;
826 if (std::isfinite(S) && S > 1.0)
827 throw UnsupportedError(
828 "solver_nc_jointmarg: getProbSysMarg does not support the multiserver station " +
829 std::to_string(i + 1) + " (" + std::to_string(static_cast<int>(S)) +
830 " servers): the permanent identity supplies exactly one n_i! per queueing "
831 "station");
832 }
833}
834
835} // namespace detail
836
837/**
838 * Joint probability that station i holds `nvec[i]` jobs IN TOTAL, all classes
839 * summed out.
840 *
841 * This is NOT `solver_nc_jointaggr`, which fixes the per-class population of
842 * every station: each state here is the SUM of jointaggr over the whole fibre
843 * of per-class tables with these row sums, and that fibre grows
844 * combinatorially. `pfqn_jointmarg` evaluates the sum in closed form as a
845 * permanent of the demand matrix replicated once per job.
846 *
847 * @param nvec (M) per-station total job counts
848 * @param engine "exact" (default), "spm", "bethe", "heur", "huberlaw" or
849 * "adapart"; see pfqn_jointmarg for what each guarantees
850 * @param lG_out receives the log normalizing constant when not null
851 */
852template <class T>
854 const std::vector<int>& nvec, const std::string& engine = "exact",
855 double* lG_out = nullptr) {
856 const std::size_t M = sn.nstations;
857 if (nvec.size() != M)
858 throw InputError("solver_nc_jointmarg: the occupancy vector has " +
859 std::to_string(nvec.size()) + " entries but the model has " +
860 std::to_string(M) + " stations");
861 detail::check_jointmarg_supported(sn);
862
864 const T zero = num_traits<T>::from_int(0);
865 Matrix<T> Lchain = d.Lchain;
866 for (std::size_t i = 0; i < Lchain.rows(); ++i)
867 for (std::size_t c = 0; c < Lchain.cols(); ++c)
868 if (!std::isfinite(num_traits<T>::to_double(Lchain(i, c)))) Lchain(i, c) = zero;
869 std::vector<int> Nchain(sn.nchains, 0);
870 for (std::size_t c = 0; c < sn.nchains; ++c)
871 Nchain[c] = static_cast<int>(std::llround(d.Nchain[c]));
872
873 std::vector<std::size_t> infset;
874 for (std::size_t i = 0; i < M; ++i)
875 if (std::isinf(sn.stations[i].nservers)) infset.push_back(i);
876
877 // G does not depend on how the delay stations are split: they aggregate by
878 // the multinomial theorem, so it is taken with the infinite-server rows
879 // summed into the think time.
880 std::vector<bool> isinf(M, false);
881 for (std::size_t k = 0; k < infset.size(); ++k) isinf[infset[k]] = true;
882 std::size_t nq = 0;
883 for (std::size_t i = 0; i < M; ++i)
884 if (!isinf[i]) ++nq;
885 Matrix<T> Lq(nq, sn.nchains, zero);
886 std::size_t a = 0;
887 for (std::size_t i = 0; i < M; ++i) {
888 if (isinf[i]) continue;
889 for (std::size_t c = 0; c < sn.nchains; ++c) Lq(a, c) = Lchain(i, c);
890 ++a;
891 }
892 Matrix<T> Z;
893 if (!infset.empty()) {
894 Z = Matrix<T>(1, sn.nchains, zero);
895 for (std::size_t k = 0; k < infset.size(); ++k)
896 for (std::size_t c = 0; c < sn.nchains; ++c) Z(0, c) += Lchain(infset[k], c);
897 }
898 const pfqn::NcResult<T> ca = pfqn::pfqn_ca(Lq, Nchain, Z);
899 if (lG_out != nullptr) *lG_out = num_traits<T>::to_double(ca.lG);
900
901 return pfqn::pfqn_jointmarg(nvec, Lchain, Nchain, infset, ca.G, engine,
902 static_cast<std::uint64_t>(opt.seed));
903}
904
905/**
906 * Port of `@@SolverNC/getProbSysMarg.m`.
907 *
908 * Compare with `solver_nc_getprob_sys_aggr`, which fixes the PER-CLASS
909 * population of every station and is a product form; each value returned here
910 * is the sum of that one over every per-class table with these row sums.
911 */
912template <class T>
914 const std::vector<int>& nvec, const std::string& engine = "exact") {
915 return solver_nc_jointmarg(sn, opt, nvec, engine, nullptr);
916}
917
918} // namespace nc
919} // namespace line
920
921#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.
NcldMethod
The load-dependent methods this port dispatches.
Definition pfqn_ncld.h:87
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
NcldResult< T > pfqn_ncld(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, const Matrix< T > &mu, NcldMethod method, const T &atol, const NcOptions &nopt)
Normalizing constant of a LOAD-DEPENDENT closed network: the dispatcher.
Definition pfqn_ncld.h:153
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.
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:668
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