LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
nc_dispatch.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_NC_DISPATCH_H
6#define LINE_SOLVERS_NC_NC_DISPATCH_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_nc_analyzer.m`, `solver_ncld_analyzer.m` and
12 * `@@SolverNC/ncDispatch.m`: one inner solve, choosing the analyzer that fits.
13 *
14 * THE ORDER IS THE CONTRACT. As in `mva_dispatch.h`, the branches are NOT
15 * disjoint -- an order-independent station is also a multiserver, a
16 * pass-and-swap tandem is also a closed network -- so the first match wins and
17 * reordering silently changes which algorithm a model gets. The sequence is
18 * the reference's, top to bottom:
19 *
20 * 0 discrete-time (slotted) model, on an explicit options.slotted
21 * 1 order-independent closed network, on 'default' or 'exact'
22 * 2 pass-and-swap importance sampling, on 'default', 'is' or 'sampling'
23 * 3 'is' on an open or mixed model -> rejected by name
24 * 4 Maximum Entropy Method, on an explicit 'mem'
25 * 5 'exact' on an open or mixed multiserver model -> rejected by name
26 * 6 fractional closed populations, by interpolation
27 * 7 everything else -> solver_nc / solver_ncld
28 *
29 * Branch 1 is the EXACT balanced-fairness analyzer and branch 2 the sampler, so
30 * a pure-OI tandem on 'default' never reaches branch 2; only a genuine swap
31 * graph, or an explicit 'is' / 'sampling', does. A station carrying a service
32 * rate function that satisfies NEITHER predicate is refused by name rather than
33 * sent down the ordinary normalizing-constant path, where its rate function
34 * would simply be ignored.
35 *
36 * `ncDispatch` (the fork-join inner solve) is the load-dependence test alone:
37 * the MMT transformation returns a plain mixed queueing network, so none of the
38 * specialised routes can apply to it.
39 */
40
42#include <cmath>
43#include <string>
44#include <vector>
45
54#include "line/util/error.h"
55
56namespace line {
57namespace nc {
58
59namespace detail {
60
61/**
62 * Does any station carry a service rate function, and does any of them have a
63 * non-zero swap graph (which makes it a genuine pass-and-swap station)?
64 *
65 * Kept as one pass because both callers need both answers, and the swap graph is
66 * read through `station_swap_graph` so the `refreshLocalVars` defaults apply.
67 */
68template <class T>
69bool has_svc_rate_fun(const qn::NetworkStruct<T>& sn, bool& pas) {
70 pas = false;
71 bool any = false;
72 for (std::size_t i = 0; i < sn.nstations; ++i) {
73 if (!sn.stations[i].svc_rate_fun) continue;
74 any = true;
75 if (!qn::station_swap_graph_is_zero(sn, i + 1)) pas = true;
76 }
77 return any;
78}
79
80/**
81 * True when any station carries a load-, class- or joint-dependent rate.
82 *
83 * THE TEST MUST NAME ALL THREE (`ncDispatch.m:22`). It read `lldscaling` and
84 * `cdscaling` only until 2026-09-13, so a CLOSED model whose only declared
85 * dependence was `jdscaling` never reached `solver_ncld_analyzer`: it went to
86 * the load-INDEPENDENT analyzer and was answered by CoMoM at its unscaled rate
87 * (QLen 3.015385 where the eta = 0.8 model's exact answer is 3.206144).
88 */
89template <class T>
90bool has_scaling(const qn::NetworkStruct<T>& sn) {
91 for (const qn::Station<T>& st : sn.stations)
92 if (!st.lldscaling.empty() || static_cast<bool>(st.cdscaling) ||
93 static_cast<bool>(st.jdscaling))
94 return true;
95 return false;
96}
97
98} // namespace detail
99
100/**
101 * Port of `solver_ncld_analyzer.m`: the load-dependent analyzer, which is
102 * `solver_ncld` plus the same fractional-population interpolation the
103 * load-independent one applies.
104 */
105template <class T>
109
110/**
111 * Port of `solver_nc_analyzer.m`.
112 *
113 * @param sn the refreshed struct
114 * @param opt solver controls
115 */
116template <class T>
118 // Discrete time first: the slot lattice is a property of the MODEL, not of
119 // a method, so it precedes every method-keyed branch below and refuses a
120 // model outside the discrete-time product form instead of falling through.
121 if (opt.slotted) return solver_nc_dt(sn, opt);
122
123 // Closed think+DPS network: Morrison's heavy-usage expansion of the generating
124 // function (npfqn_dps_morrison), the default for that shape. Not a product-form
125 // route: lG is NaN. The runner intercepts this shape before reaching here; the
126 // arm is repeated for the inner entry points (fork-join, ncld) that call the
127 // analyzer directly.
128 if (nc_is_dps_model(sn) && (opt.method == "default" || opt.method == "morrison"))
130 // Named on a model that is not the shape. The runner refuses this before reaching
131 // here; the arm is repeated because the inner entry points (fork-join, ncld) call
132 // this analyzer directly, and without it the method falls through to the ordinary
133 // normalizing-constant path and answers under the caller's label.
134 if (opt.method == "morrison")
135 throw UnsupportedError(
136 "solver_nc_analyzer: method 'morrison' requires a CLOSED network of exactly two "
137 "stations, one infinite-server (think) station and one single-server DPS station with "
138 "exponential service, which this model is not.");
139
140 // Exact balanced fairness first: a pure-OI network on 'default' or 'exact'
141 // never reaches the sampler below.
142 if (nc_is_oi_model(sn) && (opt.method == "default" || opt.method == "exact"))
144
145 // The importance sampler, on an explicit request and on 'default' for a P&S
146 // tandem with a genuine swap graph, which is reducible and has no exact path.
147 if (nc_is_pas_model(sn) &&
148 (opt.method == "default" || opt.method == "is" || opt.method == "sampling"))
150
151 bool pas = false;
152 if (detail::has_svc_rate_fun(sn, pas))
153 throw UnsupportedError(
154 "solver_nc_analyzer: a station with a service rate function (OI / PAS) has no "
155 "product-form demand and cannot be solved by the ordinary normalizing-constant path");
156
157 bool anyOpen = false;
158 for (const qn::JobClass& c : sn.classes)
159 if (std::isinf(c.population)) anyOpen = true;
160
161 if (opt.method == "is" && anyOpen)
162 throw UnsupportedError(
163 "solver_nc_analyzer: the 'is' importance-sampling method requires a closed queueing "
164 "network. Use 'sampling' for an open or mixed model");
165
166 // The Maximum Entropy Method, on an explicit request only: 'default' keeps
167 // the normalizing-constant path it had before MEM existed.
168 if (opt.method == "mem") return solver_nc_mem(sn, opt);
169
170 bool multiserver = false;
171 for (const qn::Station<T>& st : sn.stations)
172 if (std::isfinite(st.nservers) && st.nservers > 1.0) multiserver = true;
173 if (multiserver && anyOpen && opt.method == "exact")
174 throw UnsupportedError(
175 "solver_nc_analyzer: the NC solver cannot provide exact solutions for open or mixed "
176 "multiserver queueing networks. Remove the 'exact' option");
177
178 // Fractional closed populations: solve at floor and at ceiling and
179 // interpolate, which is the only meaning a non-integral population has for
180 // a constant defined on the population lattice.
181 double eta_max = 0.0;
182 for (const qn::JobClass& c : sn.classes)
183 if (std::isfinite(c.population)) {
184 const double e = std::fabs(c.population - std::floor(c.population));
185 if (e > eta_max) eta_max = e;
186 }
187 if (eta_max > GlobalConstants::FineTol) {
188 qn::NetworkStruct<T> lo = sn, hi = sn;
189 for (std::size_t k = 0; k < sn.nclasses; ++k) {
190 const double p = sn.classes[k].population;
191 if (!std::isfinite(p)) continue;
192 lo.classes[k].population = std::floor(p);
193 hi.classes[k].population = std::ceil(p);
194 }
195 const NcSolution<T> f = solver_nc(lo, opt);
196 const NcSolution<T> c = solver_nc(hi, opt);
197 NcSolution<T> out = c;
198 const T e = num_traits<T>::from_double(eta_max);
199 auto blend = [&](Matrix<T>& A, const Matrix<T>& Af, const Matrix<T>& Ac) {
200 A = Af;
201 for (std::size_t i = 0; i < A.rows(); ++i)
202 for (std::size_t j = 0; j < A.cols(); ++j)
203 A(i, j) = T(Af(i, j) + e * (Ac(i, j) - Af(i, j)));
204 };
205 blend(out.sol.Q, f.sol.Q, c.sol.Q);
206 blend(out.sol.U, f.sol.U, c.sol.U);
207 blend(out.sol.R, f.sol.R, c.sol.R);
208 blend(out.sol.Tp, f.sol.Tp, c.sol.Tp);
209 for (std::size_t k = 0; k < out.sol.X.size(); ++k) {
210 out.sol.X[k] = T(f.sol.X[k] + e * (c.sol.X[k] - f.sol.X[k]));
211 out.sol.C[k] = T(f.sol.C[k] + e * (c.sol.C[k] - f.sol.C[k]));
212 }
213 // REFERENCE DEFECT, reproduced: the interpolation of lG reads
214 // lGf + eta*(lGf - lGc), which extrapolates AWAY from the ceiling
215 // solve instead of towards it. It is kept because lG is a reported
216 // quantity and silently changing it would move every caller's
217 // getProbNormConstAggr on fractional populations.
218 out.sol.lG = f.sol.lG + eta_max * (f.sol.lG - c.sol.lG);
219 out.sol.iter = f.sol.iter + c.sol.iter;
220 return out;
221 }
222 return solver_nc(sn, opt);
223}
224
225/**
226 * Port of `@@SolverNC/ncDispatch.m`: the inner solve of the fork-join fixed
227 * point, which is the load-dependence test and nothing else.
228 */
229template <class T>
231 const NcSolution<T> out = opt.slotted ? solver_nc_dt(sn, opt)
232 : detail::has_scaling(sn) ? solver_ncld_analyzer(sn, opt)
234 const double lg = num_traits<T>::to_double(out.sol.lG);
235 if (std::isfinite(lg))
236 line::util::LineConsole::step("normalizing constant obtained: log G = %.6g", lg);
237 return out;
238}
239
240} // namespace nc
241} // namespace line
242
243#endif // LINE_SOLVERS_NC_NC_DISPATCH_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:51
A network plus its refreshed NetworkStruct.
std::vector< JobClass > classes
static void step(const char *fmt,...)
Write one progress line.
The exception types the port throws.
Running progress log of a LINE solver run (the "solver console").
bool nc_is_oi_model(const qn::NetworkStruct< T > &sn)
Port of nc_is_oi_model.m: a closed network with at least one OI station and nothing but BCMP product-...
NcSolution< T > solver_nc_pas_is_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_pas_is_analyzer.m.
NcSolution< T > solver_nc_dt(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Exact discrete-time analysis of sn.
NcSolution< T > solver_nc_dps_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Analyzes the closed think+DPS network.
NcSolution< T > solver_ncld(const qn::NetworkStruct< T > &sn_in, const NcSolverOptions &opt)
Port of solver_ncld.m.
NcSolution< T > solver_nc_mem(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_mem.m.
NcSolution< T > solver_nc(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc.m.
Definition solver_nc.h:142
bool nc_is_pas_model(const qn::NetworkStruct< T > &sn)
Port of nc_is_pas_model.m: a closed two-station OI / P&S tandem.
NcSolution< T > solver_nc_oi_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_oi_analyzer.m.
NcSolution< T > solver_nc_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_nc_analyzer.m.
NcSolution< T > solver_ncld_analyzer(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of solver_ncld_analyzer.m: the load-dependent analyzer, which is solver_ncld plus the same fract...
bool nc_is_dps_model(const qn::NetworkStruct< T > &sn)
True when the model is the closed two-station network Morrison's expansion is derived for: one infini...
NcSolution< T > nc_dispatch(const qn::NetworkStruct< T > &sn, const NcSolverOptions &opt)
Port of @@SolverNC/ncDispatch.m: the inner solve of the fork-join fixed point, which is the load-depe...
bool station_swap_graph_is_zero(const NetworkStruct< T > &sn, std::size_t ist)
True when the station's materialized swap graph is entirely zero.
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.
Port of solver_nc.m: the load-INDEPENDENT normalizing-constant analyzer.
Heavy-usage asymptotic analysis of the closed two-station network with one think (infinite-server) st...
Exact normalizing-constant analysis of a discrete-time (slotted) model.
Port of solver_nc_mem.m and solver_nc_mem_supports.m: the Maximum Entropy Method of Kouvatsos (1994).
Order-independent (OI) and pass-and-swap (P&S) normalizing-constant analysis.
Port of solver_ncld.m: the LOAD-DEPENDENT normalizing-constant analyzer.
static constexpr double FineTol
Definition lang_types.h:760
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
Controls, defaulting to SolverOptions('NC') in the reference.
Definition nc_types.h:33
One job class of the network.
double population
infinite for an open class
One station of the network.
double nservers
may be infinite (a Delay, or an inf-scheduled task)
CdScaling< T > jdscaling
sn.jdscaling for this station: MATLAB's Station.ljdScaling, the JOINT dependence map eta_i(n),...
std::vector< T > lldscaling
sn.lldscaling for this station: the multiplier at population 1, 2, ... Empty when the station is not ...
CdScaling< T > cdscaling
sn.cdscaling for this station: the class-dependence map, empty when unset.