LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
solver_mva_prob.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_PROB_H
6#define LINE_SOLVERS_MVA_SOLVER_MVA_PROB_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The state-probability half of the SolverMVA class surface.
12 *
13 * MVA computes means, not distributions, so every member of this family is an
14 * approximation FITTED to the means the analyzer returned, and the reference
15 * says which one:
16 *
17 * closed classes a binomial with mean Q(i,r) over N(r) trials, from
18 * R. Schmidt, "An approximate MVA algorithm for exponential,
19 * class-dependent multiple servers", PEVA 29:245-254, 1997
20 * open classes the exact BCMP product form of the station: independent
21 * Poisson at an infinite server, multinomial-geometric at a
22 * queue
23 *
24 * Ported here: `getProbMarg` (a single class's queue-length distribution),
25 * `getProbNormConstAggr` (log G, by re-entering the analyzer at method='exact'),
26 * and `getProbAggr` / `getProbSysAggr` (the per-class joint at one station, and
27 * the whole-system joint). The last two read the per-class occupancy `nir` of
28 * the model's state through `State.toMarginal`; this port carries the DEFAULT
29 * initial state -- every closed class's population sits at its reference station,
30 * open classes hold no jobs -- for which `toMarginal` reduces to that same
31 * reference-station allocation, so `nir` is computed directly. A custom initial
32 * state (there is no `setState` in the builder) would need the full state
33 * encoding; when one exists it must be refused rather than silently reported
34 * against the default.
35 *
36 * Everything is computed in logs and exponentiated once, as the reference does,
37 * so a large population does not underflow before it is normalized.
38 */
39
40#include <cmath>
41#include <vector>
42
48
49namespace line {
50namespace mva {
51
52/** A marginal distribution and its logarithm, over the states asked for. */
53template <class T>
54struct MargResult {
55 std::vector<T> P;
56 std::vector<T> logP;
57};
58
59namespace detail {
60
61/** MATLAB `nchoosekln(n,k)` = gammaln(n+1) - gammaln(k+1) - gammaln(n-k+1). */
62template <class T>
63T num_nchoosekln(const T& n, const T& k) {
64 return T(pfqn::detail::num_factln<T>(n) - pfqn::detail::num_factln<T>(k) -
65 pfqn::detail::num_factln<T>(T(n - k)));
66}
67
68/**
69 * The Schmidt binomial term for one closed class: log C(N,n) + n log(Q/N) +
70 * (N-n) log(1 - Q/N).
71 *
72 * Written exactly as the reference writes it, INCLUDING the behaviour at the
73 * endpoints: Q = 0 makes the second term -inf whenever n > 0 and 0 * log(0) = 0
74 * (NaN in IEEE) when n = 0, and the reference takes `real(exp(.))` of whatever
75 * comes out. Guarding the n = 0 case is not a defensive workaround but the
76 * documented convention that an empty class contributes nothing.
77 */
78template <class T>
79T binom_logterm(const T& N, const T& n, const T& Q) {
80 using std::log;
81 const T zero = num_traits<T>::from_int(0);
82 const T one = num_traits<T>::from_int(1);
83 // C(0,0) = 1 and there is nothing to weight: a class with no population
84 // contributes zero in logs. Forming Q/0 first would give p = +Inf, and the
85 // guards below only happen to keep it out of the result because n and N-n
86 // are both zero there -- which is luck, not a rule the next edit will keep.
88 T lp = num_nchoosekln<T>(N, n);
89 const T p = T(Q / N);
90 if (n > zero) lp += T(n * log(p));
91 if (T(N - n) > zero) lp += T(T(N - n) * log(T(one - p)));
92 return lp;
93}
94
95} // namespace detail
96
97/**
98 * Port of `@@SolverMVA/getProbMarg.m`: P(n jobs of class `r` at station `i`) for
99 * the states in `states` (or the reference's own default range when empty).
100 *
101 * The three cases are the reference's, keyed on the CLASS being open or closed
102 * and, when open, on the station's discipline:
103 *
104 * closed binomial(N_r, Q(i,r)/N_r) over n = 0..N_r; `states` selects
105 * from that vector and a state above N_r is an error
106 * open at INF Poisson with mean Q(i,r)
107 * open at a queue P(n) = (1-rho) rho_r^n / (1-rho+rho_r)^(n+1), the exact
108 * multiclass open product-form marginal
109 * open at EXT a Source has no queue-length distribution; P = 1 at n = 0
110 *
111 * When `states` is empty the open cases pick their own range, as the reference
112 * does: mean + 5 sigma for the Poisson, and for the geometric the n at which the
113 * tail falls below 1e-10, capped at 1000.
114 */
115template <class T>
117 std::size_t ist, std::size_t r,
118 const std::vector<long>& states,
119 const std::string& method = "default") {
121 "getProbMarg fits a binomial / Poisson / geometric law and needs logarithms");
122 using std::ceil;
123 using std::exp;
124 using std::log;
125 using std::sqrt;
126 if (ist == 0 || ist > L.nstations)
127 throw InputError("getProbMarg: station index exceeds the number of stations in the model");
128 if (r == 0 || r > L.nclasses)
129 throw InputError("getProbMarg: job class index exceeds the number of classes in the model");
130
131 const T zero = num_traits<T>::from_int(0);
132 const T one = num_traits<T>::from_int(1);
133 const double Nr = L.classes[r - 1].population;
134 MargResult<T> out;
135
136 if (std::isfinite(Nr)) {
137 // MATLAB getProbMarg exact branch requires all single-server stations
138 // (the pfqn_mvaldmx result is discarded and the binomial below is
139 // returned regardless, so only the guard is observable).
140 if (method == "exact")
141 for (std::size_t s = 0; s < L.nstations; ++s)
142 if (L.stations[s].nservers != 1.0)
143 throw UnsupportedError(
144 "getProbMarg: exact marginalized probabilities require single-server "
145 "stations");
146 // ---- closed class: the Schmidt binomial over 0..N_r ------------------
147 const long n_max = static_cast<long>(Nr);
148 std::vector<T> all(static_cast<std::size_t>(n_max) + 1, zero), alllog(all.size(), zero);
149 const T N = num_traits<T>::from_double(Nr);
150 for (long k = 0; k <= n_max; ++k) {
151 const T lp =
152 detail::binom_logterm<T>(N, num_traits<T>::from_int(k), avg.QN(ist - 1, r - 1));
153 alllog[static_cast<std::size_t>(k)] = lp;
154 all[static_cast<std::size_t>(k)] = exp(lp);
155 }
156 if (states.empty()) {
157 out.P = all;
158 out.logP = alllog;
159 return out;
160 }
161 for (long s : states) {
162 if (s < 0 || s > n_max)
163 throw InputError(
164 "getProbMarg: the requested state exceeds the maximum population for this "
165 "class");
166 out.P.push_back(all[static_cast<std::size_t>(s)]);
167 out.logP.push_back(alllog[static_cast<std::size_t>(s)]);
168 }
169 return out;
170 }
171
172 // ---- open class -------------------------------------------------------
173 const lang::SchedStrategy sched = L.stations[ist - 1].sched;
174 const T neg_inf = num_traits<T>::from_double(-std::numeric_limits<double>::infinity());
175
176 if (sched == lang::SchedStrategy::EXT) {
177 // a Source is not a queue; the reference returns the degenerate law
178 out.P.push_back(one);
179 out.logP.push_back(zero);
180 return out;
181 }
182
183 std::vector<long> sm = states;
184 if (sched == lang::SchedStrategy::INF) {
185 const T lam = avg.QN(ist - 1, r - 1);
186 const double lamd = num_traits<T>::to_double(lam);
187 if (sm.empty()) {
188 const long nm = std::max<long>(
189 1, static_cast<long>(std::ceil(lamd + 5.0 * std::sqrt(std::max(lamd, 1.0)))));
190 for (long n = 0; n <= nm; ++n) sm.push_back(n);
191 }
192 for (long n : sm) {
193 T lp;
194 if (lam > zero) {
195 lp = T(num_traits<T>::from_int(n) * log(lam) - lam -
196 pfqn::detail::num_factln<T>(num_traits<T>::from_int(n)));
197 } else {
198 lp = (n == 0) ? zero : neg_inf;
199 }
200 out.logP.push_back(lp);
201 out.P.push_back(n == 0 && !(lam > zero) ? one : exp(lp));
202 }
203 return out;
204 }
205
206 // a queueing station: the multiclass open product-form marginal. rho is
207 // capped just below 1 exactly as the reference caps it, so a saturated
208 // station yields the all-zero law rather than a negative logarithm.
209 const T rho_r = avg.UN(ist - 1, r - 1);
210 T rho_tot = zero;
211 for (std::size_t k = 0; k < L.nclasses; ++k) {
212 const T u = avg.UN(ist - 1, k);
213 if (num_traits<T>::to_double(u) == num_traits<T>::to_double(u)) rho_tot += u; // skip NaN
214 }
215 const T rho_cap = T(one - num_traits<T>::from_double(GlobalConstants::FineTol));
216 if (rho_tot > rho_cap) rho_tot = rho_cap;
217
218 if (sm.empty()) {
219 long nm = 0;
220 if (rho_r > zero && rho_tot < one) {
221 const T denom = T(one - rho_tot + rho_r);
222 const double ratio = num_traits<T>::to_double(T(rho_r / denom));
223 if (ratio > 0.0 && ratio < 1.0)
224 nm = std::max<long>(1, static_cast<long>(std::ceil(-std::log(1e-10) / -std::log(ratio))));
225 nm = std::min<long>(nm, 1000);
226 }
227 for (long n = 0; n <= nm; ++n) sm.push_back(n);
228 }
229 if (!(rho_tot < rho_cap)) {
230 out.P.assign(sm.size(), zero);
231 out.logP.assign(sm.size(), neg_inf);
232 return out;
233 }
234 const T denom = T(one - rho_tot + rho_r);
235 for (long n : sm) {
236 const T nn = num_traits<T>::from_int(n);
237 T lp;
238 if (rho_r > zero) {
239 lp = T(log(T(one - rho_tot)) + nn * log(rho_r) - T(nn + one) * log(denom));
240 } else if (n == 0) {
241 lp = T(log(T(one - rho_tot)) - log(denom));
242 } else {
243 lp = neg_inf;
244 }
245 out.logP.push_back(lp);
246 out.P.push_back(exp(lp));
247 }
248 return out;
249}
250
251/**
252 * Port of `@@SolverMVA/getProbNormConstAggr.m`: log G.
253 *
254 * The reference re-enters the analyzer with `method='exact'` rather than reusing
255 * whatever the last solve produced, because only the exact MVA recursion carries
256 * a normalizing constant; an AMVA result has no G to report. A cached value from
257 * a previous solve is returned unchanged, which is what `self.result.Prob` does.
258 */
259template <class T>
261 MvaOptions o = opt;
262 o.method = "exact";
263 const DispatchResult<T> dr = mva_dispatch(L, o, Matrix<T>());
264 return dr.sol.lG;
265}
266
267namespace detail {
268
269/**
270 * The per-class occupancy `nir` of the model's default initial state, station by
271 * station: every closed class's whole population sits at its reference station,
272 * open classes hold no jobs. This is what `State.toMarginal(sn.state{i})`
273 * returns for that state, so `getProbAggr`/`getProbSysAggr` read it directly.
274 */
275template <class T>
276Matrix<T> initial_marginal(const qn::NetworkStruct<T>& L) {
277 // THE MODEL'S DECLARED STATE, not a rebuilt default marking. The reference
278 // reads `State.toMarginal(sn, ist, state{isf})`, so a `setState` moves the
279 // question these getters answer; rebuilding the default here answered about
280 // the reference-station placement under the caller's name. `sn_declared_marginal`
281 // falls back to that placement per station where nothing was declared.
283}
284
285} // namespace detail
286
287/**
288 * Port of `@@SolverMVA/getProbAggr.m`: P(n1 jobs of class 1, n2 of class 2, ...)
289 * at station `ist` for the model's state, a scalar in [0,1] with its log.
290 *
291 * Closed classes take the Schmidt binomial fitted to Q(i,r); open classes take
292 * the BCMP product form of the station (independent Poisson at an INF server,
293 * multinomial-geometric at a queue, nothing at an EXT source), exactly as the
294 * reference splits them.
295 */
296template <class T>
298 T P;
300};
301
302template <class T>
304 std::size_t ist, const std::string& method = "default") {
306 "getProbAggr fits a binomial / product-form law and needs logarithms");
307 using std::exp;
308 using std::log;
309 if (ist == 0 || ist > L.nstations)
310 throw InputError("getProbAggr: station number exceeds the number of stations in the model");
311 const std::size_t i = ist - 1;
312 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
313 const Matrix<T> nir = detail::initial_marginal(L);
314
315 bool all_closed = true;
316 for (std::size_t r = 0; r < L.nclasses; ++r)
317 if (!std::isfinite(L.classes[r].population)) all_closed = false;
318
319 // MATLAB getProbAggr: a closed model with method='exact' errors -- the exact
320 // marginal is not implemented; only the Schmidt binomial approximation is.
321 if (all_closed && method == "exact")
322 throw UnsupportedError(
323 "getProbAggr: exact marginal state probabilities are not available yet in SolverMVA");
324
325 T logP = zero;
326 if (!all_closed) {
327 // Open classes: BCMP product form of the station, shared with getProbSysAggr.
328 const api::OpenProbTerm<T> term = api::sn_open_prob_terms(L, avg.QN, avg.UN, nir, i);
329 if (!term.feasible) return {zero, num_traits<T>::from_double(-1e308)};
330 logP = T(logP + term.logp);
331 }
332 // Closed classes: Schmidt binomial.
333 for (std::size_t r = 0; r < L.nclasses; ++r) {
334 if (!std::isfinite(L.classes[r].population)) continue;
335 const T N = num_traits<T>::from_double(L.classes[r].population);
336 logP = T(logP + detail::binom_logterm<T>(N, nir(i, r), avg.QN(i, r)));
337 }
338 return {T(exp(logP)), logP};
339}
340
341/**
342 * Port of `@@SolverMVA/getProbSysAggr.m`: the joint probability of the model's
343 * whole state across all stations, a scalar in [0,1] with its log. Closed
344 * classes take the multinomial-binomial normalization sum(factln(N)) - the
345 * per-station product; open classes take the same per-station BCMP form as
346 * getProbAggr.
347 */
348template <class T>
350 const AvgResult<T>& avg,
351 const std::string& method = "default") {
353 "getProbSysAggr fits a binomial / product-form law and needs logarithms");
354 using std::exp;
355 using std::log;
356 const T zero = num_traits<T>::from_int(0), one = num_traits<T>::from_int(1);
357 const Matrix<T> nir = detail::initial_marginal(L);
358
359 bool all_closed = true;
360 for (std::size_t r = 0; r < L.nclasses; ++r)
361 if (!std::isfinite(L.classes[r].population)) all_closed = false;
362
363 // MATLAB getProbSysAggr: a closed model with method='exact' errors -- only
364 // the Schmidt product-form approximation is implemented.
365 if (all_closed && method == "exact")
366 throw UnsupportedError(
367 "getProbSysAggr: exact joint state probabilities are not available yet in SolverMVA");
368
369 T logP = zero;
370 if (all_closed) {
371 for (std::size_t r = 0; r < L.nclasses; ++r)
372 logP = T(logP + pfqn::detail::num_factln<T>(num_traits<T>::from_double(
373 L.classes[r].population)));
374 for (std::size_t i = 0; i < L.nstations; ++i)
375 for (std::size_t r = 0; r < L.nclasses; ++r) {
376 // A CLASS WITH NO POPULATION CONTRIBUTES NOTHING. Under class
377 // switching a class can carry N=0 and still show a positive
378 // QN(i,r), because the jobs in it arrived by switching; then
379 // log(QN/0) is +Inf while nir(i,r) is 0, and 0*Inf is NaN,
380 // which propagates through exp() and makes the whole joint
381 // probability nan. Its binomial factor is C(0,0)=1, which is
382 // zero in logs -- exactly what skipping it records.
383 if (L.classes[r].population == 0.0) continue;
384 const T N = num_traits<T>::from_double(L.classes[r].population);
385 logP = T(logP - pfqn::detail::num_factln<T>(nir(i, r)));
386 if (avg.QN(i, r) > zero)
387 logP = T(logP + nir(i, r) * log(T(avg.QN(i, r) / N)));
388 }
389 return {T(exp(logP)), logP};
390 }
391
392 // Mixed / open: closed-class multinomial normalization, then per station.
393 for (std::size_t r = 0; r < L.nclasses; ++r)
394 if (std::isfinite(L.classes[r].population))
395 logP = T(logP + pfqn::detail::num_factln<T>(num_traits<T>::from_double(
396 L.classes[r].population)));
397 for (std::size_t i = 0; i < L.nstations; ++i) {
398 const api::OpenProbTerm<T> term = api::sn_open_prob_terms(L, avg.QN, avg.UN, nir, i);
399 if (!term.feasible) return {zero, num_traits<T>::from_double(-1e308)};
400 logP = T(logP + term.logp);
401 for (std::size_t r = 0; r < L.nclasses; ++r) {
402 if (!std::isfinite(L.classes[r].population)) continue;
403 // See the closed branch: N=0 must not reach log(QN/N).
404 if (L.classes[r].population == 0.0) continue;
405 const T N = num_traits<T>::from_double(L.classes[r].population);
406 logP = T(logP - pfqn::detail::num_factln<T>(nir(i, r)));
407 if (avg.QN(i, r) > zero) logP = T(logP + nir(i, r) * log(T(avg.QN(i, r) / N)));
408 }
409 }
410 return {T(exp(logP)), logP};
411}
412
413} // namespace mva
414} // namespace line
415
416#endif // LINE_SOLVERS_MVA_SOLVER_MVA_PROB_H
InputError(const std::string &what)
Definition error.h:39
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
std::vector< JobClass > classes
std::vector< Station< T > > stations
stations[k-1] is the k-th station
Matrix< T > sn_declared_marginal(const qn::NetworkStruct< T > &sn)
The (nstations x nclasses) per-class job counts of the model's OWN state.
Definition sn_state.h:85
OpenProbTerm< T > sn_open_prob_terms(const qn::NetworkStruct< T > &sn, const Matrix< T > &Q, const Matrix< T > &U, const Matrix< T > &nir, std::size_t ist)
Open-class contribution to an aggregate state probability at one station.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
AggrResult< T > solver_mva_get_prob_aggr(const qn::NetworkStruct< T > &L, const AvgResult< T > &avg, std::size_t ist, const std::string &method="default")
T solver_mva_get_prob_norm_const_aggr(const qn::NetworkStruct< T > &L, const MvaOptions &opt)
Port of @@SolverMVA/getProbNormConstAggr.m: log G.
MargResult< T > solver_mva_get_prob_marg(const qn::NetworkStruct< T > &L, const AvgResult< T > &avg, std::size_t ist, std::size_t r, const std::vector< long > &states, const std::string &method="default")
Port of @@SolverMVA/getProbMarg.m: P(n jobs of class r at station i) for the states in states (or the...
AggrResult< T > solver_mva_get_prob_sys_aggr(const qn::NetworkStruct< T > &L, const AvgResult< T > &avg, const std::string &method="default")
Port of @@SolverMVA/getProbSysAggr.m: the joint probability of the model's whole state across all sta...
DispatchResult< T > mva_dispatch(const qn::NetworkStruct< T > &L, const MvaOptions &opt, const Matrix< T > &init_sol)
The ladder itself.
A queueing network and its refreshed NetworkStruct.
Shared scalar machinery for the integration / asymptotic members of the pfqn family (pfqn_le,...
Open-class contribution to an aggregate state probability at one station.
Ports of matlab/src/api/sn/sn_get_state_aggr.m and sn_is_state_valid.m.
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
The log contribution, and whether the state carries any mass at all.
bool feasible
false when the law gives the state zero probability
T logp
log of the open-class factor at this station
Port of @@SolverMVA/getProbAggr.m: P(n1 jobs of class 1, n2 of class 2, ...) at station ist for the m...
The metrics getAvg returns, after filtering.
Matrix< T > UN
utilization
Matrix< T > QN
queue length
What the dispatch returns: the metrics plus the algorithm that produced them.
static constexpr double FineTol
Definition lang_types.h:668
A marginal distribution and its logarithm, over the states asked for.
std::vector< T > logP
The options SolverMVA reads.
Definition mva_types.h:31
std::string method
Definition mva_types.h:32