LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
npfqn_nonexp_approx.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_API_NPFQN_NONEXP_APPROX_H
6#define LINE_API_NPFQN_NONEXP_APPROX_H
7
8/**
9 * @file
10 * @ingroup api_npfqn
11 * Handler for non-exponential service and arrival processes in AMVA and NC.
12 *
13 * Templated port of matlab/src/api/npfqn/npfqn_nonexp_approx.m, cross-checked
14 * against jar/src/main/java/jline/api/npfqn/Npfqn_nonexp_approx.java. See the
15 * note on the method names at the end of this comment: the two disagree there.
16 *
17 * The 'interp' method replaces the service time of every FCFS station whose
18 * per-class demands or SCVs make it non-product-form by the WSC 2020
19 * interpolation (LINE paper, Sec. 4.2) between the M/G/1 diffusion decay rate
20 *
21 * eta_i = exp(-2 (1 - rho_i) / (c2_{s,i} + c2_{a,i} rho_i)) (Kobayashi)
22 *
23 * and the multiserver asymptotic decay rate gamma_i = (rho_i^{c_i} + rho_i)/2,
24 * with weights a_i = b_i = rho_i^8. The multiserver effect is absorbed into
25 * the scaled service time, so the station is returned with one server.
26 *
27 * Arithmetic. eta carries an exp, so this requires transcendental arithmetic
28 * and cannot be instantiated at T = Rational. rho^{c_i} is written with a real
29 * exponent because MATLAB stores the server count as a double; rho^8 uses the
30 * integer power, which is the same value in every arithmetic.
31 *
32 * Model layer. The MATLAB function reads exactly two fields of the
33 * NetworkStruct, sn.sched(i) (only ever compared against SchedStrategy.FCFS)
34 * and sn.rates(i,k) (only ever compared against zero). Since the model layer
35 * is not part of this port, those two are passed explicitly as isFCFS and
36 * rates; nothing else about sn is consulted by the algorithm, so this is a
37 * transcription of the same code, not a reduced variant.
38 *
39 * MATLAB / JAR disagreement (unresolved here, reported upstream): MATLAB's
40 * no-op branch is case {'default','none'} plus case {'hvmva'}, and
41 * solver_amvald_forward.m dispatches on the string 'hvmva'. The JAR instead
42 * accepts "hmva" and throws IllegalArgumentException on anything else, while
43 * its own Solver_amvald.java tests options.config.highvar against "hvmva", so
44 * a JAR run with highvar='hvmva' reaching SolverFluid throws instead of
45 * no-opping. Python native (line_solver/api/npfqn/nonexp.py) copies the JAR.
46 * This port follows MATLAB, the reference implementation, and accepts both
47 * spellings so that a caller ported from either side behaves identically.
48 */
49
50#include <cstddef>
51#include <string>
52#include <vector>
53
55#include "line/num/number.h"
56#include "line/util/error.h"
57#include "line/util/matrix.h"
58
59namespace line {
60namespace npfqn {
61
62/** Return value, mirroring MATLAB's [ST,gamma,nservers,rho,scva,scvs,eta]. */
63template <class T>
65 Matrix<T> ST; ///< (M x R) scaled service times
66 std::vector<T> gamma; ///< (M) multiserver asymptotic decay rate
67 std::vector<T> nservers; ///< (M) server counts, set to 1 where rescaled
68 std::vector<T> rho; ///< (M) station utilization
69 std::vector<T> scva; ///< (M) arrival SCV used by the approximation
70 std::vector<T> scvs; ///< (M) throughput-weighted service SCV
71 std::vector<T> eta; ///< (M) diffusion decay rate
72};
73
74/**
75 * @brief Handler for non-exponential service and arrival processes in AMVA
76 * and NC.
77 *
78 * @param method 'default', 'none', 'hvmva' (all no-ops) or 'interp'
79 * @param isFCFS (M) sn.sched(i) == SchedStrategy.FCFS
80 * @param rates (M x R) sn.rates
81 * @param ST (M x R) service times
82 * @param V (M x R) visit ratios; present in the MATLAB signature but
83 * never read by it, kept here for a 1:1 argument list
84 * @param SCV (M x R) service SCVs
85 * @param Tput (M x R) per-class throughputs
86 * @param U (M x R) per-class utilizations
87 * @param gamma (M) input decay rates
88 * @param nservers (M) input server counts
89 */
90template <class T>
91NonexpApproxResult<T> npfqn_nonexp_approx(const std::string& method, const std::vector<bool>& isFCFS,
92 const Matrix<T>& rates, const Matrix<T>& ST,
93 const Matrix<T>& V, const Matrix<T>& SCV,
94 const Matrix<T>& Tput, const Matrix<T>& U,
95 const std::vector<T>& gamma,
96 const std::vector<T>& nservers) {
98 "npfqn_nonexp_approx requires transcendental arithmetic");
99 (void)V;
100 const std::size_t M = isFCFS.size();
101 const std::size_t R = ST.cols();
102 if (ST.rows() != M || SCV.rows() != M || Tput.rows() != M || U.rows() != M || rates.rows() != M)
103 throw InputError("npfqn_nonexp_approx: an input matrix has the wrong number of stations");
104 if (gamma.size() != M || nservers.size() != M)
105 throw InputError("npfqn_nonexp_approx: gamma or nservers has the wrong length");
106
107 const T zero = num_traits<T>::from_int(0);
108 const T one = num_traits<T>::from_int(1);
109 const T two = num_traits<T>::from_int(2);
110 const T fineTol = num_traits<T>::from_double(1e-8); // GlobalConstants.FineTol
111 const T unitTol = num_traits<T>::from_double(1e-6);
112
114 res.ST = ST;
115 res.gamma = gamma;
116 res.nservers = nservers;
117 res.rho.assign(M, zero);
118 res.scva.assign(M, one);
119 res.scvs.assign(M, one);
120 res.eta.assign(M, one);
121
122 if (method == "default" || method == "none" || method == "hvmva" || method == "hmva") return res;
123 if (method != "interp")
124 throw InputError("npfqn_nonexp_approx: unsupported method '" + method + "'");
125
126 for (std::size_t ist = 0; ist < M; ++ist) {
127 std::vector<std::size_t> nnz;
128 for (std::size_t k = 0; k < R; ++k)
129 if (detail::num_isfinite(res.ST(ist, k)) && detail::num_isfinite(SCV(ist, k)))
130 nnz.push_back(k);
131 for (std::size_t k : nnz) res.rho[ist] += U(ist, k);
132 if (nnz.empty() || !isFCFS[ist]) continue;
133
134 // non-product-form test: unequal demands, or any SCV away from 1
135 T stMin = res.ST(ist, nnz[0]), stMax = res.ST(ist, nnz[0]);
136 T scvMin = SCV(ist, nnz[0]), scvMax = SCV(ist, nnz[0]);
137 for (std::size_t k : nnz) {
138 if (res.ST(ist, k) < stMin) stMin = res.ST(ist, k);
139 if (res.ST(ist, k) > stMax) stMax = res.ST(ist, k);
140 if (SCV(ist, k) < scvMin) scvMin = SCV(ist, k);
141 if (SCV(ist, k) > scvMax) scvMax = SCV(ist, k);
142 }
143 const bool nonPf = stMax - stMin > zero || scvMax > one + fineTol || scvMin < one - fineTol;
144 if (!nonPf) continue;
145
146 res.scva[ist] = one; // use an M/G/k approximation
147 T tsum = zero, wsum = zero;
148 for (std::size_t k : nnz) {
149 tsum += Tput(ist, k);
150 wsum += SCV(ist, k) * Tput(ist, k);
151 }
152 res.scvs[ist] = wsum / tsum;
153 // multi-server asymptotic decay rate
154 res.gamma[ist] = (detail::num_pow(res.rho[ist], res.nservers[ist]) + res.rho[ist]) / two;
155
156 if (res.scvs[ist] > one - unitTol && res.scvs[ist] < one + unitTol && res.nservers[ist] == one) {
157 res.eta[ist] = res.rho[ist]; // M/M/1
158 } else {
159 // single-server diffusion approximation (Kobayashi, JACM)
160 res.eta[ist] = detail::num_exp(
161 T(-two * (one - res.rho[ist]) / (res.scvs[ist] + res.scva[ist] * res.rho[ist])));
162 }
163
164 // interpolation (Sec. 4.2, LINE paper at WSC 2020). The ai, bi
165 // coefficients use the 8th power, which numerically beats the 4th.
166 const T ai = num_pow_int(res.rho[ist], 8u);
167 const T bi = ai;
168 T oneMinusAi = one - ai;
169 if (oneMinusAi < zero) oneMinusAi = zero;
170 T oneMinusBi = one - bi;
171 if (oneMinusBi < zero) oneMinusBi = zero;
172 for (std::size_t k : nnz) {
173 if (rates(ist, k) > zero)
174 res.ST(ist, k) = oneMinusAi * res.ST(ist, k) +
175 ai * (bi * res.eta[ist] + oneMinusBi * res.gamma[ist]) *
176 (res.nservers[ist] / tsum);
177 }
178 // multi-server effects are now inside the scaled service times
179 res.nservers[ist] = one;
180 }
181 return res;
182}
183
184} // namespace npfqn
185} // namespace line
186
187#endif // LINE_API_NPFQN_NONEXP_APPROX_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
The exception types the port throws.
Dense matrix and non-owning view.
NonexpApproxResult< T > npfqn_nonexp_approx(const std::string &method, const std::vector< bool > &isFCFS, const Matrix< T > &rates, const Matrix< T > &ST, const Matrix< T > &V, const Matrix< T > &SCV, const Matrix< T > &Tput, const Matrix< T > &U, const std::vector< T > &gamma, const std::vector< T > &nservers)
Handler for non-exponential service and arrival processes in AMVA and NC.
T num_pow_int(const T &base, unsigned e)
Integer power, valid in any field (no transcendental requirement).
Definition number.h:192
Shared arithmetic helpers for the templated npfqn port.
Number-type abstraction for the templated API port.
Return value, mirroring MATLAB's [ST,gamma,nservers,rho,scva,scvs,eta].
std::vector< T > scva
(M) arrival SCV used by the approximation
std::vector< T > nservers
(M) server counts, set to 1 where rescaled
Matrix< T > ST
(M x R) scaled service times
std::vector< T > scvs
(M) throughput-weighted service SCV
std::vector< T > gamma
(M) multiserver asymptotic decay rate
std::vector< T > eta
(M) diffusion decay rate
std::vector< T > rho
(M) station utilization