LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
mva_types.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_MVA_TYPES_H
6#define LINE_SOLVERS_MVA_MVA_TYPES_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The option and result types every MVA analyzer shares.
12 *
13 * They live in their own header so that an analyzer in a separate translation
14 * unit (solver_qna.h, and any analyzer that follows it) can speak the same
15 * contract without including solver_mva.h and forming a cycle.
16 */
17
18#include <cmath>
19#include <optional>
20#include <string>
21#include <vector>
22
25#include "line/util/matrix.h"
26
27namespace line {
28namespace mva {
29
30/** The options SolverMVA reads. Defaults are SolverOptions('MVA'). */
31struct MvaOptions {
32 std::string method = "default";
33 double tol = 1e-4;
34 double iter_tol = 1e-6;
35 int iter_max = 1000;
36 std::string multiserver = "default";
37 std::string highvar = "default";
38 /**
39 * `options.config.np_priority`: which non-preemptive priority correction a
40 * HOL station takes. `default` and `cl` are Chandy-Lakshmi, which discounts
41 * the higher-priority utilization by the Linearizer's throughput
42 * difference; `shadow` is Sevcik's shadow server, which does not.
43 */
44 std::string np_priority = "default";
45 /**
46 * `options.config.fork_join`: which fork-join arm the fixed point takes.
47 * `default`, `mmt` and `fjt` are the MMT transform of fj_mmt.h;
48 * `ht` and `heidelberger-trivedi` are the Heidelberger-Trivedi transform of
49 * fj_ht.h, which is closed-model only. Read only by a model with a Fork.
50 */
51 std::string fork_join = "default";
52 /** Warm start, (nstations x nchains) chain-aggregated queue lengths; empty for none. */
53 bool has_init_sol = false;
54 /**
55 * Whether the model this solve came from has a Fork, which the model handed
56 * to the analyzer no longer does.
57 *
58 * `mvaDispatch.m` tests `self.model.hasFork` -- the BASE model -- to force
59 * `default` onto AMVA, and by the time the analyzer runs the fork-join
60 * transform has already replaced the fork with a router, so the struct in
61 * hand cannot answer the question. Testing the transformed struct instead
62 * silently sends the model to exact mixed MVA, which the auxiliary
63 * near-zero-rate open classes degenerate.
64 */
65 bool base_has_fork = false;
66 /**
67 * `options.config.rqt_regime`: which service adaptation regime of the
68 * Robust Queueing Theory Table 1 the RQT analyzer takes. Empty and
69 * `independent` leave the service distribution unknown; `normal` and
70 * `pareto` are the service-dependent fits.
71 */
72 std::string rqt_regime;
73 /** `options.config.rqt_exact`: use the exact worst case over the uncertainty
74 * sets instead of the closed-form bound of Theorem 3. */
75 bool rqt_exact = false;
76 /** `options.config.rqt_alpha_a`: arrival tail coefficient in (1,2]; 0 = 2. */
77 double rqt_alpha_a = 0.0;
78 /** `options.config.rqt_alpha_s`: service tail coefficient in (1,2]; 0 = 2. */
79 double rqt_alpha_s = 0.0;
80 /**
81 * `options.config.interlock`: the interlocked-flow matrix of Franks (1999),
82 * Eq. (4.7), CLASS-indexed and row-major (nclasses x nclasses). interlock[r][s]
83 * is the share of the class-s queue that a class-r arrival must not see, because
84 * that work was itself caused by the class-r request. Empty for every model but
85 * the layers of SolverLN. It is kept CLASS-indexed, not chain-indexed, so that a
86 * later chain refresh cannot leave it stale: the analyzer aggregates it to chains
87 * against the struct it is about to solve. Doubles rather than T because the
88 * probabilities come from a fixed-point iterate and never enter an exact
89 * computation of their own.
90 */
91 std::vector<std::vector<double>> interlock;
92};
93
94/** Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers. */
95template <class T>
98 std::vector<T> C, X;
99 std::string method;
100 int iter = 0;
101 /**
102 * Whether the fixed point met its tolerance, or empty when the handler
103 * reports none. NOT `iter < iter_max`: `solver_amva` increments `totiter`
104 * inside the nested per-chain and inner loops, so it aggregates inner sweeps
105 * and can reach the cap on a solve whose outer residual is exactly zero.
106 * Only the residual decides there, which is why the flag has to travel with
107 * the solution instead of being recomputed by a caller from the count.
108 */
109 std::optional<bool> converged;
110 /**
111 * log of the normalizing constant, the reference's `lG`.
112 *
113 * Only the exact MVA recursion carries one: an AMVA result has no G, and
114 * the reference returns 0 there rather than a fabricated value, which is
115 * why `getProbNormConstAggr` re-enters the analyzer at method='exact'
116 * instead of reusing whatever the last solve produced.
117 */
118 double lG = 0.0;
119};
120
121/**
122 * Per-method structural gates, shared by `list_valid_methods` and by the
123 * analyzers themselves.
124 *
125 * ONE predicate with TWO callers. A rule kept in two places is how the report
126 * comes to offer a (solver, method) pair that the analyzer then refuses -- or,
127 * worse, answers with a table of zeros. What the feature registry CAN name lives
128 * in `qn::mva_feature_set` instead; these are the rules it cannot: a product
129 * form, a class count and a server count have no registry name.
130 *
131 * Each returns the refusal sentence, or an empty string when the method may run.
132 */
133
134/**
135 * The closed-population AMVA family: an open chain, or a network outside strict
136 * product form, has nothing for the recursion to work on.
137 *
138 * Open chains are also expressed in the registry (`mva_feature_set` drops
139 * OpenClass for these), which is what keeps them off the report; they are
140 * repeated here because the analyzer must refuse by name with a sentence rather
141 * than fall through to `solver_amvald` and answer under a method nobody asked
142 * for. Strict product form has no registry name at all, so this is its only home.
143 */
144template <class T>
146 const std::string& method) {
147 if (!qn::mva_is_closed_population_method(method)) return "";
148 const std::string base = qn::mva_base_method(method);
149 if (L.has_open_classes())
150 return "solver_amva: the '" + base +
151 "' method approximates the arrival-instant queue length as a function of the "
152 "closed population vector N, so it is defined for closed models only; use "
153 "'default', 'lin', 'qd' or 'qna' for a model with open classes";
154 // ab, schmidt and schmidt-ext ARE the class-dependent FCFS algorithms, so
155 // heterogeneous FCFS service means are their subject matter rather than a
156 // disqualification.
157 const bool check_means = !(base == "ab" || base == "schmidt" || base == "schmidt-ext");
158 if (L.has_product_form_not_het_fcfs(check_means)) return "";
159 return "solver_amva: the '" + base +
160 "' method is defined for strict product-form, load-independent models; use 'default', "
161 "'lin' or 'qd' for this model";
162}
163
164/**
165 * RQNA and RQT decompose an open network into GI/G/1 queues and build one
166 * uncertainty set per flow out of the first two moments of a SINGLE stream, so a
167 * multiclass model has no counterpart in their equations. No registry feature
168 * names a class count, so that half of the rule is structural.
169 *
170 * A FORK-JOIN model is refused too. A Join is a synchronisation node, not a
171 * queue: it carries no service process, so the index-of-dispersion curve these
172 * analyzers read off every station does not exist for it, and neither has a
173 * synchronisation term to put in its place. That half IS nameable, so
174 * `qn::mva_feature_set` drops Fork/Join for these two as well and this is the
175 * analyzer's half of it.
176 */
177template <class T>
179 const std::string& method) {
180 const std::string base = qn::mva_base_method(method);
181 if (base != "rqna" && base != "rqt") return "";
182 const std::string label = (base == "rqna") ? "RQNA" : "RQT";
183 // Scanned over the NODES and not the stations: a Fork is not a station in
184 // this port, so a station scan would see only the Join.
185 for (const qn::NodeDef& nd : L.nodes)
186 if (nd.nodetype == qn::NodeType::Fork || nd.nodetype == qn::NodeType::Join)
187 return "solver_" + base + ": " + label +
188 " decomposes an open network into GI/G/1 queues and has no synchronisation "
189 "term; a Join carries no service process for its index of dispersion to be "
190 "read from; use the 'default' method for a fork-join model";
191 if (L.nclasses == 1) return "";
192 return "solver_" + base + ": " + label +
193 " supports single-class open networks only; use method 'qna' for multiclass models";
194}
195
196/**
197 * The extended Schmidt method needs a customer of every class to tag.
198 *
199 * Schmidt's EXTENSION over plain Schmidt is an alpha correction applied at an
200 * FCFS station, computed from the network with ONE class-r customer TAGGED, that
201 * is at population N - 1_r. A class holding no customer has none to tag: the
202 * sub-problem is formed at a negative population, whose state lattice prod(N+1)
203 * collapses to zero and the recursion indexes an empty array. Plain `schmidt`
204 * forms no such sub-problem, which is why the requirement is the -ext arm's
205 * alone.
206 *
207 * THE TEST IS STATED AT THE FCFS STATION AND NOT AT A CLASS-DEPENDENT ONE,
208 * because the four kernels differ on when they form the correction: MATLAB, this
209 * port and native python form it only where the station's demands differ by
210 * class, the JAR forms it at every FCFS station. Stating the union is what keeps
211 * one rule safe for all four; the case it costs -- an FCFS station whose demands
212 * are identical across classes, one of them empty -- is one where the extension
213 * reduces to plain `schmidt`, which stays offered.
214 *
215 * `njobs` and `fcfs` are the numbers the CALLER'S OWN arm passes: CHAIN-indexed
216 * here as in MATLAB, CLASS-indexed in the JAR and native python, whose arms
217 * aggregate no chains. That difference belongs to those arms, not to this rule.
218 */
219inline std::string mva_schmidt_ext_reason(const std::vector<double>& njobs,
220 const std::vector<bool>& fcfs,
221 const std::string& method) {
222 if (qn::mva_base_method(method) != "schmidt-ext") return "";
223 bool any_fcfs = false;
224 for (std::size_t i = 0; i < fcfs.size(); ++i)
225 if (fcfs[i]) any_fcfs = true;
226 if (!any_fcfs) return "";
227 for (std::size_t r = 0; r < njobs.size(); ++r)
228 if (std::isfinite(njobs[r]) && njobs[r] < 1.0)
229 return "solver_amva: the 'schmidt-ext' method corrects an FCFS station from the "
230 "network with one customer of that class tagged, so it needs every class to "
231 "hold at least one customer; class " +
232 std::to_string(r + 1) +
233 " holds none; use 'schmidt' for the uncorrected recursion";
234 return "";
235}
236
237/**
238 * MVAC is the exact chain recursion over single-server fixed-rate (SSFR) queues
239 * and infinite-server centres of a product-form network, and it recurs on the
240 * queueing centres, so it needs at least one. Neither the server count nor
241 * product form has a registry feature name; the scheduling restriction IS
242 * nameable and lives in `qn::mva_feature_set`.
243 */
244template <class T>
245std::string mva_mvac_reason(const qn::NetworkStruct<T>& L, const std::string& method) {
246 if (qn::mva_base_method(method) != "mvac") return "";
247 if (!L.has_product_form()) return "solver_mvac_analyzer: MVAC requires a product-form model";
248 std::size_t nq = 0;
249 for (const qn::Station<T>& st : L.stations) {
250 if (st.sched == qn::SchedStrategy::INF || st.sched == qn::SchedStrategy::EXT) continue;
251 ++nq;
252 // An infinite count is refused here too, as the reference does: a station
253 // scheduled FCFS with infinitely many servers is not an IS centre to MVAC,
254 // and `infSET` below is built from the DISCIPLINE, not the count.
255 if (st.nservers != 1.0)
256 return "solver_mvac_analyzer: MVAC supports single-server (SSFR) queues only; use "
257 "method 'exact' for multiserver stations";
258 }
259 if (nq == 0)
260 return "solver_mvac_analyzer: MVAC recurs on the queueing centers and needs at least one; "
261 "this model has only delay stations";
262 return "";
263}
264
265/**
266 * QNA's station update has an arm for INF, PS and FCFS and none for any other
267 * discipline, so a SIRO, LCFS, LCFS-PR, HOL or priority station used to leave
268 * its whole row of Q, U, R and T at zero and the table was returned as a
269 * solution. The registry expresses this too (`mva_feature_set` drops the
270 * disciplines from QNA's envelope); this is the analyzer's half of it.
271 */
272template <class T>
274 for (std::size_t i = 0; i < L.stations.size(); ++i) {
275 // A Join station carries no service and is skipped by the update below.
276 if (L.stations[i].nodetype == qn::NodeType::Join) continue;
277 const qn::SchedStrategy sched = L.stations[i].sched;
278 if (sched == qn::SchedStrategy::EXT || sched == qn::SchedStrategy::INF ||
279 sched == qn::SchedStrategy::PS || sched == qn::SchedStrategy::FCFS)
280 continue;
281 return std::string("solver_qna: QNA decomposes every station as a GI/G/m centre and has "
282 "no arm for ") +
283 lang::sched_to_text(sched) + " scheduling; use the 'default' or 'lin' methods";
284 }
285 return "";
286}
287
288} // namespace mva
289} // namespace line
290
291#endif // LINE_SOLVERS_MVA_MVA_TYPES_H
A network plus its refreshed NetworkStruct.
bool has_product_form_not_het_fcfs(bool check_means=true) const
Port of sn_has_product_form_not_het_fcfs: LCFS is excluded, and at FCFS the service must be exponenti...
std::vector< Station< T > > stations
stations[k-1] is the k-th station
std::vector< NodeDef > nodes
every node, in creation order
Dense matrix and non-owning view.
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
std::string mva_single_class_open_reason(const qn::NetworkStruct< T > &L, const std::string &method)
RQNA and RQT decompose an open network into GI/G/1 queues and build one uncertainty set per flow out ...
Definition mva_types.h:178
std::string mva_schmidt_ext_reason(const std::vector< double > &njobs, const std::vector< bool > &fcfs, const std::string &method)
The extended Schmidt method needs a customer of every class to tag.
Definition mva_types.h:219
std::string mva_mvac_reason(const qn::NetworkStruct< T > &L, const std::string &method)
MVAC is the exact chain recursion over single-server fixed-rate (SSFR) queues and infinite-server cen...
Definition mva_types.h:245
std::string mva_qna_scheduling_reason(const qn::NetworkStruct< T > &L)
QNA's station update has an arm for INF, PS and FCFS and none for any other discipline,...
Definition mva_types.h:273
std::string mva_closed_population_reason(const qn::NetworkStruct< T > &L, const std::string &method)
Per-method structural gates, shared by list_valid_methods and by the analyzers themselves.
Definition mva_types.h:145
std::string mva_base_method(const std::string &method)
SolverMVA.getFeatureSet, plus getMethodFeatureSet's per-method deltas.
bool mva_is_closed_population_method(const std::string &method)
The AMVA algorithms whose recursion is over a CLOSED population vector.
A queueing network and its refreshed NetworkStruct.
The DECLARED side of the gate: one feature set per solver.
The options SolverMVA reads.
Definition mva_types.h:31
std::string np_priority
options.config.np_priority: which non-preemptive priority correction a HOL station takes.
Definition mva_types.h:44
std::string highvar
Definition mva_types.h:37
std::string multiserver
Definition mva_types.h:36
bool has_init_sol
Warm start, (nstations x nchains) chain-aggregated queue lengths; empty for none.
Definition mva_types.h:53
bool base_has_fork
Whether the model this solve came from has a Fork, which the model handed to the analyzer no longer d...
Definition mva_types.h:65
double rqt_alpha_a
options.config.rqt_alpha_a: arrival tail coefficient in (1,2]; 0 = 2.
Definition mva_types.h:77
std::string rqt_regime
options.config.rqt_regime: which service adaptation regime of the Robust Queueing Theory Table 1 the ...
Definition mva_types.h:72
std::string method
Definition mva_types.h:32
bool rqt_exact
options.config.rqt_exact: use the exact worst case over the uncertainty sets instead of the closed-fo...
Definition mva_types.h:75
double rqt_alpha_s
options.config.rqt_alpha_s: service tail coefficient in (1,2]; 0 = 2.
Definition mva_types.h:79
std::vector< std::vector< double > > interlock
options.config.interlock: the interlocked-flow matrix of Franks (1999), Eq.
Definition mva_types.h:91
std::string fork_join
options.config.fork_join: which fork-join arm the fixed point takes.
Definition mva_types.h:51
Class-level results, the [Q,U,R,T,C,X] of the MATLAB analyzers.
Definition mva_types.h:96
std::vector< T > X
Definition mva_types.h:98
double lG
log of the normalizing constant, the reference's lG.
Definition mva_types.h:118
std::vector< T > C
Definition mva_types.h:98
std::optional< bool > converged
Whether the fixed point met its tolerance, or empty when the handler reports none.
Definition mva_types.h:109
A node of the network.
One station of the network.
SchedStrategy sched
double nservers
may be infinite (a Delay, or an inf-scheduled task)