LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_nc.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_H
6#define LINE_SOLVERS_NC_SOLVER_NC_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_nc.m`: the load-INDEPENDENT normalizing-constant analyzer.
12 *
13 * WHAT IT COMPUTES. One evaluation of the normalizing constant G(N) settles the
14 * whole product-form network: the chain throughput is X_c = G(N - 1_c) / G(N),
15 * and the chain queue length at station i follows from a constant evaluated on
16 * the model with station i REPLICATED into a private class,
17 *
18 * Q_ic = Zms_ic X_c + Lms_ic exp(lG_ar(i,c) - lG),
19 *
20 * which is the arrival-theorem identity written in constants rather than in a
21 * recursion. That is why this file calls `pfqn_nc` once per chain and once per
22 * (station, chain) pair rather than solving anything itself: the algorithms
23 * live in `api/pfqn`, and choosing among them is `pfqn_nc`'s job.
24 *
25 * MULTISERVER IS SEIDMANN'S APPROXIMATION, NOT AN EXACT SOLVE. A station with
26 * c > 1 servers is split into a demand L/c at a single server plus a pure delay
27 * L (c-1)/c. It is exact only in the limit; the reference warns and continues,
28 * and `@@SolverNC/runAnalyzer` converts genuine multiservers to load-dependent
29 * rates BEFORE reaching here whenever the model is product-form, so this branch
30 * serves the models that have no exact load-dependent form.
31 *
32 * WHY THERE IS AN OUTER LOOP AT ALL. `SolverOptions('NC')` sets
33 * `config.highvar = 'interp'`, so a non-product-form FCFS station has its
34 * service time rescaled by `npfqn_nonexp_approx` after each pass and the
35 * analyzer re-solves until the decay rates eta stop moving. With no FCFS
36 * station the reference pins iter_max to 1 and the loop runs once.
37 *
38 * ARITHMETIC. Every measure here is formed from differences of LOGARITHMS of
39 * normalizing constants, which is the only way the ratios stay representable;
40 * there is no exact-field formulation of exp(lG' - lG), so a non-transcendental
41 * backend is refused by name rather than narrowed.
42 */
43
44#include <algorithm>
45#include <cmath>
46#include <limits>
47#include <string>
48#include <vector>
49
59#include "line/util/error.h"
60#include "line/util/matrix.h"
61
62namespace line {
63namespace nc {
64
65using lang::GlobalConstants;
67
68namespace detail {
69
70/** `sn.njobs` per chain, infinite when any class of the chain is open. */
71template <class T>
72std::vector<double> chain_population(const qn::NetworkStruct<T>& sn) {
73 std::vector<double> Nchain(sn.nchains, 0.0);
74 for (std::size_t c = 0; c < sn.nchains; ++c) {
75 double n = 0.0;
76 bool open = false;
77 for (std::size_t k : sn.inchain[c]) {
78 const double p = sn.classes[k - 1].population;
79 if (std::isinf(p)) open = true;
80 n += p;
81 }
82 Nchain[c] = open ? std::numeric_limits<double>::infinity() : n;
83 }
84 return Nchain;
85}
86
87/**
88 * The population vector `pfqn_nc` wants: integral counts, with an OPEN chain
89 * marked by a negative entry.
90 *
91 * MATLAB marks an open class by N_r = Inf and zeroes it inside pfqn_nc; the
92 * exact-capable port has no infinity and uses the sign instead, which is also
93 * the .qn interchange convention.
94 */
95inline std::vector<int> nc_population(const std::vector<double>& Nchain) {
96 std::vector<int> N(Nchain.size(), 0);
97 for (std::size_t c = 0; c < Nchain.size(); ++c)
98 N[c] = std::isinf(Nchain[c]) ? -1 : static_cast<int>(std::llround(Nchain[c]));
99 return N;
100}
101
102/**
103 * `cellsum(sn.visits)` at STATION level: the visit ratios summed over chains.
104 *
105 * The reference passes this to npfqn_nonexp_approx, which never reads it; it is
106 * built anyway so the argument list matches the reference one for one.
107 */
108template <class T>
109Matrix<T> station_visits(const qn::NetworkStruct<T>& sn) {
110 const T zero = num_traits<T>::from_int(0);
111 Matrix<T> V(sn.nstations, sn.nclasses, zero);
112 for (std::size_t c = 0; c < sn.nchains; ++c)
113 for (std::size_t i = 0; i < sn.nstations; ++i) {
114 const std::size_t sf = sn.stateful_of_station(i + 1);
115 for (std::size_t k = 0; k < sn.nclasses; ++k)
116 V(i, k) = T(V(i, k) + sn.visits[c](sf - 1, k));
117 }
118 return V;
119}
120
121/** `oner(N, r)`: one job of chain r removed. */
122inline std::vector<int> oner(std::vector<int> N, std::size_t r) {
123 if (N[r] > 0) --N[r];
124 return N;
125}
126
127/** Map `options.method` onto the pfqn dispatcher, refusing an unknown name. */
128inline pfqn::NcMethod nc_pfqn_method(const std::string& method) {
129 return pfqn::nc_method_of(method);
130}
131
132} // namespace detail
133
134/**
135 * Port of `solver_nc.m`.
136 *
137 * @param sn the refreshed struct
138 * @param opt solver controls
139 * @return the class-level measures, the log normalizing constant and the
140 * concrete algorithm the constants were computed with
141 */
142template <class T>
144 NcSolution<T> out;
145 if constexpr (!num_traits<T>::has_transcendental) {
146 (void)sn;
147 (void)opt;
148 throw UnsupportedError(ErrorKind{"Arithmetic"},
149 "solver_nc: the normalizing-constant analyzer forms X = exp(lG(N-1_c) - lG(N)) and "
150 "needs transcendental arithmetic; this backend has none");
151 } else {
152 // Krzesinski state-dependent routing: the model has its own product
153 // form (eq. 16), so it is intercepted before the convolution and MVA
154 // analyzers, which assume state-independent routing; see
155 // _kb/16-state-dependent-routing.md
156 if (!sn.sdr.empty()) return solver_nc_sdr(sn, opt);
157
158 const T zero = num_traits<T>::from_int(0);
159 const T one = num_traits<T>::from_int(1);
160 const std::size_t M = sn.nstations, K = sn.nclasses, C = sn.nchains;
161
162 std::vector<double> nservers(M, 1.0);
163 std::vector<bool> isFCFS(M, false);
164 bool anyFCFS = false;
165 for (std::size_t i = 0; i < M; ++i) {
166 nservers[i] = sn.stations[i].nservers;
167 isFCFS[i] = sn.stations[i].sched == SchedStrategy::FCFS;
168 if (isFCFS[i]) anyFCFS = true;
169 }
170
171 // LCFS paired with LCFS-PR is a two-station special case with its own
172 // closed form; nothing else in this file can represent it, since a
173 // non-preemptive LCFS station is not a BCMP station at all.
174 std::vector<std::size_t> lcfsStat, lcfsprStat;
175 for (std::size_t i = 0; i < M; ++i) {
176 if (sn.stations[i].sched == SchedStrategy::LCFS) lcfsStat.push_back(i + 1);
177 if (sn.stations[i].sched == SchedStrategy::LCFSPR) lcfsprStat.push_back(i + 1);
178 }
179 if (!lcfsStat.empty() && !lcfsprStat.empty()) {
180 if (lcfsStat.size() != 1 || lcfsprStat.size() != 1)
181 throw UnsupportedError(ErrorKind{"SchedStrategy_LCFS"},
182 "solver_nc: LCFS NC requires exactly one LCFS and one LCFS-PR station");
183 for (std::size_t c = 0; c < C; ++c) {
184 double nc_pop = 0.0;
185 for (std::size_t r : sn.inchain[c]) nc_pop += sn.classes[r - 1].population;
186 if (std::isinf(nc_pop))
187 throw UnsupportedError(ErrorKind{"OpenClass"},
188 "solver_nc: LCFS NC requires a closed queueing network");
189 }
190 // The closed form places every job on one boundary-crossing sequence,
191 // which a self-loop would let a job re-enter without crossing.
192 for (std::size_t i : {lcfsStat[0], lcfsprStat[0]}) {
193 const std::size_t sf = sn.stateful_of_station(i) - 1;
194 for (std::size_t r = 0; r < K; ++r)
195 if (sn.rt(sf * K + r, sf * K + r) > zero)
196 throw UnsupportedError(ErrorKind{"SchedStrategy_LCFS"},
197 "solver_nc: LCFS NC does not support self-loops at stations");
198 }
199 out = solver_nc_lcfsqn(sn, opt, lcfsStat[0], lcfsprStat[0]);
200 // STeff is the reference's `ST = 1./sn.rates` with NaN zeroed: an
201 // INFINITE service time (a zero rate) is kept, only a NaN is not.
202 out.STeff = Matrix<T>(M, K, zero);
203 for (std::size_t i = 0; i < M; ++i)
204 for (std::size_t r = 0; r < K; ++r) {
205 const double mu = num_traits<T>::to_double(sn.rates(i, r));
206 if (std::isnan(mu)) continue;
207 out.STeff(i, r) = mu == 0.0
209 std::numeric_limits<double>::infinity())
210 : T(one / sn.rates(i, r));
211 }
212 return out;
213 }
214 if (!lcfsStat.empty())
215 throw UnsupportedError(ErrorKind{"SchedStrategy_LCFS"},
216 "solver_nc: LCFS scheduling requires a paired LCFS-PR station");
217
218 const std::vector<double> Nchain = detail::chain_population(sn);
219 const std::vector<int> Nnc = detail::nc_population(Nchain);
220 std::vector<std::size_t> openChains, closedChains;
221 for (std::size_t c = 0; c < C; ++c)
222 (std::isinf(Nchain[c]) ? openChains : closedChains).push_back(c);
223
224 const pfqn::NcMethod pmethod = detail::nc_pfqn_method(opt.method);
225 pfqn::NcOptions popt;
226 popt.samples = opt.samples;
227 popt.seed = opt.seed;
228 popt.tol = opt.tol;
229 popt.aghq_nodes = opt.aghq_nodes;
230 popt.mcmc_batches = opt.mcmc_batches;
231 popt.mcmc_burnin = opt.mcmc_burnin;
232 const T atol = num_traits<T>::from_double(opt.tol);
233
234 // Chain aggregation, and the class-level service times the outer loop
235 // rescales. ST0 is the untouched original: npfqn_nonexp_approx is always
236 // handed the original times, never its own previous output.
238 Matrix<T> ST = d.ST, ST0 = d.ST;
239 const Matrix<T> V = detail::station_visits(sn);
240 // sn.scv with the disabled pairs marked NOT FINITE, which is what
241 // isfinite(SCV) selects on in the reference: sn.scv is NaN there, and
242 // this port carries the marker in `disabled` instead.
243 Matrix<T> SCVnan = sn.scv;
244 for (std::size_t i = 0; i < M; ++i)
245 for (std::size_t k = 0; k < K; ++k)
246 if (sn.disabled[i][k])
247 SCVnan(i, k) = num_traits<T>::from_double(
248 std::numeric_limits<double>::quiet_NaN());
249
250 std::vector<T> gamma(M, zero);
251 std::vector<T> nserv_t(M, one);
252 for (std::size_t i = 0; i < M; ++i) nserv_t[i] = num_traits<T>::from_double(nservers[i]);
253
254 std::vector<T> lambda(C, zero);
255 std::vector<T> eta(M, one), eta_1(M, zero);
256 const int iter_max = anyFCFS ? opt.iter_max : 1;
257 int it = 0;
258 double lG = 0.0;
259 std::string actualmethod = opt.method;
261
262 while (it < iter_max) {
263 { // the reference's max(abs(1 - eta./eta_1)) > iter_tol test
264 double dev = 0.0;
265 for (std::size_t i = 0; i < M; ++i) {
266 const double e1 = num_traits<T>::to_double(eta_1[i]);
267 const double e = num_traits<T>::to_double(eta[i]);
268 const double v = std::fabs(1.0 - e / e1);
269 if (!(v <= dev)) dev = v; // NaN and Inf both count as "not converged"
270 }
271 if (!(dev > opt.iter_tol)) break;
272 }
273 ++it;
274 eta_1 = eta;
275
276 if (it == 1) {
277 // An open chain's reference station is its Source, whose chain
278 // service time is 1 / total arrival rate.
279 for (std::size_t c = 0; c < C; ++c) {
280 if (!std::isinf(Nchain[c])) continue;
281 const std::size_t rst = sn.classes[sn.inchain[c][0] - 1].refstat;
282 if (d.STchain(rst - 1, c) != zero)
283 lambda[c] = T(one / d.STchain(rst - 1, c));
284 }
285 } else {
286 for (std::size_t c = 0; c < C; ++c)
287 for (std::size_t i = 0; i < M; ++i) {
288 T st = zero;
289 for (std::size_t k : sn.inchain[c]) st += ST(i, k - 1) * d.alpha(i, k - 1);
290 d.STchain(i, c) = st;
291 d.Lchain(i, c) = T(d.Vchain(i, c) * st);
292 }
293 }
294 for (std::size_t i = 0; i < M; ++i)
295 for (std::size_t c = 0; c < C; ++c) {
296 if (!std::isfinite(num_traits<T>::to_double(d.STchain(i, c))))
297 d.STchain(i, c) = zero;
298 if (!std::isfinite(num_traits<T>::to_double(d.Lchain(i, c))))
299 d.Lchain(i, c) = zero;
300 }
301
302 // Seidmann's approximation: a c-server station becomes a demand L/c
303 // at one server plus a pure delay L (c-1)/c.
304 Matrix<T> Lms(M, C, zero), Z(M, C, zero), Zms(M, C, zero);
305 std::vector<std::size_t> infServers;
306 for (std::size_t i = 0; i < M; ++i) {
307 if (std::isinf(nservers[i])) {
308 infServers.push_back(i);
309 for (std::size_t c = 0; c < C; ++c) Z(i, c) = d.Lchain(i, c);
310 } else {
311 const T cs = num_traits<T>::from_double(nservers[i]);
312 for (std::size_t c = 0; c < C; ++c) {
313 Lms(i, c) = T(d.Lchain(i, c) / cs);
314 Zms(i, c) = T(d.Lchain(i, c) *
315 num_traits<T>::from_double(nservers[i] - 1.0) / cs);
316 }
317 }
318 }
319 Matrix<T> Ztot(1, C, zero);
320 for (std::size_t c = 0; c < C; ++c)
321 for (std::size_t i = 0; i < M; ++i) Ztot(0, c) += Z(i, c) + Zms(i, c);
322
323 // step 1: the base constant
324 const pfqn::NcDispatchResult<T> base =
325 pfqn::pfqn_nc(lambda, Lms, Nnc, Ztot, pmethod, atol, popt);
326 if (!base.valid) {
327 // The method declined this model (the reference's empty lG).
328 // MATLAB returns every output empty here and `getAvg` renders a
329 // table of ZEROS while reporting a completed analysis; that
330 // convention is reproduced deliberately, ruled by the user on
331 // 2026-07-25 (register row N1). The consequence the ruling
332 // accepts: a caller cannot tell "no jobs" from "declined".
333 out.sol.Q = Matrix<T>(M, K, zero);
334 out.sol.U = Matrix<T>(M, K, zero);
335 out.sol.R = Matrix<T>(M, K, zero);
336 out.sol.Tp = Matrix<T>(M, K, zero);
337 out.sol.X.assign(K, zero);
338 out.sol.C.assign(K, zero);
339 out.sol.lG = 0.0;
340 out.sol.iter = 1;
341 out.sol.method = opt.method;
342 out.actualmethod = opt.method;
343 out.STeff = ST;
344 return out;
345 }
346 lG = base.lG;
347 actualmethod = base.method;
348 std::vector<T> Xchain = base.X;
349 Matrix<T> Qchain = base.Q;
350
351 // Seidmann's delay term makes the by-product measures inconsistent
352 // with the approximated model, so the reference discards them and
353 // takes the constant-ratio route below. 'mcmc' is EXEMPT: it obtains
354 // X and Q from ONE simulation of the regularized network, so
355 // discarding them would cost R + M*R further simulations to recover
356 // the same means by differencing lG. The surrogate delay is added
357 // back to Qchain in the fill branch below instead.
358 bool allZms = true;
359 for (std::size_t c = 0; c < C; ++c) {
360 T s = zero;
361 for (std::size_t i = 0; i < M; ++i) s += Zms(i, c);
362 if (!(num_traits<T>::to_double(s) > GlobalConstants::FineTol)) allZms = false;
363 }
364 if (allZms && pmethod != pfqn::NcMethod::Mcmc) {
365 Xchain.clear();
366 Qchain = Matrix<T>();
367 }
368
369 if (Xchain.empty()) {
370 Xchain = lambda;
371 Qchain = Matrix<T>(M, C, zero);
372 for (std::size_t r : closedChains) {
373 const std::vector<int> Nr = detail::oner(Nnc, r);
374 const pfqn::NcDispatchResult<T> sub =
375 pfqn::pfqn_nc(lambda, Lms, Nr, Ztot, pmethod, atol, popt);
376 if (!sub.valid) {
377 // The reference returns empty from the SUBPROBLEMS too,
378 // rather than assigning [] into a scalar slot.
379 out.sol.Q = Matrix<T>(M, K, zero);
380 out.sol.U = Matrix<T>(M, K, zero);
381 out.sol.R = Matrix<T>(M, K, zero);
382 out.sol.Tp = Matrix<T>(M, K, zero);
383 out.sol.X.assign(K, zero);
384 out.sol.C.assign(K, zero);
385 out.sol.lG = 0.0;
386 out.sol.iter = 1;
387 out.sol.method = opt.method;
388 out.actualmethod = opt.method;
389 out.STeff = ST;
390 return out;
391 }
392 const double lGr = sub.lG;
393 Xchain[r] = num_traits<T>::from_double(std::exp(lGr - lG));
394 for (std::size_t i = 0; i < M; ++i) {
395 if (!(d.Lchain(i, r) > zero)) continue;
396 if (std::isinf(nservers[i])) {
397 Qchain(i, r) = T(d.Lchain(i, r) * Xchain[r]);
398 continue;
399 }
400 // Station i replicated into a private class holding one
401 // job: the arrival theorem in normalizing-constant form.
402 Matrix<T> Lar(M, C + 1, zero);
403 std::size_t row = 0;
404 for (std::size_t i2 = 0; i2 < M; ++i2) {
405 if (i2 == i) continue;
406 for (std::size_t c = 0; c < C; ++c) Lar(row, c) = Lms(i2, c);
407 ++row;
408 }
409 for (std::size_t c = 0; c < C; ++c) Lar(row, c) = Lms(i, c);
410 Lar(row, C) = one;
411 std::vector<T> lam_ar = lambda;
412 lam_ar.push_back(zero);
413 std::vector<int> N_ar = Nr;
414 N_ar.push_back(1);
415 Matrix<T> Z_ar(1, C + 1, zero);
416 for (std::size_t c = 0; c < C; ++c) Z_ar(0, c) = Ztot(0, c);
418 pfqn::pfqn_nc(lam_ar, Lar, N_ar, Z_ar, pmethod, atol, popt);
419 // The most costly call of the whole solve, so it is the
420 // one whose algorithm is reported.
421 actualmethod = ar.method;
422 Qchain(i, r) = T(Zms(i, r) * Xchain[r] +
423 Lms(i, r) * num_traits<T>::from_double(
424 std::exp(ar.lG - lG)));
425 }
426 }
427 for (std::size_t i = 0; i < M; ++i)
428 for (std::size_t c = 0; c < C; ++c)
429 if (std::isnan(num_traits<T>::to_double(Qchain(i, c)))) Qchain(i, c) = zero;
430
431 // An open chain sees the closed population as extra load: the
432 // mixed-network queue length of Bruell and Balbo.
433 for (std::size_t r : openChains)
434 for (std::size_t i = 0; i < M; ++i) {
435 T load = zero;
436 for (std::size_t o : openChains) load += lambda[o] * d.Lchain(i, o);
437 // In an OPEN product-form network a station with c servers has the
438 // exact M/M/c marginal, split over the classes by their offered load
439 // (solver_nc.m). The mixed expression reads c only in its
440 // denominator: an open M/M/3 (lambda 1.2, mu 1) came out at 2.0
441 // against 1.294.
442 if (closedChains.empty() && std::isfinite(nservers[i]) && nservers[i] > 1.0) {
443 if (load > zero && d.Lchain(i, r) > zero) {
444 const T W = qsys::qsys_mmk(load, one, static_cast<unsigned>(nservers[i])).W;
445 Qchain(i, r) = T(W * load * lambda[r] * d.Lchain(i, r) / load);
446 } else {
447 Qchain(i, r) = zero;
448 }
449 continue;
450 }
451 const T den =
452 std::isinf(nservers[i])
453 ? one
454 : T(one - load / num_traits<T>::from_double(nservers[i]));
455 if (den == zero) continue;
456 T qc = zero;
457 for (std::size_t cc : closedChains) qc += Qchain(i, cc);
458 Qchain(i, r) = T(lambda[r] * d.Lchain(i, r) / den * (one + qc));
459 }
460 } else {
461 // The method returned the measures, so they are KEPT: only the delay
462 // stations, which it does not model, are filled in, and the population
463 // Seidmann's surrogate delay holds outside a multiserver queueing station
464 // is restored. Zms is zero at a single-server station, so the second arm
465 // is a no-op on a single-server model. Rebuilding Qchain from zero here
466 // instead would throw away the queueing-station queue lengths the method
467 // just produced, which is what the reference keeps.
468 if (Qchain.rows() != M || Qchain.cols() != C) Qchain = Matrix<T>(M, C, zero);
469 for (std::size_t c = 0; c < C; ++c)
470 for (std::size_t i = 0; i < M; ++i) {
471 if (!(d.Lchain(i, c) > zero)) continue;
472 if (std::isinf(nservers[i]))
473 Qchain(i, c) = T(d.Lchain(i, c) * Xchain[c]);
474 else if (nservers[i] > 1.0)
475 Qchain(i, c) = T(Qchain(i, c) + Zms(i, c) * Xchain[c]);
476 }
477 }
478
479 Matrix<T> Rchain(M, C, zero), Tchain(M, C, zero);
480 for (std::size_t i = 0; i < M; ++i)
481 for (std::size_t c = 0; c < C; ++c) {
482 if (Xchain[c] != zero && d.Vchain(i, c) != zero)
483 Rchain(i, c) = T(Qchain(i, c) / Xchain[c] / d.Vchain(i, c));
484 Tchain(i, c) = T(Xchain[c] * d.Vchain(i, c));
485 }
486 for (std::size_t i : infServers)
487 for (std::size_t c = 0; c < C; ++c)
488 Rchain(i, c) = d.Vchain(i, c) == zero
489 ? zero
490 : T(d.Lchain(i, c) / d.Vchain(i, c));
491
492 d.ST = ST;
494 Tchain, Xchain);
495 out.STeff = ST; // the effective service times of THIS pass
496
498 opt.highvar, isFCFS, sn.rates, ST0, V, SCVnan, cls.Tp, cls.U, gamma, nserv_t);
499 ST = na.ST;
500 gamma = na.gamma;
501 eta = na.eta;
502 }
503
504 if (it == 0)
505 throw UnsupportedError(ErrorKind{"Option"},
506 "solver_nc: the analyzer made no pass; iter_max must be at least one");
507
508 Matrix<T> Q = cls.Q, U = cls.U, R = cls.R, Tp = cls.Tp;
509 std::vector<T> X = cls.X;
510 auto sanitize = [&](Matrix<T>& A) {
511 for (std::size_t i = 0; i < A.rows(); ++i)
512 for (std::size_t j = 0; j < A.cols(); ++j) {
513 if (A(i, j) < zero) A(i, j) = T(-A(i, j));
514 if (!std::isfinite(num_traits<T>::to_double(A(i, j)))) A(i, j) = zero;
515 }
516 };
517 sanitize(Q);
518 sanitize(U);
519 sanitize(R);
520 for (T& x : X) {
521 if (x < zero) x = T(-x);
522 if (!std::isfinite(num_traits<T>::to_double(x))) x = zero;
523 }
524
525 // Renormalize: the approximations above do not conserve the closed
526 // population exactly, and a chain whose queue lengths do not add up to
527 // N would report a throughput off by the same factor.
528 for (std::size_t c = 0; c < C; ++c) {
529 if (std::isinf(Nchain[c])) continue;
530 T qden = zero;
531 for (std::size_t k : sn.inchain[c])
532 for (std::size_t i = 0; i < M; ++i) qden += Q(i, k - 1);
533 const T ratio = qden > zero
534 ? T(num_traits<T>::from_double(Nchain[c]) / qden)
535 : zero;
536 for (std::size_t k : sn.inchain[c]) {
537 X[k - 1] = T(ratio * X[k - 1]);
538 for (std::size_t i = 0; i < M; ++i) {
539 Q(i, k - 1) = T(ratio * Q(i, k - 1));
540 Tp(i, k - 1) = T(ratio * Tp(i, k - 1));
541 U(i, k - 1) = T(ratio * U(i, k - 1));
542 R(i, k - 1) = Tp(i, k - 1) == zero ? zero : T(Q(i, k - 1) / Tp(i, k - 1));
543 }
544 }
545 }
546
547 out.sol.Q = Q;
548 out.sol.U = U;
549 out.sol.R = R;
550 out.sol.Tp = Tp;
551 out.sol.X = X;
552 out.sol.C.assign(K, zero);
553 for (std::size_t k = 0; k < K; ++k) {
554 const double njobs = sn.classes[k].population;
555 if (std::isfinite(njobs) && X[k] != zero)
556 out.sol.C[k] = T(num_traits<T>::from_double(njobs) / X[k]);
557 else
558 out.sol.C[k] = cls.C[k];
559 }
560 out.sol.lG = lG;
561 out.sol.iter = it;
562 out.sol.method = actualmethod;
563 out.actualmethod = actualmethod;
564 return out;
565 }
566}
567
568} // namespace nc
569} // namespace line
570
571#endif // LINE_SOLVERS_NC_SOLVER_NC_H
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
UnsupportedError(const std::string &what)
Definition error.h:119
A network plus its refreshed NetworkStruct.
The exception types the port throws.
Dense matrix and non-owning view.
The option and result types every MVA analyzer shares.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
ClassResults< T > sn_deaggregate_chain_results(const qn::NetworkStruct< T > &L, const ChainDemands< T > &d, const Matrix< T > &Qchain, const Matrix< T > &Uchain, const Matrix< T > &Rchain, const Matrix< T > &Tchain, const std::vector< T > &Xchain)
Port of sn_deaggregate_chain_results.
Definition sn_chain.h:227
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
NcSolution< T > solver_nc(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc.m.
Definition solver_nc.h:143
NcSolution< T > solver_nc_lcfsqn(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt, std::size_t lcfsStat, std::size_t lcfsprStat)
Port of solver_nc_lcfsqn.m.
NcSolution< T > solver_nc_sdr(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Solves a network whose entry centre routes by state-dependent routing.
NonexpApproxResult< T > npfqn_nonexp_approx(const std::string &method, const std::vector< bool > &isFCFS, const Matrix< T > &rates, const Matrix< T > &ST, const Matrix< T > &V, const Matrix< T > &SCV, const Matrix< T > &Tput, const Matrix< T > &U, const std::vector< T > &gamma, const std::vector< T > &nservers)
Handler for non-exponential service and arrival processes in AMVA and NC.
NcMethod nc_method_of(const std::string &s)
Map a method name to its enum; throws UnsupportedError on an unknown one.
Definition pfqn_nc.h:177
NcMethod
The methods this port dispatches, one per compute_norm_const case.
Definition pfqn_nc.h:101
@ Mcmc
Chen-O'Cinneide regularization; supplies X and Q, never a constant.
Definition pfqn_nc.h:126
NcDispatchResult< T > pfqn_nc(const std::vector< T > &lambda, const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, NcMethod method, const T &atol, const NcOptions &nopt)
Normalizing constant of a product-form queueing network: the dispatcher.
Definition pfqn_nc.h:275
QsysResult< T > qsys_mmk(const T &lambda, const T &mu, unsigned k)
Exact mean response time of the M/M/k queue (Erlang-C).
Definition qsys_mmk.h:60
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.
Handler for non-exponential service and arrival processes in AMVA and NC.
Normalizing constant of a product-form queueing network: the dispatcher.
Exact mean response time of the M/M/k queue (Erlang-C).
Chain aggregation and de-aggregation.
The two-station LCFS + LCFS-PR closed network.
Exact product-form analysis under state-dependent routing.
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
Matrix< T > ST
(M x K) class-level mean service time, 0 where disabled
Definition sn_chain.h:54
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
Matrix< T > Vchain
(M x C) visits
Definition sn_chain.h:49
Class-level results, as sn_deaggregate_chain_results returns them.
Definition sn_chain.h:204
std::vector< T > C
Definition sn_chain.h:206
std::vector< T > X
Definition sn_chain.h:206
static constexpr double FineTol
Definition lang_types.h:765
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
std::string actualmethod
Definition nc_types.h:115
Controls, defaulting to SolverOptions('NC') in the reference.
Definition nc_types.h:33
Return value, mirroring MATLAB's [ST,gamma,nservers,rho,scva,scvs,eta].
Matrix< T > ST
(M x R) scaled service times
std::vector< T > gamma
(M) multiserver asymptotic decay rate
std::vector< T > eta
(M) diffusion decay rate
std::vector< T > X
(R) per-class throughput, empty when not produced
Definition pfqn_nc.h:244
std::string method
the algorithm actually used
Definition pfqn_nc.h:246
Matrix< T > Q
(M x R) queue lengths, empty when not produced
Definition pfqn_nc.h:245
bool valid
False when the METHOD DECLINED THE MODEL, which is the reference's lG = []: pana outside normal usage...
Definition pfqn_nc.h:259
double lG
its logarithm
Definition pfqn_nc.h:243
The options fields compute_norm_const reads beyond the method itself.
Definition pfqn_nc.h:215
double mcmc_burnin
options.config.mcmc_burnin: warm-up fraction pfqn_mcmc discards
Definition pfqn_nc.h:226
unsigned long seed
SolverOptions('NC').seed.
Definition pfqn_nc.h:217
std::size_t mcmc_batches
options.config.mcmc_batches: batches pfqn_mcmc splits its run into
Definition pfqn_nc.h:224
std::size_t aghq_nodes
options.config.aghq_nodes: nodes per simplex direction of the adaptive Gauss-Hermite rule.
Definition pfqn_nc.h:222
std::size_t samples
SolverOptions('NC').samples.
Definition pfqn_nc.h:216
double tol
handed to pfqn_comomrm
Definition pfqn_nc.h:218