LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mva_oi.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_OI_H
6#define LINE_SOLVERS_MVA_SOLVER_MVA_OI_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Exact mean-value analysis for networks with order-independent stations.
12 *
13 * Templated port of `matlab/src/solvers/MVA/solver_mva_oi_analyzer.m`.
14 *
15 * An ORDER-INDEPENDENT (OI) station is a class-dependent load-dependent server
16 * whose total service rate mu(n) is a permutation-invariant function of the
17 * per-class count vector. A closed network of delay stations, load-independent
18 * single-server queues and ANY number of OI stations is product-form, and
19 * `pfqn_mvaoi` -- the Conditional MVA that carries one rate-shift vector per OI
20 * station -- solves it exactly, with no normalizing constant and no joint
21 * marginal.
22 *
23 * References: Reiser and Lavenberg, JACM 27(2), 1980; the load-dependent
24 * extension of Bruell, Balbo and Afshari, 1984; OI stations and CMVA in
25 * Casale 2009 and Casale, Comte and Dorsman 2026.
26 *
27 * A MULTISERVER BCMP QUEUE IS PROMOTED TO AN OI STATION rather than
28 * approximated. `pfqn_mvaoi` models a load-independent queue as a SINGLE
29 * server, so a c-server station cannot take that path; but its BCMP weight
30 * satisfies the OI balance recursion exactly for the permutation-invariant rate
31 *
32 * mu(n) = (min(|n|, c) / |n|) * sum_{r : n_r > 0} n_r / D_r,
33 *
34 * so it goes down the OI path instead and the answer stays exact. Reporting
35 * still classifies it as a queue, so its utilization keeps the per-server
36 * offered-load convention.
37 *
38 * DETECTION mirrors `nc_is_oi_model`: PAS or OI scheduling, a service-rate
39 * function, and an ALL-ZERO swap graph. A nonzero swap graph is a genuine
40 * pass-and-swap station, which is not order-independent and is not product
41 * form, so it is excluded from the list -- and if that leaves no OI station at
42 * all, the analyzer refuses rather than solving a different model.
43 *
44 * Arithmetic: whatever `pfqn_mvaoi` accepts; the analyzer itself is field
45 * arithmetic.
46 */
47
48#include <cmath>
49#include <functional>
50#include <vector>
51
55
56namespace line {
57namespace mva {
58
59/**
60 * The OI stations of a model, 1-based station indices.
61 *
62 * Port of `find_oi_stations`. Empty when the model has none, which is what the
63 * dispatch tests before entering this analyzer.
64 */
65template <class T>
66std::vector<std::size_t> find_oi_stations(const qn::NetworkStruct<T>& L) {
67 std::vector<std::size_t> oi;
68 for (std::size_t i = 0; i < L.nstations; ++i) {
69 const qn::Station<T>& st = L.stations[i];
70 if (st.sched != qn::SchedStrategy::PAS && st.sched != qn::SchedStrategy::OI) continue;
71 if (!st.svc_rate_fun) continue;
72 // The graph is read through `station_swap_graph`, which applies the
73 // defaults `refreshLocalVars.m` installs: an OI station is always the
74 // zero graph, and a PAS station with no explicit graph is the complete
75 // compatibility graph, hence a genuine pass-and-swap station and NOT
76 // order-independent. MATLAB never stores an empty graph, so the
77 // `isempty(sg)` arm of `nc_is_oi_model` is unreachable there.
78 if (!qn::station_swap_graph_is_zero(L, i + 1)) continue;
79 oi.push_back(i + 1);
80 }
81 return oi;
82}
83
84/**
85 * Port of `nc_is_oi_model`: whether the model as a WHOLE is order-independent,
86 * which is a strictly stronger condition than having an OI station.
87 *
88 * The whole network must be product-form for the CMVA to be exact, so every
89 * station has to be a delay, an OI station with an all-zero swap graph, a PS or
90 * LCFS-PR queue, or an FCFS / SIRO queue whose service rate does NOT depend on
91 * the class. A closed population is required as well: the recursion is over a
92 * finite lattice.
93 */
94template <class T>
97 for (const auto& c : L.classes)
98 if (std::isinf(c.population)) return false;
99 bool hasOI = false;
100 for (std::size_t i = 0; i < L.nstations; ++i) {
101 const qn::Station<T>& st = L.stations[i];
102 if (st.sched == SchedStrategy::INF) continue;
103 if (st.sched == SchedStrategy::PAS || st.sched == SchedStrategy::OI) {
104 if (!st.svc_rate_fun) return false;
105 // see the note in find_oi_stations on the swap-graph defaults
106 if (!qn::station_swap_graph_is_zero(L, i + 1))
107 return false; // genuine pass-and-swap: not order-independent
108 hasOI = true;
109 } else if (st.sched == SchedStrategy::PS || st.sched == SchedStrategy::LCFSPR ||
110 st.sched == SchedStrategy::SIRO || st.sched == SchedStrategy::FCFS) {
111 if (st.sched != SchedStrategy::FCFS && st.sched != SchedStrategy::SIRO) continue;
112 // a class-dependent FCFS or SIRO rate breaks product form
113 double lo = 0.0, hi = 0.0;
114 bool any = false;
115 for (std::size_t r = 0; r < L.nclasses; ++r) {
116 if (!(L.classes[r].population > 0.0)) continue;
117 const double v = num_traits<T>::to_double(L.rates(i, r));
118 if (!std::isfinite(v)) continue;
119 if (!any) {
120 lo = hi = v;
121 any = true;
122 } else {
123 lo = std::min(lo, v);
124 hi = std::max(hi, v);
125 }
126 }
127 if (any && (hi - lo) > 1e-9 * hi) return false;
128 } else {
129 return false; // an unsupported (non-product-form) station
130 }
131 }
132 return hasOI;
133}
134
135namespace detail {
136
137/**
138 * The OI rate at a count vector, evaluated through the station's microstate
139 * function: `repelem(1:R, n)` is the canonical 1-based ordered state with n_r
140 * jobs of class r, and permutation invariance makes any other order give the
141 * same rate.
142 */
143template <class T>
144T oi_rate(const std::function<T(const std::vector<std::size_t>&)>& f, const std::vector<int>& n,
145 std::size_t R) {
146 int tot = 0;
147 for (int v : n) tot += v;
148 if (tot == 0) return num_traits<T>::from_int(0);
149 std::vector<std::size_t> micro;
150 micro.reserve(static_cast<std::size_t>(tot));
151 for (std::size_t r = 0; r < R && r < n.size(); ++r)
152 for (int a = 0; a < n[r]; ++a) micro.push_back(r + 1);
153 return f(micro);
154}
155
156/**
157 * The OI rate that reproduces a c-server BCMP queue with per-class demands Dq:
158 * `mu(n) = (min(|n|,c)/|n|) sum_{r : n_r>0} n_r/Dq_r`. At c = 1 this is the
159 * ordinary total completion rate of a multiclass single server, and for one
160 * class it reduces to min(n,c)/Dq, the M/M/c rate.
161 */
162template <class T>
163T ms_oi_rate(const std::vector<int>& n, const std::vector<T>& Dq, double c) {
164 const T zero = num_traits<T>::from_int(0);
165 int tot = 0;
166 for (int v : n) tot += v;
167 if (tot == 0) return zero;
168 T acc = zero;
169 for (std::size_t r = 0; r < n.size() && r < Dq.size(); ++r)
170 if (n[r] > 0 && Dq[r] > zero) acc += T(num_traits<T>::from_int(n[r]) / Dq[r]);
171 const double cc = (std::isfinite(c) && c > 0.0) ? c : 1.0;
172 const double m = std::min<double>(static_cast<double>(tot), cc);
173 return T(num_traits<T>::from_double(m / static_cast<double>(tot)) * acc);
174}
175
176} // namespace detail
177
178/** Port of `solver_mva_oi_analyzer.m`. */
179template <class T>
181 using qn::SchedStrategy;
182 (void)opt;
183 const T zero = num_traits<T>::from_int(0);
184 const std::size_t M = L.nstations, R = L.nclasses;
185
186 const std::vector<std::size_t> oi_list = find_oi_stations(L);
187 if (oi_list.empty())
188 throw UnsupportedError(
189 "solver_mva_oi_analyzer: the OI solver requires at least one order-independent "
190 "station (PAS or OI scheduling, a service-rate function, and an all-zero swap graph)");
191
192 // The OI rank rates are indexed by RAW class, so a chain that merges several
193 // classes has no rate to evaluate: the recursion is driven by the per-class
194 // population, which class switching makes meaningless (a class appearing only
195 // mid-chain carries population 0, so the OI station reads as empty).
196 for (std::size_t c = 0; c < L.nchains; ++c)
197 if (L.inchain[c].size() > 1)
198 throw UnsupportedError(
199 "solver_mva_oi: requires one class per chain (no class switching)");
200
201 std::vector<int> N(R, 0);
202 for (std::size_t r = 0; r < R; ++r) {
203 const double nr = L.classes[r].population;
204 if (!std::isfinite(nr))
205 throw UnsupportedError(
206 "solver_mva_oi_analyzer: the CMVA recursion is over a closed lattice; this model "
207 "has an open class");
208 N[r] = static_cast<int>(std::llround(nr));
209 }
210
211 std::vector<bool> isOI(M, false), isDelay(M, false);
212 for (std::size_t i : oi_list) isOI[i - 1] = true;
213 for (std::size_t i = 0; i < M; ++i) isDelay[i] = (L.stations[i].sched == SchedStrategy::INF);
214
215 // per-class demand D = V / rate at every station
216 Matrix<T> V(M, R, zero), D(M, R, zero);
217 for (std::size_t c = 0; c < L.nchains; ++c)
218 for (std::size_t i = 0; i < M; ++i)
219 for (std::size_t r = 0; r < R; ++r)
220 V(i, r) = T(V(i, r) + L.visits[c](L.stateful_of_station(i + 1) - 1, r));
221 for (std::size_t i = 0; i < M; ++i)
222 for (std::size_t r = 0; r < R; ++r) {
223 const T mu = L.rates(i, r);
224 if (std::isfinite(num_traits<T>::to_double(mu)) && mu > zero)
225 D(i, r) = T(V(i, r) / mu);
226 }
227
228 // aggregate the delays into Z; split the rest into single-server queues and
229 // multiserver queues, the latter promoted to OI stations
230 std::vector<T> Z(R, zero);
231 std::vector<std::size_t> li_list, ms_list;
232 for (std::size_t i = 0; i < M; ++i) {
233 if (isOI[i]) continue;
234 if (isDelay[i]) {
235 for (std::size_t r = 0; r < R; ++r) Z[r] += D(i, r);
236 } else if (std::isfinite(L.stations[i].nservers) && L.stations[i].nservers > 1.0) {
237 ms_list.push_back(i + 1);
238 } else {
239 li_list.push_back(i + 1);
240 }
241 }
242 Matrix<T> Dli(li_list.size(), R, zero);
243 for (std::size_t a = 0; a < li_list.size(); ++a)
244 for (std::size_t r = 0; r < R; ++r) Dli(a, r) = D(li_list[a] - 1, r);
245
246 std::vector<std::function<T(const std::vector<int>&)>> mu;
247 mu.reserve(oi_list.size() + ms_list.size());
248 for (std::size_t o = 0; o < oi_list.size(); ++o) {
249 const std::function<T(const std::vector<std::size_t>&)> f =
250 L.stations[oi_list[o] - 1].svc_rate_fun;
251 mu.push_back([f, R](const std::vector<int>& n) { return detail::oi_rate<T>(f, n, R); });
252 }
253 for (std::size_t j = 0; j < ms_list.size(); ++j) {
254 std::vector<T> Dq(R, zero);
255 for (std::size_t r = 0; r < R; ++r) Dq[r] = D(ms_list[j] - 1, r);
256 const double c = L.stations[ms_list[j] - 1].nservers;
257 mu.push_back([Dq, c](const std::vector<int>& n) { return detail::ms_oi_rate<T>(n, Dq, c); });
258 }
259
260 const pfqn::MvaoiResult<T> res = pfqn::pfqn_mvaoi(Z, N, mu, Dli, /*want_soi=*/true);
261
262 Matrix<T> QN(M, R, zero), TN(M, R, zero), RN(M, R, zero), UN(M, R, zero);
263 for (std::size_t o = 0; o < oi_list.size(); ++o)
264 for (std::size_t r = 0; r < R; ++r) QN(oi_list[o] - 1, r) = res.Qoi(o, r);
265 for (std::size_t j = 0; j < ms_list.size(); ++j)
266 for (std::size_t r = 0; r < R; ++r)
267 QN(ms_list[j] - 1, r) = res.Qoi(oi_list.size() + j, r);
268 for (std::size_t a = 0; a < li_list.size(); ++a)
269 for (std::size_t r = 0; r < R; ++r) QN(li_list[a] - 1, r) = res.Qli(a, r);
270 // a delay station takes its exact product-form share, X * D
271 for (std::size_t i = 0; i < M; ++i)
272 if (isDelay[i])
273 for (std::size_t r = 0; r < R; ++r) QN(i, r) = T(res.X[r] * D(i, r));
274
275 // which row of Soi / Qoi holds each OI station
276 std::vector<std::size_t> oiRow(M, 0);
277 for (std::size_t o = 0; o < oi_list.size(); ++o) oiRow[oi_list[o] - 1] = o + 1;
278
279 for (std::size_t i = 0; i < M; ++i)
280 for (std::size_t r = 0; r < R; ++r) {
281 TN(i, r) = T(res.X[r] * V(i, r));
282 if (res.X[r] > zero) RN(i, r) = T(QN(i, r) / res.X[r]);
283 const double sv_raw = L.stations[i].nservers;
284 const double sv = (std::isfinite(sv_raw) && sv_raw > 0.0) ? sv_raw : 1.0;
285 if (isOI[i]) {
286 // IN-SERVICE utilization, E[sir_r]/nservers, the convention
287 // solver_nc_oi_analyzer and the exact CTMC/LDES also use: sir_r
288 // counts the class-r jobs receiving a strictly positive rank
289 // rate, which Soi holds.
290 UN(i, r) = T(res.Soi(oiRow[i] - 1, r) / num_traits<T>::from_double(sv));
291 } else if (isDelay[i]) {
292 UN(i, r) = QN(i, r);
293 } else {
294 UN(i, r) = T(res.X[r] * D(i, r) / num_traits<T>::from_double(sv));
295 }
296 }
297
298 MvaSolution<T> out;
299 out.Q = QN;
300 out.U = UN;
301 out.R = RN;
302 out.Tp = TN;
303 out.X = res.X;
304 // the reference reports CN as the per-station response time, not as a
305 // per-class system time; it returns the RN matrix itself
306 out.C.assign(R, zero);
307 for (std::size_t r = 0; r < R; ++r)
308 for (std::size_t i = 0; i < M; ++i) out.C[r] += RN(i, r);
309 out.method = "oi";
310 int iter = 0;
311 for (int v : N) iter += v;
312 out.iter = iter;
313 out.lG = 0.0;
314 return out;
315}
316
317} // namespace mva
318} // namespace line
319
320#endif // LINE_SOLVERS_MVA_SOLVER_MVA_OI_H
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::size_t stateful_of_station(std::size_t st) const
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
Matrix< T > rates
(nstations x nclasses) service rates and SCVs, with a PARALLEL disabled flag instead of MATLAB's NaN ...
std::vector< std::vector< std::size_t > > inchain
1-based class indices per chain
std::vector< Matrix< T > > visits
(nchains) each (nstateful x nclasses)
The option and result types every MVA analyzer shares.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
std::vector< std::size_t > find_oi_stations(const qn::NetworkStruct< T > &L)
The OI stations of a model, 1-based station indices.
MvaSolution< T > solver_mva_oi_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of solver_mva_oi_analyzer.m.
bool nc_is_oi_model(const qn::NetworkStruct< T > &L)
Port of nc_is_oi_model: whether the model as a WHOLE is order-independent, which is a strictly strong...
MvaoiResult< T > pfqn_mvaoi(const std::vector< T > &Z, const std::vector< int > &N, const std::vector< std::function< T(const std::vector< int > &)> > &mu, const Matrix< T > &Dli, const Matrix< T > &visits, bool want_soi)
Mean-value analysis of a closed network with order-independent (OI) stations, the composition-depende...
Definition pfqn_mvaoi.h:122
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.
A queueing network and its refreshed NetworkStruct.
Mean-value analysis of a closed network with order-independent (OI) stations, the composition-depende...
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
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
Return value of pfqn_mvaoi, mirroring [X, Qoi, Qli, Qdelay, Soi].
Definition pfqn_mvaoi.h:82
Matrix< T > Qoi
(K x R) per-class queue length at each OI station
Definition pfqn_mvaoi.h:84
Matrix< T > Qli
(J x R) per-class queue length at each LI queue
Definition pfqn_mvaoi.h:85
std::vector< T > X
(R) per-class throughput
Definition pfqn_mvaoi.h:83
Matrix< T > Soi
(K x R) per-class mean in-service jobs, if requested
Definition pfqn_mvaoi.h:87
One station of the network.
std::function< T(const std::vector< std::size_t > &)> svc_rate_fun
sn.nodeparam{ind}.svcRateFun for a PAS / OI station: the TOTAL service rate as a function of the orde...
SchedStrategy sched