LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mva_sjn.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_MVA_SOLVER_MVA_SJN_H
6#define LINE_SOLVERS_MVA_SOLVER_MVA_SJN_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Closed networks with shortest-job-next (SJF) stations, ladder branch 0.
12 *
13 * Templated port of `matlab/src/solvers/MVA/solver_mva_sjn_analyzer.m`,
14 * cross-checked against
15 * `jar/src/main/java/jline/solvers/mva/analyzers/Solver_mva_sjn_analyzer.java`.
16 *
17 * A non-preemptive SJF station serves the shortest queued job first, the job
18 * size being known on arrival. It is not product form and no ordinary MVA
19 * equation covers it, so the station is modelled by the conditional waiting
20 * time equation of K. Kant, "MVA approximations for SJN scheduling",
21 * Performance Evaluation 15(1):41-61, 1992, which `pfqn_sjn.h` carries.
22 *
23 * WHY THE LADDER PUTS THIS FIRST. `mvaDispatch.m` tests `hasSJN` ahead of every
24 * other branch, and the branches are NOT disjoint: a closed model with an SJF
25 * station and a delay would otherwise fall through to the generic AMVA path,
26 * which reads only the mean service time and would report the SJF station as if
27 * it scheduled size-blind. The answer would be a plausible number for a
28 * different model, which is worse than a refusal.
29 *
30 * CLOSED MODELS ONLY, BY NAME. The conditional waiting time equation is a
31 * POPULATION recursion, so the open case has nothing to recur over. MATLAB's
32 * getFeatureSet declares SchedStrategy_SJF unconditionally and the dispatch
33 * then refuses the open case by name; the boolean registry cannot express
34 * "this discipline, but only in a closed model", so the C++ gate declares the
35 * feature and this file carries the same imperative refusal.
36 *
37 * LATTICE OR FIXED POINT. The exact recursion steps over prod(N+1) states, so
38 * `default` switches to the Schweitzer closure once the lattice exceeds
39 * `kLatticeMax`. Ask for `exact` or `mva` to force the lattice, `amva` or `bs`
40 * to force the fixed point; `sjn.mva` and `sjn.amva` name them directly. The
41 * reported `actualmethod` is the one actually run.
42 *
43 * OTHER STATIONS. The remaining stations are solved with the single-server MVA
44 * equation, so the reference restricts them to INF (folded into the think time)
45 * and single-server PS, LCFS-PR, FCFS or SIRO. Anything else, and any
46 * multi-server queueing station, is refused by name.
47 *
48 * WARNINGS, AND HOW FAR THEY GET. The reference calls line_warning in two
49 * places that still return a usable answer: the utilization cap binding, and
50 * the fixed point exhausting its iterations. `pfqn_sjn.h` turns both into
51 * result flags because the api layer has no warning channel; this file turns
52 * the flags back into the reference's own text on
53 * `SjnAnalyzerResult::warning`, which `mva_dispatch.h` copies onto
54 * `DispatchResult::warning`. That is as far as it goes: this port has NO
55 * general warning facility, and `solver_mva_runner.h` does not read the field,
56 * so a user going through SolverMVA still does not see it. Recorded here as a
57 * known divergence rather than left silent -- the capped answer is stable and
58 * satisfies the population law exactly, but its accuracy is not warranted, and
59 * a number that looks authoritative while carrying an accuracy claim the
60 * reference declines to make is worse than a visibly wrong one.
61 *
62 * Arithmetic: TRANSCENDENTAL. The recursion evaluates regularized incomplete
63 * gammas on a Simpson grid; `pfqn_sjn.h` is a double-precision API for that
64 * reason, and the exact (Rational) path is refused by name below.
65 */
66
67#include <cmath>
68#include <limits>
69#include <string>
70#include <vector>
71
77
78namespace line {
79namespace mva {
80
81/**
82 * Lattice size above which `default` prefers the fixed point.
83 *
84 * The reference reads it, and the grid parameters ns / Lfactor / umax, from
85 * `options.config.sjn_*`. MvaOptions carries no config map, so the four sit at
86 * the reference's own defaults here; the method name still selects the route.
87 */
88inline constexpr double kSjnLatticeMax = 1e5;
89
90/** What the analyzer returns, the reference's metrics plus its `actualmethod`. */
91template <class T>
94 std::string actualmethod;
95 /**
96 * Non-empty when the reference would have called line_warning AND RETURNED,
97 * carrying its text. Today: the utilization cap binding, and the fixed
98 * point running out of iterations.
99 *
100 * This is the shape solver_nc_cdf.h:83 established for a reference warning
101 * that accompanies a usable answer, as opposed to solver_mam_retrial.h:437,
102 * which throws because its warning accompanies an answer the port declines
103 * to stand behind. The SJN cap is the first kind: the numbers are stable
104 * and the population law still holds exactly, but their ACCURACY is not
105 * warranted, and MATLAB says so out loud. Dropping it would leave the
106 * caller an authoritative-looking number with a silent accuracy claim the
107 * reference explicitly declines to make.
108 */
109 std::string warning;
110};
111
112/** True when the layer has an SJF station, the reference's `any(sn.sched == SchedStrategy.SJF)`. */
113template <class T>
115 for (const auto& st : L.stations)
116 if (st.sched == lang::SchedStrategy::SJF) return true;
117 return false;
118}
119
120/** Port of `solver_mva_sjn_analyzer.m`. */
121template <class T>
124 if constexpr (!num_traits<T>::has_transcendental) {
125 (void)L;
126 (void)opt;
127 throw UnsupportedError(
128 "solver_mva_sjn_analyzer: the SJN conditional waiting time recursion needs "
129 "transcendental arithmetic (regularized incomplete gammas on a quadrature grid); "
130 "rerun this model with --arith double or --arith real");
131 } else {
132 const std::size_t M = L.nstations, C = L.nchains;
134
135 for (std::size_t c = 0; c < C; ++c)
136 if (std::isinf(d.Nchain[c]))
137 throw UnsupportedError(
138 "SolverMVA supports shortest-job-next (SJF) scheduling only in closed models, the "
139 "conditional waiting time equation being a population recursion. Use SolverLDES, "
140 "or SolverMVA with SRPT or PSJF for the preemptive size-based open queue");
141
142 // rows: the queueing stations handed to the SJN recursion; infrows: the
143 // delay stations, folded into the think time; sjnrows: positions of the SJN
144 // stations WITHIN rows, which is the indexing pfqn_sjn expects
145 std::vector<std::size_t> rows, infrows, sjnrows;
146 for (std::size_t i = 0; i < M; ++i) {
147 const SchedStrategy s = L.stations[i].sched;
148 const double k = L.stations[i].nservers;
149 if (s == SchedStrategy::EXT) continue; // no arrival stream in a closed model
150 if (s == SchedStrategy::INF) {
151 infrows.push_back(i);
152 continue;
153 }
154 if (s == SchedStrategy::SJF) {
155 if (k != 1.0)
156 throw UnsupportedError(
157 "solver_mva_sjn_analyzer: SJN scheduling at station " +
158 std::to_string(i + 1) +
159 " requires a single server, the response time equation is a single-server one");
160 rows.push_back(i);
161 sjnrows.push_back(rows.size() - 1);
162 continue;
163 }
164 if (s == SchedStrategy::PS || s == SchedStrategy::LCFSPR || s == SchedStrategy::FCFS ||
165 s == SchedStrategy::SIRO) {
166 if (k != 1.0)
167 throw UnsupportedError(
168 "solver_mva_sjn_analyzer: station " + std::to_string(i + 1) + " has " +
169 (std::isfinite(k) ? std::to_string(static_cast<long long>(k))
170 : std::string("infinitely many")) +
171 " servers, the SJN analyzer solves the remaining stations with the "
172 "single-server MVA equation");
173 rows.push_back(i);
174 continue;
175 }
176 throw UnsupportedError("solver_mva_sjn_analyzer: the SJN analyzer does not support " +
177 std::string(lang::sched_to_text(s)) +
178 " scheduling at the other stations");
179 }
180
181 const std::size_t Mq = rows.size();
182 Matrix<double> Ld(Mq, C, 0.0), Vd(Mq, C, 0.0), scvd(Mq, C, 1.0);
183 for (std::size_t j = 0; j < Mq; ++j) {
184 const std::size_t i = rows[j];
185 for (std::size_t c = 0; c < C; ++c) {
186 Ld(j, c) = num_traits<T>::to_double(T(d.STchain(i, c) * d.Vchain(i, c)));
187 Vd(j, c) = num_traits<T>::to_double(d.Vchain(i, c));
188 }
189 }
190 // only the SJN stations read an SCV: the size distribution is reconstructed
191 // from it, and the reference leaves the others at one
192 for (std::size_t j : sjnrows) {
193 const std::size_t i = rows[j];
194 for (std::size_t c = 0; c < C; ++c) {
195 const double v = num_traits<T>::to_double(d.SCVchain(i, c));
196 if (std::isfinite(v) && v > 0.0) scvd(j, c) = v;
197 }
198 }
199 std::vector<double> Zd(C, 0.0), Nd(C, 0.0);
200 for (std::size_t c = 0; c < C; ++c) {
201 for (std::size_t i : infrows)
202 Zd[c] += num_traits<T>::to_double(T(d.STchain(i, c) * d.Vchain(i, c)));
203 Nd[c] = d.Nchain[c];
204 }
205
206 pfqn::SjnOptions sjnopt;
207 sjnopt.tol = opt.iter_tol;
208 sjnopt.iter_max = static_cast<std::size_t>(opt.iter_max);
209 // SJN applies within a class and the classes are then non-preemptively
210 // prioritised; without distinct priorities the jobs of every class are
211 // compared by size directly (the pooled reading, options.prio empty).
212 // The reference indexes classprio by CLASS and hands it to a per-CHAIN
213 // argument, which is well defined only because the two counts agree here.
214 if (L.nchains == L.nclasses) {
215 bool distinct = true;
216 for (std::size_t i = 0; i < L.nclasses && distinct; ++i)
217 for (std::size_t j = i + 1; j < L.nclasses; ++j)
218 if (L.classes[i].prio == L.classes[j].prio) {
219 distinct = false;
220 break;
221 }
222 if (distinct) {
223 sjnopt.prio.assign(C, 0);
224 for (std::size_t c = 0; c < C; ++c) sjnopt.prio[c] = L.classes[c].prio;
225 }
226 }
227
228 double lattice = 1.0;
229 for (std::size_t c = 0; c < C; ++c) lattice *= Nd[c] + 1.0;
230 bool uselattice;
231 if (opt.method == "amva" || opt.method == "bs" || opt.method == "sjn.amva")
232 uselattice = false;
233 else if (opt.method == "exact" || opt.method == "mva" || opt.method == "sjn.mva")
234 uselattice = true;
235 else
236 uselattice = lattice <= kSjnLatticeMax;
237
238 pfqn::SjnResult sjn;
239 if (uselattice) {
240 try {
241 sjn = pfqn::pfqn_mvasjn(Ld, Nd, Zd, scvd, sjnrows, Vd, sjnopt);
242 } catch (const pfqn::SjnStarvationError&) {
243 if (opt.method != "default") throw;
244 sjn = pfqn::pfqn_amvasjn(Ld, Nd, Zd, scvd, sjnrows, Vd, sjnopt);
245 uselattice = false;
246 }
247 } else {
248 sjn = pfqn::pfqn_amvasjn(Ld, Nd, Zd, scvd, sjnrows, Vd, sjnopt);
249 }
250
251 Matrix<T> Qchain(M, C, num_traits<T>::from_int(0)), Uchain(M, C, num_traits<T>::from_int(0));
252 Matrix<T> Rchain(M, C, num_traits<T>::from_int(0)), Tchain(M, C, num_traits<T>::from_int(0));
253 std::vector<T> Xchain(C, num_traits<T>::from_int(0));
254 for (std::size_t c = 0; c < C; ++c) Xchain[c] = num_traits<T>::from_double(sjn.XN[c]);
255 for (std::size_t j = 0; j < Mq; ++j) {
256 const std::size_t i = rows[j];
257 for (std::size_t c = 0; c < C; ++c) {
258 Qchain(i, c) = num_traits<T>::from_double(sjn.QN(j, c));
259 Uchain(i, c) = num_traits<T>::from_double(sjn.UN(j, c));
260 }
261 }
262 for (std::size_t i = 0; i < M; ++i)
263 for (std::size_t c = 0; c < C; ++c) Tchain(i, c) = T(Xchain[c] * d.Vchain(i, c));
264 // a delay station holds T S jobs, all of them in service
265 for (std::size_t i : infrows)
266 for (std::size_t c = 0; c < C; ++c) {
267 Qchain(i, c) = T(Tchain(i, c) * d.STchain(i, c));
268 Uchain(i, c) = Qchain(i, c);
269 }
270 const T zero = num_traits<T>::from_int(0);
271 for (std::size_t i = 0; i < M; ++i)
272 for (std::size_t c = 0; c < C; ++c)
273 if (Tchain(i, c) != zero) Rchain(i, c) = T(Qchain(i, c) / Tchain(i, c));
274 // The reference also runs an isfinite sweep here. It has nothing to do in
275 // this port: every division above is guarded, so no non-finite value can be
276 // written in the first place. What does remain is the empty-chain columns.
277 for (std::size_t c = 0; c < C; ++c) {
278 if (d.Nchain[c] != 0.0) continue;
279 Xchain[c] = zero;
280 for (std::size_t i = 0; i < M; ++i) {
281 Qchain(i, c) = zero;
282 Uchain(i, c) = zero;
283 Rchain(i, c) = zero;
284 Tchain(i, c) = zero;
285 }
286 }
287
288 // Qchain and Uchain are DELIBERATELY not handed to the deaggregation: the
289 // reference passes [] in both slots (solver_mva_sjn_analyzer.m, last line)
290 // and lets it rebuild them from Rchain, Tchain and the per-class alpha. The
291 // Java port passes them and so disagrees; MATLAB is ground truth.
293 Tchain, Xchain);
295 out.sol.Q = cr.Q;
296 out.sol.U = cr.U;
297 out.sol.R = cr.R;
298 out.sol.Tp = cr.Tp;
299 out.sol.C = cr.C;
300 out.sol.X = cr.X;
301 out.sol.method = opt.method;
302 out.sol.iter = static_cast<int>(sjn.iter);
303 // the reference returns lG = NaN: an SJN solve carries no normalizing constant
304 out.sol.lG = std::numeric_limits<double>::quiet_NaN();
305 out.actualmethod = uselattice ? "sjn.mva" : "sjn.amva";
306 // Carry the reference's two line_warning texts rather than drop them.
307 if (sjn.capped)
308 out.warning =
309 "the utilization cap of 0.999 was binding at an SJN station: the station is in the "
310 "starvation regime, where long jobs are held back and the arrival theorem is badly "
311 "violated. The results are stable but their accuracy is not warranted, use SolverCTMC "
312 "or SolverLDES there";
313 if (!sjn.converged) {
314 if (!out.warning.empty()) out.warning += "; ";
315 out.warning += "the SJN fixed point did not converge in " + std::to_string(sjn.iter) +
316 " iterations";
317 }
318 return out;
319 } // if constexpr has_transcendental
320}
321
322} // namespace mva
323} // namespace line
324
325#endif // LINE_SOLVERS_MVA_SOLVER_MVA_SJN_H
UnsupportedError(const std::string &what)
Definition error.h:51
The conditional waiting time equation has no solution at some population.
Definition pfqn_sjn.h:115
A network plus its refreshed NetworkStruct.
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
Enumerations and the minimal distribution descriptor shared by the model layer of the C++ port.
The option and result types every MVA analyzer shares.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:230
bool sn_has_sjn(const qn::NetworkStruct< T > &L)
True when the layer has an SJF station, the reference's any(sn.sched == SchedStrategy....
SjnAnalyzerResult< T > solver_mva_sjn_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of solver_mva_sjn_analyzer.m.
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
constexpr double kSjnLatticeMax
Lattice size above which default prefers the fixed point.
ChainDemands< T > sn_get_demands_chain(const qn::NetworkStruct< T > &L)
Port of sn_get_demands_chain.
Definition sn_chain.h:63
SjnResult pfqn_amvasjn(const Matrix< double > &L, const std::vector< double > &N, const std::vector< double > &Z, const Matrix< double > &scv, const std::vector< std::size_t > &sjnset, const Matrix< double > &V, const SjnOptions &options)
Schweitzer fixed point counterpart of pfqn_mvasjn.
Definition pfqn_sjn.h:800
SjnResult pfqn_mvasjn(const Matrix< double > &L, const std::vector< double > &N, const std::vector< double > &Z, const Matrix< double > &scv, const std::vector< std::size_t > &sjnset, const Matrix< double > &V, const SjnOptions &options)
Exact-lattice MVA for closed networks with SJN stations, the unidirectional scheme of Kant 1992.
Definition pfqn_sjn.h:641
A queueing network and its refreshed NetworkStruct.
Closed networks with non-preemptive shortest-job-next (SJN/SJF) stations.
Chain aggregation and de-aggregation.
The chain-level view of a layer, as sn_get_demands_chain returns it.
Definition sn_chain.h:46
std::vector< double > Nchain
(C) population, infinite for an open chain
Definition sn_chain.h:51
Matrix< T > STchain
(M x C) mean service time
Definition sn_chain.h:48
Matrix< T > Vchain
(M x C) visits
Definition sn_chain.h:49
Matrix< T > SCVchain
(M x C)
Definition sn_chain.h:52
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 options SolverMVA reads.
Definition mva_types.h:31
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:96
What the analyzer returns, the reference's metrics plus its actualmethod.
std::string warning
Non-empty when the reference would have called line_warning AND RETURNED, carrying its text.
Options of the SJN solvers, the fields sjn_args fills in.
Definition pfqn_sjn.h:80
std::vector< int > prio
distinct levels, 1 = highest; empty for the pooled reading
Definition pfqn_sjn.h:83
std::size_t iter_max
Definition pfqn_sjn.h:85
Return block of pfqn_mvasjn and pfqn_amvasjn.
Definition pfqn_sjn.h:98
Matrix< double > QN
Definition pfqn_sjn.h:100
Matrix< double > UN
Definition pfqn_sjn.h:100
std::vector< double > XN
(R)
Definition pfqn_sjn.h:99
bool capped
the utilization cap was binding somewhere
Definition pfqn_sjn.h:104
bool converged
always true for the lattice recursion
Definition pfqn_sjn.h:103