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