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/** True when any station carries a load- or class-dependent rate. */
81template <class T>
82bool has_scaling(const qn::NetworkStruct<T>& sn) {
83 for (const qn::Station<T>& st : sn.stations)
84 if (!st.lldscaling.empty() || static_cast<bool>(st.cdscaling)) return true;
85 return false;
86}
87
88} // namespace detail
89
90/**
91 * Port of `solver_ncld_analyzer.m`: the load-dependent analyzer, which is
92 * `solver_ncld` plus the same fractional-population interpolation the
93 * load-independent one applies.
94 */
95template <class T>
99
100/**
101 * Port of `solver_nc_analyzer.m`.
102 *
103 * @param sn the refreshed struct
104 * @param opt solver controls
105 */
106template <class T>
108 // Discrete time first: the slot lattice is a property of the MODEL, not of
109 // a method, so it precedes every method-keyed branch below and refuses a
110 // model outside the discrete-time product form instead of falling through.
111 if (opt.slotted) return solver_nc_dt(sn, opt);
112
113 // Closed think+DPS network: Morrison's heavy-usage expansion of the generating
114 // function (npfqn_dps_morrison), the default for that shape. Not a product-form
115 // route: lG is NaN. The runner intercepts this shape before reaching here; the
116 // arm is repeated for the inner entry points (fork-join, ncld) that call the
117 // analyzer directly.
118 if (nc_is_dps_model(sn) && (opt.method == "default" || opt.method == "morrison"))
120 // Named on a model that is not the shape. The runner refuses this before reaching
121 // here; the arm is repeated because the inner entry points (fork-join, ncld) call
122 // this analyzer directly, and without it the method falls through to the ordinary
123 // normalizing-constant path and answers under the caller's label.
124 if (opt.method == "morrison")
125 throw UnsupportedError(
126 "solver_nc_analyzer: method 'morrison' requires a CLOSED network of exactly two "
127 "stations, one infinite-server (think) station and one single-server DPS station with "
128 "exponential service, which this model is not.");
129
130 // Exact balanced fairness first: a pure-OI network on 'default' or 'exact'
131 // never reaches the sampler below.
132 if (nc_is_oi_model(sn) && (opt.method == "default" || opt.method == "exact"))
134
135 // The importance sampler, on an explicit request and on 'default' for a P&S
136 // tandem with a genuine swap graph, which is reducible and has no exact path.
137 if (nc_is_pas_model(sn) &&
138 (opt.method == "default" || opt.method == "is" || opt.method == "sampling"))
140
141 bool pas = false;
142 if (detail::has_svc_rate_fun(sn, pas))
143 throw UnsupportedError(
144 "solver_nc_analyzer: a station with a service rate function (OI / PAS) has no "
145 "product-form demand and cannot be solved by the ordinary normalizing-constant path");
146
147 bool anyOpen = false;
148 for (const qn::JobClass& c : sn.classes)
149 if (std::isinf(c.population)) anyOpen = true;
150
151 if (opt.method == "is" && anyOpen)
152 throw UnsupportedError(
153 "solver_nc_analyzer: the 'is' importance-sampling method requires a closed queueing "
154 "network. Use 'sampling' for an open or mixed model");
155
156 // The Maximum Entropy Method, on an explicit request only: 'default' keeps
157 // the normalizing-constant path it had before MEM existed.
158 if (opt.method == "mem") return solver_nc_mem(sn, opt);
159
160 bool multiserver = false;
161 for (const qn::Station<T>& st : sn.stations)
162 if (std::isfinite(st.nservers) && st.nservers > 1.0) multiserver = true;
163 if (multiserver && anyOpen && opt.method == "exact")
164 throw UnsupportedError(
165 "solver_nc_analyzer: the NC solver cannot provide exact solutions for open or mixed "
166 "multiserver queueing networks. Remove the 'exact' option");
167
168 // Fractional closed populations: solve at floor and at ceiling and
169 // interpolate, which is the only meaning a non-integral population has for
170 // a constant defined on the population lattice.
171 double eta_max = 0.0;
172 for (const qn::JobClass& c : sn.classes)
173 if (std::isfinite(c.population)) {
174 const double e = std::fabs(c.population - std::floor(c.population));
175 if (e > eta_max) eta_max = e;
176 }
177 if (eta_max > GlobalConstants::FineTol) {
178 qn::NetworkStruct<T> lo = sn, hi = sn;
179 for (std::size_t k = 0; k < sn.nclasses; ++k) {
180 const double p = sn.classes[k].population;
181 if (!std::isfinite(p)) continue;
182 lo.classes[k].population = std::floor(p);
183 hi.classes[k].population = std::ceil(p);
184 }
185 const NcSolution<T> f = solver_nc(lo, opt);
186 const NcSolution<T> c = solver_nc(hi, opt);
187 NcSolution<T> out = c;
188 const T e = num_traits<T>::from_double(eta_max);
189 auto blend = [&](Matrix<T>& A, const Matrix<T>& Af, const Matrix<T>& Ac) {
190 A = Af;
191 for (std::size_t i = 0; i < A.rows(); ++i)
192 for (std::size_t j = 0; j < A.cols(); ++j)
193 A(i, j) = T(Af(i, j) + e * (Ac(i, j) - Af(i, j)));
194 };
195 blend(out.sol.Q, f.sol.Q, c.sol.Q);
196 blend(out.sol.U, f.sol.U, c.sol.U);
197 blend(out.sol.R, f.sol.R, c.sol.R);
198 blend(out.sol.Tp, f.sol.Tp, c.sol.Tp);
199 for (std::size_t k = 0; k < out.sol.X.size(); ++k) {
200 out.sol.X[k] = T(f.sol.X[k] + e * (c.sol.X[k] - f.sol.X[k]));
201 out.sol.C[k] = T(f.sol.C[k] + e * (c.sol.C[k] - f.sol.C[k]));
202 }
203 // REFERENCE DEFECT, reproduced: the interpolation of lG reads
204 // lGf + eta*(lGf - lGc), which extrapolates AWAY from the ceiling
205 // solve instead of towards it. It is kept because lG is a reported
206 // quantity and silently changing it would move every caller's
207 // getProbNormConstAggr on fractional populations.
208 out.sol.lG = f.sol.lG + eta_max * (f.sol.lG - c.sol.lG);
209 out.sol.iter = f.sol.iter + c.sol.iter;
210 return out;
211 }
212 return solver_nc(sn, opt);
213}
214
215/**
216 * Port of `@@SolverNC/ncDispatch.m`: the inner solve of the fork-join fixed
217 * point, which is the load-dependence test and nothing else.
218 */
219template <class T>
221 const NcSolution<T> out = opt.slotted ? solver_nc_dt(sn, opt)
222 : detail::has_scaling(sn) ? solver_ncld_analyzer(sn, opt)
224 const double lg = num_traits<T>::to_double(out.sol.lG);
225 if (std::isfinite(lg))
226 line::util::LineConsole::step("normalizing constant obtained: log G = %.6g", lg);
227 return out;
228}
229
230} // namespace nc
231} // namespace line
232
233#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...
Definition nc_dispatch.h:96
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.
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:668
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)
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.