LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_ncld.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_NCLD_H
6#define LINE_SOLVERS_NC_SOLVER_NCLD_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_ncld.m`: the LOAD-DEPENDENT normalizing-constant analyzer.
12 *
13 * WHAT MAKES IT A DIFFERENT SOLVER. `solver_nc.h` assumes every station serves
14 * at a constant rate, so its constants come from `pfqn_nc`. Here each station
15 * carries a rate lattice mu_i(n) and the constants come from `pfqn_ncld`. That
16 * is not a refinement of the same formula: the arrival-theorem shortcut used
17 * there does not hold, and the queue length is recovered instead from the
18 * CONDITIONAL MVA identity
19 *
20 * Q_ic = exp(log L_ic + lG(mu^i, N-1_c) - log mu_i(1) - lG(N-1_c)) X_c (1 + CQ_i)
21 *
22 * where mu^i is the lattice shifted at station i (`pfqn_mushift`) and CQ_i is
23 * assembled from the flow-equivalent complement (`pfqn_fnc`). Four constants
24 * per (station, chain) rather than one, which is the price of load dependence.
25 *
26 * WHY MULTISERVER ARRIVES HERE. `@@SolverNC/runAnalyzer` rewrites a genuine
27 * c-server station as mu(n) = min(n, c) whenever the model is product-form, and
28 * that lattice is EXACT where Seidmann's approximation in `solver_nc.h` is not.
29 * The server count is deliberately kept alongside the lattice: utilization is
30 * the fraction of the c servers busy, and c cannot be read back from
31 * min(1:Nt, c) once the population falls below it.
32 *
33 * MIXED MODELS take a different route entirely -- the Bruell-Balbo-Ashfari
34 * effective-capacity MVA (`pfqn_mvaldmx`) -- because an open chain has no
35 * finite population for the constant to be evaluated at.
36 *
37 * ARITHMETIC. As in `solver_nc.h`, every measure is a difference of logarithms
38 * of normalizing constants; a non-transcendental backend is refused by name.
39 */
40
41#include <algorithm>
42#include <cmath>
43#include <limits>
44#include <string>
45#include <vector>
46
59#include "line/util/error.h"
60#include "line/util/matrix.h"
61
62namespace line {
63namespace nc {
64
65namespace detail {
66
67/**
68 * True when every finite multi-server station already carries mu(n)=min(n,c),
69 * i.e. the multiserver is fully described by the load-dependent rates.
70 *
71 * Port of the local `lld_encodes_multiserver` of the reference.
72 */
73template <class T>
74bool lld_encodes_multiserver(const qn::NetworkStruct<T>& sn) {
75 double Ntot = 0.0;
76 for (const qn::JobClass& c : sn.classes)
77 if (std::isfinite(c.population)) Ntot += c.population;
78 if (!std::isfinite(Ntot) || Ntot < 1.0) return false;
79 const std::size_t Nt = static_cast<std::size_t>(std::llround(Ntot));
80 bool anyLld = false;
81 for (std::size_t i = 0; i < sn.nstations; ++i)
82 if (!sn.stations[i].lldscaling.empty()) anyLld = true;
83 if (!anyLld) return false;
84 for (std::size_t i = 0; i < sn.nstations; ++i) {
85 const double c = sn.stations[i].nservers;
86 if (!std::isfinite(c) || c <= 1.0) continue;
87 const std::vector<T>& lld = sn.stations[i].lldscaling;
88 if (lld.size() < Nt) return false;
89 for (std::size_t n = 1; n <= Nt; ++n)
90 if (num_traits<T>::to_double(lld[n - 1]) != std::min<double>(n, c)) return false;
91 }
92 return true;
93}
94
95/**
96 * First column (1-based) of the trailing constant run of a limited
97 * load-dependence row, i.e. the level b with mu(n) = mu(b) for every n >= b; 1
98 * on a flat row. This is the level `pfqn_ldmx_ec` infers from the row it is
99 * handed, so a row cut below it is read as a different, slower station.
100 */
101template <class T>
102std::size_t lld_saturation_level(const Matrix<T>& lldscaling, std::size_t ist) {
103 std::size_t b = lldscaling.cols();
104 if (b == 0) return 1;
105 while (b > 1 && lldscaling(ist, b - 2) == lldscaling(ist, b - 1)) --b;
106 return b;
107}
108
109} // namespace detail
110
111/**
112 * Port of `solver_ncld.m`.
113 *
114 * @param sn_in the refreshed struct; its lldscaling is completed in place on a
115 * two-station multiserver model, exactly as the reference does
116 * @param opt solver controls
117 */
118template <class T>
120 NcSolution<T> out;
121 if constexpr (!num_traits<T>::has_transcendental) {
122 (void)sn_in;
123 (void)opt;
124 throw UnsupportedError(
125 "solver_ncld: the load-dependent normalizing-constant analyzer forms "
126 "X = exp(lG(N-1_c) - lG(N)) and needs transcendental arithmetic; this backend has "
127 "none");
128 } else {
129 const T zero = num_traits<T>::from_int(0);
130 const T one = num_traits<T>::from_int(1);
131 qn::NetworkStruct<T> sn = sn_in;
132 const std::size_t M = sn.nstations, K = sn.nclasses, C = sn.nchains;
133
134 std::vector<double> nservers(M, 1.0);
135 std::vector<bool> isFCFS(M, false);
136 bool anyFCFS = false, anyMulti = false;
137 for (std::size_t i = 0; i < M; ++i) {
138 nservers[i] = sn.stations[i].nservers;
139 isFCFS[i] = sn.stations[i].sched == SchedStrategy::FCFS;
140 if (isFCFS[i]) anyFCFS = true;
141 if (std::isfinite(nservers[i]) && nservers[i] > 1.0) anyMulti = true;
142 }
143
144 double Ntot_d = 0.0;
145 bool allClosed = true;
146 for (const qn::JobClass& c : sn.classes) {
147 if (std::isinf(c.population))
148 allClosed = false;
149 else
150 Ntot_d += c.population;
151 }
152 const std::size_t Nt = static_cast<std::size_t>(
153 std::max<double>(1.0, std::ceil(std::isfinite(Ntot_d) ? Ntot_d : 1.0)));
154
155 // Class-dependent rates beta_{i,r}(n) AND joint-dependent rates eta_i(n)
156 // route to the convolution analyzer BEFORE anything else and
157 // unconditionally on the method: every algorithm below reads mu(n) from
158 // lldscaling alone and has no way to apply either, so gating this on
159 // 'exact' would silently return the UNSCALED network for every other
160 // method. The test read `cdscaling` alone until 2026-09-13, so a
161 // joint-dependent station fell through to those algorithms and was
162 // answered unscaled; `solver_nc_conv` folds eta into the same handle.
163 for (std::size_t i = 0; i < M; ++i)
164 if (static_cast<bool>(sn.stations[i].cdscaling) ||
165 static_cast<bool>(sn.stations[i].jdscaling))
166 return solver_nc_conv(sn, opt);
167
168 bool anyLld = false;
169 for (std::size_t i = 0; i < M; ++i)
170 if (!sn.stations[i].lldscaling.empty()) anyLld = true;
171
172 if (anyMulti) {
173 if (!anyLld && M == 2 && allClosed) {
174 // Two stations and no lattice yet: express every station as
175 // mu(n) = min(n, c), which is what the reference installs here.
176 for (std::size_t i = 0; i < M; ++i) {
177 std::vector<T> lld(Nt, one);
178 for (std::size_t n = 1; n <= Nt; ++n)
179 lld[n - 1] = num_traits<T>::from_double(
180 std::min<double>(static_cast<double>(n), nservers[i]));
181 sn.stations[i].lldscaling = lld;
182 }
183 anyLld = true;
184 } else if (!detail::lld_encodes_multiserver(sn)) {
185 throw UnsupportedError(
186 "solver_ncld: the load-dependent solver does not support multi-server "
187 "stations unless they are expressed as limited load dependence mu(n) = "
188 "min(n, c)");
189 }
190 }
191
192 // The rate lattice, defaulting to a constant rate of one. Its width is the
193 // wider of the closed population and the longest row the caller gave
194 // setLoadDependence: the mixed route below reads the saturation level off
195 // the row itself, and a row cut at Nt is read as a slower station. Past a
196 // row's own end the limited load dependence holds its last rate.
197 std::size_t Wlld = Nt;
198 for (std::size_t i = 0; i < M; ++i)
199 Wlld = std::max(Wlld, sn.stations[i].lldscaling.size());
200 Matrix<T> lldscaling(M, Wlld, one);
201 for (std::size_t i = 0; i < M; ++i) {
202 const std::vector<T>& lld = sn.stations[i].lldscaling;
203 if (lld.empty()) continue;
204 for (std::size_t n = 0; n < Wlld; ++n) lldscaling(i, n) = n < lld.size() ? lld[n] : lld.back();
205 }
206
208 Matrix<T> ST = d.ST, ST0 = d.ST;
209 const Matrix<T> V = detail::station_visits(sn);
210 Matrix<T> SCVnan = sn.scv;
211 for (std::size_t i = 0; i < M; ++i)
212 for (std::size_t k = 0; k < K; ++k)
213 if (sn.disabled[i][k])
214 SCVnan(i, k) =
215 num_traits<T>::from_double(std::numeric_limits<double>::quiet_NaN());
216
217 const std::vector<double> Nchain = detail::chain_population(sn);
218 std::vector<std::size_t> openChains, closedChains;
219 for (std::size_t c = 0; c < C; ++c)
220 (std::isinf(Nchain[c]) ? openChains : closedChains).push_back(c);
221 std::vector<int> Nnc = detail::nc_population(Nchain);
222 std::size_t Ncl = 0;
223 for (std::size_t c : closedChains) Ncl += static_cast<std::size_t>(Nnc[c]);
224
225 // The method travels as a NAME, not as an enum: pfqn_ncld and
226 // pfqn_ncldmx resolve it where the reference does, past their degenerate
227 // returns, so a model that reduces to a closed form is answered rather
228 // than refused for carrying a name the load-dependent ladder does not
229 // know. See pfqn_ncld.h.
230 const std::string& pmethod = opt.method;
231 // The estimator controls the log-domain ladder reads; the exact ladder
232 // ignores them, so this is passed unconditionally.
233 pfqn::NcOptions nopt;
234 nopt.samples = opt.samples;
235 nopt.seed = opt.seed;
236 nopt.tol = opt.tol;
237 const T atol = num_traits<T>::from_double(opt.tol);
238
239 std::vector<T> gamma(M, zero), nserv_t(M, one);
240 for (std::size_t i = 0; i < M; ++i) nserv_t[i] = num_traits<T>::from_double(nservers[i]);
241 std::vector<T> eta(M, one), eta_1(M, zero);
242 const int iter_max = anyFCFS ? opt.iter_max : 1;
243 int it = 0;
244 double lG = 0.0;
245 std::string actualmethod = opt.method;
247 std::vector<T> Xchain(C, zero);
248
249 while (it < iter_max) {
250 {
251 double dev = 0.0;
252 for (std::size_t i = 0; i < M; ++i) {
253 const double v = std::fabs(1.0 - num_traits<T>::to_double(eta[i]) /
254 num_traits<T>::to_double(eta_1[i]));
255 if (!(v <= dev)) dev = v;
256 }
257 if (!(dev > opt.iter_tol)) break;
258 }
259 ++it;
260 eta_1 = eta;
261
262 // Chain demands from the CURRENT service times.
263 for (std::size_t c = 0; c < C; ++c) {
264 const bool open = std::isinf(Nchain[c]);
265 const std::size_t rst = sn.classes[sn.inchain[c][0] - 1].refstat;
266 for (std::size_t i = 0; i < M; ++i) {
267 T st = zero;
268 for (std::size_t k : sn.inchain[c]) st += ST(i, k - 1) * d.alpha(i, k - 1);
269 d.Lchain(i, c) = T(d.Vchain(i, c) * st);
270 if (open && i + 1 == rst) {
271 // A source row carries 1 / arrival rate, summed over the
272 // classes whose rate is defined.
273 T s = zero;
274 for (std::size_t k : sn.inchain[c])
275 if (!sn.disabled[i][k - 1] &&
276 std::isfinite(num_traits<T>::to_double(ST(i, k - 1))))
277 s += ST(i, k - 1);
278 d.STchain(i, c) = s;
279 } else {
280 d.STchain(i, c) = st;
281 }
282 }
283 }
284 for (std::size_t i = 0; i < M; ++i)
285 for (std::size_t c = 0; c < C; ++c) {
286 if (!std::isfinite(num_traits<T>::to_double(d.STchain(i, c))))
287 d.STchain(i, c) = zero;
288 if (!std::isfinite(num_traits<T>::to_double(d.Lchain(i, c))))
289 d.Lchain(i, c) = zero;
290 }
291
292 std::vector<T> lambda(C, zero);
293 for (std::size_t c : openChains) {
294 const std::size_t rst = sn.classes[sn.inchain[c][0] - 1].refstat;
295 if (d.STchain(rst - 1, c) > zero) lambda[c] = T(one / d.STchain(rst - 1, c));
296 }
297
298 Matrix<T> L(M, C, zero), Z(M, C, zero), mu(M, Nt, one);
299 std::vector<std::size_t> infServers;
300 for (std::size_t i = 0; i < M; ++i) {
301 for (std::size_t c = 0; c < C; ++c) L(i, c) = d.Lchain(i, c);
302 if (std::isinf(nservers[i])) {
303 infServers.push_back(i);
304 for (std::size_t c = 0; c < C; ++c) Z(i, c) = d.Lchain(i, c);
305 for (std::size_t n = 1; n <= Nt; ++n)
306 mu(i, n - 1) = num_traits<T>::from_double(static_cast<double>(n));
307 } else {
308 for (std::size_t n = 0; n < Nt; ++n) mu(i, n) = lldscaling(i, n);
309 }
310 }
311
312 Matrix<T> Qchain(M, C, zero);
313 Xchain.assign(C, zero);
314
315 if (!openChains.empty()) {
316 // Mixed limited load-dependent network: the chain-level
317 // normalizing constant of Bruell-Balbo-Afshari effective
318 // capacity (pfqn_ncldmx), which never enumerates the closed
319 // population lattice. The source rows carry only the 1/lambda bookkeeping
320 // demand and are excluded from the queueing set; the delay rows
321 // fold into the think-time vector.
322 std::vector<bool> isSource(M, false), isDelay(M, false);
323 for (std::size_t c : openChains)
324 isSource[sn.classes[sn.inchain[c][0] - 1].refstat - 1] = true;
325 for (std::size_t i : infServers)
326 if (!isSource[i]) isDelay[i] = true;
327 std::vector<std::size_t> queueStations;
328 for (std::size_t i = 0; i < M; ++i)
329 if (!isSource[i] && !isDelay[i]) queueStations.push_back(i);
330 const std::size_t nq = queueStations.size();
331
332 Matrix<T> Zvec(1, C, zero);
333 for (std::size_t i = 0; i < M; ++i)
334 if (isDelay[i])
335 for (std::size_t c = 0; c < C; ++c) Zvec(0, c) += d.Lchain(i, c);
336
337 // pfqn_ldmx_ec reads the limited-load-dependence level b_i off the
338 // rate row itself -- the first column equal to the LAST one -- and
339 // treats every rate past it as saturated. Cutting the row at the
340 // closed population Ncl declares a c-server station saturated at
341 // min(n,c) with n < c whenever c exceeds Ncl (and with no closed
342 // class at all it flattens the row to mu(1)), so keep every column
343 // up to the start of each row's trailing constant run.
344 std::size_t ncol = std::max<std::size_t>(1, Ncl);
345 for (std::size_t qi = 0; qi < nq; ++qi)
346 ncol = std::max(ncol, detail::lld_saturation_level(lldscaling, queueStations[qi]));
347 Matrix<T> Dq(nq, C, zero), muq(nq, ncol, one);
348 const std::size_t Wq = lldscaling.cols();
349 for (std::size_t qi = 0; qi < nq; ++qi) {
350 for (std::size_t c = 0; c < C; ++c) Dq(qi, c) = d.Lchain(queueStations[qi], c);
351 for (std::size_t n = 0; n < ncol; ++n)
352 muq(qi, n) = lldscaling(queueStations[qi], n < Wq ? n : Wq - 1);
353 }
354 std::vector<int> Nmx(C, 0);
355 for (std::size_t c = 0; c < C; ++c)
356 Nmx[c] = std::isinf(Nchain[c]) ? pfqn::OPEN_CLASS : Nnc[c];
357 const pfqn::NcldmxResult<T> mx =
358 pfqn::pfqn_ncldmx(lambda, Dq, Nmx, Zvec, muq, pmethod, atol, nopt);
359 lG = mx.lG;
360 Xchain = mx.XN;
361 for (std::size_t qi = 0; qi < nq; ++qi)
362 for (std::size_t c = 0; c < C; ++c) Qchain(queueStations[qi], c) = mx.QN(qi, c);
363 for (std::size_t i = 0; i < M; ++i)
364 if (isDelay[i])
365 for (std::size_t c = 0; c < C; ++c)
366 Qchain(i, c) = T(d.Lchain(i, c) * Xchain[c]);
367 actualmethod = "ncldmx";
368 } else {
369 Matrix<T> Zzero(1, C, zero);
370 const pfqn::NcldResult<T> base =
371 pfqn::pfqn_ncld(L, Nnc, Zzero, mu, pmethod, atol, nopt);
372 lG = base.lG;
373 actualmethod = base.method;
374
375 const bool repairman = (M == 2 && !infServers.empty());
376 std::size_t firstDelay = infServers.empty() ? 0 : infServers[0];
377 for (std::size_t r = 0; r < C; ++r) {
378 const std::vector<int> Nr = detail::oner(Nnc, r);
379 const double lGr = pfqn::pfqn_ncld(L, Nr, Zzero, mu, pmethod, atol, nopt).lG;
380 Xchain[r] = num_traits<T>::from_double(std::exp(lGr - lG));
381 if (repairman) {
382 const T qd = T(d.Lchain(firstDelay, r) * Xchain[r]);
383 Qchain(firstDelay, r) = qd;
384 for (std::size_t i = 0; i < M; ++i)
385 if (i != firstDelay)
386 Qchain(i, r) = T(num_traits<T>::from_double(Nchain[r]) - qd);
387 continue;
388 }
389 std::size_t nfinite = 0;
390 for (std::size_t i = 0; i < M; ++i)
391 if (std::isfinite(nservers[i])) ++nfinite;
392 for (std::size_t i = 0; i < M; ++i) {
393 if (!(d.Lchain(i, r) > zero)) continue;
394 if (std::isinf(nservers[i])) {
395 Qchain(i, r) = T(d.Lchain(i, r) * Xchain[r]);
396 continue;
397 }
398 if (i + 1 == M && nfinite == 1) {
399 // The only queueing station: give it the balance of
400 // the population rather than a fourth constant.
401 T acc = zero;
402 for (std::size_t i2 : infServers) acc += d.Lchain(i2, r);
403 T q = T(num_traits<T>::from_double(Nchain[r]) - acc * Xchain[r]);
404 for (std::size_t i2 = 0; i2 + 1 < M; ++i2)
405 if (std::isfinite(nservers[i2])) q -= Qchain(i2, r);
406 Qchain(i, r) = q < zero ? zero : q;
407 continue;
408 }
409 // Conditional MVA: the shifted lattice at station i, its
410 // flow-equivalent complement, and the model with station
411 // i removed.
412 const Matrix<T> muhati = pfqn::pfqn_mushift(mu, i);
413 Matrix<T> muhati_row(1, muhati.cols(), zero);
414 for (std::size_t n = 0; n < muhati.cols(); ++n) muhati_row(0, n) = muhati(i, n);
415 const pfqn::FncResult<T> fnc = pfqn::pfqn_fnc(muhati_row);
416 Matrix<T> Lhat(M + 1, C, zero), muhat(M + 1, muhati.cols(), zero);
417 for (std::size_t i2 = 0; i2 < M; ++i2) {
418 for (std::size_t c = 0; c < C; ++c) Lhat(i2, c) = L(i2, c);
419 for (std::size_t n = 0; n < muhati.cols(); ++n)
420 muhat(i2, n) = muhati(i2, n);
421 }
422 for (std::size_t c = 0; c < C; ++c) Lhat(M, c) = L(i, c);
423 for (std::size_t n = 0; n < fnc.mu.cols(); ++n) muhat(M, n) = fnc.mu(0, n);
424 Matrix<T> Lms_i(M - 1, C, zero), mu_i(M - 1, mu.cols(), zero);
425 std::size_t row = 0;
426 for (std::size_t i2 = 0; i2 < M; ++i2) {
427 if (i2 == i) continue;
428 for (std::size_t c = 0; c < C; ++c) Lms_i(row, c) = L(i2, c);
429 for (std::size_t n = 0; n < mu.cols(); ++n) mu_i(row, n) = mu(i2, n);
430 ++row;
431 }
432 const double lGhat_fnci =
433 pfqn::pfqn_ncld(Lhat, Nr, Zzero, muhat, pmethod, atol, nopt).lG;
434 const double lGhatir =
435 pfqn::pfqn_ncld(L, Nr, Zzero, muhati, pmethod, atol, nopt).lG;
436 const double lGr_i =
437 pfqn::pfqn_ncld(Lms_i, Nr, Zzero, mu_i, pmethod, atol, nopt).lG;
438 const double dlGa = lGhat_fnci - lGhatir;
439 const double dlG_i = lGr_i - lGhatir;
440 const T CQ = T(num_traits<T>::from_double(std::exp(dlGa) - 1.0) +
441 fnc.c[0] * num_traits<T>::from_double(std::exp(dlG_i) - 1.0));
442 const double ldDemand = std::log(num_traits<T>::to_double(L(i, r))) +
443 lGhatir -
444 std::log(num_traits<T>::to_double(mu(i, 0))) - lGr;
445 Qchain(i, r) = T(num_traits<T>::from_double(std::exp(ldDemand)) *
446 Xchain[r] * (one + CQ));
447 }
448 }
449 }
450
451 Matrix<T> Rchain(M, C, zero), Tchain(M, C, zero);
452 for (std::size_t i = 0; i < M; ++i)
453 for (std::size_t c = 0; c < C; ++c) {
454 if (Xchain[c] != zero && d.Vchain(i, c) != zero)
455 Rchain(i, c) = T(Qchain(i, c) / Xchain[c] / d.Vchain(i, c));
456 Tchain(i, c) = T(Xchain[c] * d.Vchain(i, c));
457 }
458 for (std::size_t i : infServers)
459 for (std::size_t c = 0; c < C; ++c)
460 Rchain(i, c) =
461 d.Vchain(i, c) == zero ? zero : T(d.Lchain(i, c) / d.Vchain(i, c));
462 for (std::size_t c = 0; c < C; ++c) {
463 if (Nnc[c] != 0) continue;
464 Xchain[c] = zero;
465 for (std::size_t i = 0; i < M; ++i) {
466 Qchain(i, c) = zero;
467 Rchain(i, c) = zero;
468 Tchain(i, c) = zero;
469 }
470 }
471 for (std::size_t i = 0; i < M; ++i)
472 for (std::size_t c = 0; c < C; ++c) {
473 if (!std::isfinite(num_traits<T>::to_double(Qchain(i, c)))) Qchain(i, c) = zero;
474 if (!std::isfinite(num_traits<T>::to_double(Rchain(i, c)))) Rchain(i, c) = zero;
475 }
476
477 d.ST = ST;
479 Tchain, Xchain);
480 out.STeff = ST;
481
483 opt.highvar, isFCFS, sn.rates, ST0, V, SCVnan, cls.Tp, cls.U, gamma, nserv_t);
484 ST = na.ST;
485 gamma = na.gamma;
486 eta = na.eta;
487 }
488
489 Matrix<T> Q = cls.Q, U = cls.U, R = cls.R, Tp = cls.Tp;
490 std::vector<T> X = cls.X;
491 auto absify = [&](Matrix<T>& A) {
492 for (std::size_t i = 0; i < A.rows(); ++i)
493 for (std::size_t j = 0; j < A.cols(); ++j)
494 if (A(i, j) < zero) A(i, j) = T(-A(i, j));
495 };
496 absify(Q);
497 absify(R);
498 absify(U);
499 for (T& x : X)
500 if (x < zero) x = T(-x);
501
502 // Utilization is re-derived rather than taken from the deaggregation:
503 // at a load-dependent station the chain-level U carries the lattice, and
504 // what the reference reports is the fraction of the SERVERS busy.
505 for (std::size_t i = 0; i < M; ++i) {
506 const bool multi = std::isfinite(nservers[i]) && nservers[i] > 1.0;
507 if (multi || std::isinf(nservers[i])) {
508 const double div = multi ? nservers[i] : 1.0;
509 for (std::size_t k = 0; k < K; ++k) {
510 std::size_t c = C;
511 for (std::size_t cc = 0; cc < C; ++cc)
512 if (sn.chains[cc][k]) c = cc;
513 if (c == C) continue;
514 const std::size_t rs = sn.classes[k].refstat;
515 const T vref = sn.visits[c](sn.stateful_of_station(rs) - 1, k);
516 if (vref == zero) continue;
517 const T vi = sn.visits[c](sn.stateful_of_station(i + 1) - 1, k);
518 const bool open = std::isinf(sn.classes[k].population);
519 T rate = zero;
520 if (open) {
521 // the open class's own arrival rate, sn's source rate
522 const std::size_t src = sn.classes[k].refstat;
523 if (!sn.disabled[src - 1][k]) rate = sn.rates(src - 1, k);
524 } else {
525 rate = X[k];
526 }
527 if (!(rate > zero)) continue;
528 U(i, k) = T(rate * vi / vref * ST(i, k) / num_traits<T>::from_double(div));
529 }
530 } else {
531 // `solver_ncld.m:300` divides by `max(lldscaling(ist,:))` over the
532 // STATION'S OWN ROW, whose width is whatever the caller gave
533 // setLoadDependence -- not over the Nt-column lattice this file
534 // builds. Taking the max over the lattice dropped every entry past
535 // the population: on a 4-job model with scaling [1 1.6 2 2.2 2.3]
536 // the divisor came out 2.2 instead of 2.3 and Util read 0.419042
537 // against the reference's 0.400823, with QLen, RespT and Tput all
538 // exact. A station with no row of its own keeps the lattice's
539 // constant one, which is what the row would hold anyway.
540 T mx = zero;
541 const std::vector<T>& lldrow = sn.stations[i].lldscaling;
542 if (lldrow.empty()) {
543 for (std::size_t n = 0; n < Nt; ++n)
544 if (lldscaling(i, n) > mx) mx = lldscaling(i, n);
545 } else {
546 for (std::size_t n = 0; n < lldrow.size(); ++n)
547 if (lldrow[n] > mx) mx = lldrow[n];
548 }
549 if (mx > zero)
550 for (std::size_t k = 0; k < K; ++k) U(i, k) = T(U(i, k) / mx);
551 T s = zero;
552 for (std::size_t k = 0; k < K; ++k) s += U(i, k);
553 if (num_traits<T>::to_double(s) > 1.0)
554 for (std::size_t k = 0; k < K; ++k) U(i, k) = T(U(i, k) / s);
555 }
556 }
557
558 auto clear_nonfinite = [&](Matrix<T>& A) {
559 for (std::size_t i = 0; i < A.rows(); ++i)
560 for (std::size_t j = 0; j < A.cols(); ++j)
561 if (!std::isfinite(num_traits<T>::to_double(A(i, j)))) A(i, j) = zero;
562 };
563 clear_nonfinite(Q);
564 clear_nonfinite(U);
565 clear_nonfinite(R);
566 for (T& x : X)
567 if (!std::isfinite(num_traits<T>::to_double(x))) x = zero;
568
569 for (std::size_t c = 0; c < C; ++c) {
570 if (std::isinf(Nchain[c])) continue;
571 T qden = zero;
572 for (std::size_t k : sn.inchain[c])
573 for (std::size_t i = 0; i < M; ++i) qden += Q(i, k - 1);
574 const T ratio =
575 qden > zero ? T(num_traits<T>::from_double(Nchain[c]) / qden) : zero;
576 for (std::size_t k : sn.inchain[c]) {
577 X[k - 1] = T(ratio * X[k - 1]);
578 for (std::size_t i = 0; i < M; ++i) {
579 Q(i, k - 1) = T(ratio * Q(i, k - 1));
580 Tp(i, k - 1) = T(ratio * Tp(i, k - 1));
581 U(i, k - 1) = T(ratio * U(i, k - 1));
582 R(i, k - 1) = Tp(i, k - 1) == zero ? zero : T(Q(i, k - 1) / Tp(i, k - 1));
583 }
584 }
585 }
586
587 out.sol.Q = Q;
588 out.sol.U = U;
589 out.sol.R = R;
590 out.sol.Tp = Tp;
591 out.sol.X = X;
592 out.sol.C.assign(K, zero);
593 for (std::size_t k = 0; k < K; ++k) {
594 const double njobs = sn.classes[k].population;
595 out.sol.C[k] = (std::isfinite(njobs) && X[k] != zero)
596 ? T(num_traits<T>::from_double(njobs) / X[k])
597 : cls.C[k];
598 }
599 out.sol.lG = lG;
600 out.sol.iter = it;
601 out.sol.method = actualmethod;
602 out.actualmethod = actualmethod;
603 return out;
604 }
605}
606
607} // namespace nc
608} // namespace line
609
610#endif // LINE_SOLVERS_NC_SOLVER_NCLD_H
std::size_t cols() const
Definition matrix.h:90
UnsupportedError(const std::string &what)
Definition error.h:51
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.
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:214
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_conv(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_conv.m.
NcSolution< T > solver_ncld(const qn::NetworkStruct< T > &sn_in, const NcSolverOptions &opt)
Port of solver_ncld.m.
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.
NcldmxResult< T > pfqn_ncldmx(const std::vector< T > &lambda, const Matrix< T > &D, const std::vector< int > &N, const Matrix< T > &Z, const Matrix< T > &mu, const std::string &method, const T &atol, const NcOptions &nopt)
Normalizing constant of a MIXED open/closed network with limited load dependence.
constexpr int OPEN_CLASS
Marks an open (infinite-population) class in a population vector.
Definition pfqn_mvams.h:77
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
Matrix< T > pfqn_mushift(const Matrix< T > &mu, const std::vector< std::size_t > &iset)
Shift the load-dependent service-rate lattice of selected stations.
FncResult< T > pfqn_fnc(const Matrix< T > &alpha)
Automatic offset search (the one-argument MATLAB branch).
Definition pfqn_fnc.h:166
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.
Load-dependent rates of the functional server f(n) = n + c.
Shift the load-dependent service-rate lattice of selected stations.
Exact Mean Value Analysis for mixed open/closed networks with multiserver stations.
Normalizing constant of a LOAD-DEPENDENT closed network: the dispatcher.
Normalizing constant of a MIXED open/closed network with limited load dependence.
Chain aggregation and de-aggregation.
Port of solver_nc.m: the load-INDEPENDENT normalizing-constant analyzer.
Exact convolution analysis of a closed network with class-dependent service rates.
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:191
std::vector< T > C
Definition sn_chain.h:193
std::vector< T > X
Definition sn_chain.h:193
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
Return value of pfqn_fnc, mirroring [mu, c].
Definition pfqn_fnc.h:62
std::vector< T > c
Definition pfqn_fnc.h:64
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
std::string method
the algorithm actually used
Definition pfqn_ncld.h:144
double lG
its logarithm
Definition pfqn_ncld.h:143
std::vector< T > XN
(R) throughputs: G(N-e_r)/G(N) closed, lambda_r open
Definition pfqn_ncldmx.h:91
Matrix< T > QN
(M x R) mean queue lengths
Definition pfqn_ncldmx.h:92
double lG
its logarithm
Definition pfqn_ncldmx.h:87
One job class of the network.
double population
infinite for an open class