LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fluid_qsys.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_FLUID_FLUID_QSYS_H
6#define LINE_SOLVERS_FLUID_FLUID_QSYS_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of matlab/src/solvers/FLD/solver_fluid_qsys_analyzer.m: the
12 * single-station fluid limits.
13 *
14 * A Source -> Queue -> Sink model with one class, answered by a closed-form
15 * fluid or Gaussian limit rather than by integrating the network drift.
16 *
17 * WHY THESE ARE FLUID METHODS AND NOT MVA ONES. Each depends on the service or
18 * patience law BEYOND ITS MEAN -- the stationary point of the Liu-Whitt model is
19 * where the patience ccdf crosses 1/rho, the Mt/G/inf mean is a convolution with
20 * the service ccdf -- and each is the limit of a sequence of systems, not an
21 * approximation to a fixed one. That is the fluid solver's contract.
22 *
23 * METHODS
24 * `ggisgi` stationary point of the G/GI/s+GI fluid model (Liu and Whitt,
25 * Operations Research 60(5), 2012)
26 * `ggingi` truncated Gaussian approximation, the O(sqrt(n)) fluctuation
27 * around that point (Liu, Whitt and Yu, NRL 63(3), 2016)
28 * `tvms` the Gt/Mt/st+GI many-server fluid queue at CONSTANT staffing
29 * (Liu and Whitt, INFORMS J. Computing 26(1), 2014)
30 * `mtginf` the exact Mt/G/inf mean (Eick, Massey and Whitt, Management
31 * Science 39(2), 1993)
32 * `mol` the modified-offered-load approximation for a finite server
33 * count (Massey and Whitt, Ann. Appl. Prob. 4(4), 1994)
34 *
35 * ARITHMETIC: transcendental. Every one of them integrates or bisects.
36 */
37
38#include <algorithm>
39#include <cmath>
40#include <cstddef>
41#include <functional>
42#include <limits>
43#include <string>
44#include <vector>
45
55#include "line/num/number.h"
57
58namespace line {
59namespace fluid {
60
61namespace detail {
62
63/**
64 * The method names the single-station limits answer, AFTER `fluid_unqualify`.
65 *
66 * That strips a leading `fld.`, so the qualified spellings arrive here as the
67 * canonical bare names reported by `FluidSolution::method`.
68 */
69inline bool fluid_qsys_handles(const std::string& m) {
70 return m == "ggisgi" || m == "ggingi" ||
71 m == "tvms" || m == "mtginf" || m == "mol";
72}
73
74/** The primary name of an alias. */
75inline std::string fluid_qsys_canonical(const std::string& m) { return m; }
76
77/**
78 * The integration window of a time-varying single-station limit, asked as a
79 * predicate rather than thrown.
80 *
81 * A horizon is a solver OPTION and not a model feature, so the feature registry
82 * has no name for it and a report has to ask this predicate directly.
83 * `fluid_qsys_horizon` asks the same one on the solve path, which is what keeps
84 * the report and the run from disagreeing about whether a method can be asked
85 * for.
86 *
87 * @param opt the fluid knobs, read for the end of the horizon
88 * @return an empty string when the window is a finite non-empty interval
89 */
90inline std::string fluid_qsys_horizon_reason(const FluidOptions& opt) {
91 const double t1 = opt.timespan_end;
92 if (!std::isfinite(t1) || t1 <= 0.0)
93 return "solver_fluid_qsys: a time-varying fluid method needs a finite horizon; set "
94 "options.timespan_end";
95 return std::string();
96}
97
98/**
99 * The integration window, [0, timespan_end].
100 *
101 * The C++ FluidOptions carries only the END of the horizon, where MATLAB and
102 * Python carry a pair; the start is 0 for every fluid route in this port, which
103 * is also what `runAnalyzer.m` forces when the start is not finite. A
104 * non-positive or infinite end has no trajectory to report, and rather than
105 * substituting `fluid_default_horizon`'s 30/min_rate -- a stationary-model rule
106 * of thumb that says nothing about the PERIOD of a time-varying arrival -- the
107 * caller is asked for one.
108 */
109template <class T>
110void fluid_qsys_horizon(const qn::NetworkStruct<T>&, const FluidOptions& opt, double& t0,
111 double& t1) {
112 const std::string reason = fluid_qsys_horizon_reason(opt);
113 if (!reason.empty()) throw UnsupportedError(reason);
114 t0 = 0.0;
115 t1 = opt.timespan_end;
116}
117
118} // namespace detail
119
120/**
121 * The three single-station limits that report a TRAJECTORY rather than a
122 * stationary point, and so need a finite horizon; `ggisgi` and `ggingi` are
123 * stationary and are not among them.
124 *
125 * @param m the method name, already unqualified
126 * @return true when the method integrates over a finite horizon
127 */
128inline bool fluid_is_time_varying_limit(const std::string& m) {
129 return m == "tvms" || m == "mtginf" || m == "mol";
130}
131
132/**
133 * The horizon rule the time-varying limits impose, as a public predicate a
134 * REPORT can ask: empty when `method` may be asked for with these options.
135 *
136 * `solver_fluid_qsys` refuses through the same body, so a pair the report
137 * offers is a pair the limit runs.
138 *
139 * @param method the method name, qualified or not
140 * @param opt the fluid knobs, read for the end of the horizon
141 * @return an empty string when the method may run, else the refusal
142 */
143inline std::string fluid_qsys_horizon_supports(const std::string& method,
144 const FluidOptions& opt) {
145 // The `fld.` prefix is stripped here rather than through
146 // `detail::fluid_unqualify`, which lives in fluid_runner.h: that header
147 // includes this one, so the dependency only goes the one way.
148 const std::string m = (method.size() > 4 && method.compare(0, 4, "fld.") == 0)
149 ? method.substr(4)
150 : method;
151 if (!fluid_is_time_varying_limit(m)) return std::string();
152 const std::string reason = detail::fluid_qsys_horizon_reason(opt);
153 if (reason.empty()) return std::string();
154 return "the '" + method + "' method reports a trajectory. " + reason;
155}
156
157/**
158 * Solve a single-station model with one of the closed-form fluid limits.
159 *
160 * The steady-state row of a TIME-VARYING model is the TIME AVERAGE over the
161 * horizon, which is what a stationary reader of a periodic system measures; the
162 * trajectory itself is available from `solver_fluid_qsys_transient`.
163 */
164template <class T>
166 std::vector<FluidTranPoint>* traj = nullptr) {
167 if constexpr (!num_traits<T>::has_transcendental) {
168 throw UnsupportedError(
169 "solver_fluid_qsys: the single-station fluid limits integrate and bisect, so they need "
170 "transcendental arithmetic; rerun with --arith double or --arith real");
171 } else {
172 const std::size_t M = sn.nstations;
173 const std::size_t K = sn.nclasses;
174 FluidSolution out;
175 out.QN = Matrix<double>(M, K, 0.0);
176 out.UN = Matrix<double>(M, K, 0.0);
177 out.RN = Matrix<double>(M, K, 0.0);
178 out.TN = Matrix<double>(M, K, 0.0);
179 out.CN.assign(K, 0.0);
180 out.XN.assign(K, 0.0);
181 out.iters = 1;
182
183 std::size_t src = 0;
184 std::size_t qi = 0;
185 bool haveSrc = false;
186 bool haveQ = false;
187 for (std::size_t i = 0; i < sn.nof_nodes(); ++i) {
188 if (sn.nodes[i].nodetype == qn::NodeType::Source) {
189 src = sn.nodes[i].station - 1;
190 haveSrc = true;
191 } else if (sn.nodes[i].nodetype == qn::NodeType::Queue ||
192 sn.nodes[i].nodetype == qn::NodeType::Delay) {
193 qi = sn.nodes[i].station - 1;
194 haveQ = true;
195 }
196 }
197 // THE SHAPE THESE LIMITS ARE STATED FOR, refused by name rather than
198 // answered on a model they do not describe: one open class through one
199 // queueing station. The MVA qsys analyzer is reached by a structural
200 // dispatch that guarantees it; these methods are selected by NAME, so the
201 // check has to live here.
202 if (!haveSrc || !haveQ)
203 throw UnsupportedError(
204 "solver_fluid_qsys: the single-station fluid limits need a Source and a queueing "
205 "station");
206 if (K != 1 || sn.nclosedjobs() > 0)
207 throw UnsupportedError("solver_fluid_qsys: the '" + opt.method +
208 "' method is a single-station limit: it needs one open class "
209 "through one Source and one queueing station");
210
211 const std::size_t qstateful = sn.stateful_of_station(qi + 1);
212 const T Vq = sn.visits[0](qstateful - 1, 0);
213 const T lambda = T(sn.rates(src, 0) * Vq);
214 const T mu = sn.rates(qi, 0);
215 const double nserv = sn.stations[qi].nservers;
216 const T scvS = sn.scv(qi, 0);
217 const T ca = num_traits<T>::from_double(std::sqrt(num_traits<T>::to_double(sn.scv(src, 0))));
218 const T cs = num_traits<T>::from_double(std::sqrt(num_traits<T>::to_double(scvS)));
220
221 // The service ccdf, needed by the two Mt/G methods: they are exact in the
222 // service DISTRIBUTION, not in its mean, which is the whole point of the
223 // Eick-Massey-Whitt lag.
224 const lang::Distrib<T>& svc = sn.service[qi][0];
225 std::function<T(const T&)> serviceCcdf;
226 if (svc.has_map()) {
227 const mam::Map<T> sm = lang::dist_to_map(svc);
228 const T one = num_traits<T>::from_int(1);
229 serviceCcdf = [sm, one](const T& x) {
230 std::vector<T> pts(1, x);
231 return T(one - mam::map_cdf(sm, pts)[0]);
232 };
233 } else {
234 serviceCcdf = [mu](const T& x) { return qsys::detail::num_exp(T(-mu * x)); };
235 }
236 const T ES = T(num_traits<T>::from_int(1) / mu);
237 const double ES2 =
240
241 const std::string m = detail::fluid_qsys_canonical(opt.method);
242 const double VqD = num_traits<T>::to_double(Vq);
243 const double lamD = num_traits<T>::to_double(lambda);
244 const double muD = num_traits<T>::to_double(mu);
245
246 // Little's law on the CARRIED rate, as every LINE solver reports a station
247 // that loses work.
248 auto stationary = [&](double Lsys, double Tq, double Uq) {
249 const double R = Tq > 0 ? Lsys / Tq : 0.0;
250 out.RN(qi, 0) = R;
251 out.QN(qi, 0) = Lsys;
252 out.UN(qi, 0) = Uq;
253 out.TN(qi, 0) = Tq;
254 out.TN(src, 0) = lamD / VqD;
255 out.XN[0] = Tq;
256 out.CN[0] = R * VqD;
257 };
258
259 auto transient = [&](const std::vector<T>& t, const std::vector<T>& Lt,
260 const std::vector<T>& Ut, const std::vector<T>& Tt,
261 const std::vector<T>& arrival) {
262 const std::size_t n = t.size();
263 std::vector<double> td(n), Ld(n), Ud(n), Td(n), Ad(n);
264 for (std::size_t i = 0; i < n; ++i) {
265 td[i] = num_traits<T>::to_double(t[i]);
266 Ld[i] = num_traits<T>::to_double(Lt[i]);
267 Ud[i] = num_traits<T>::to_double(Ut[i]);
268 Td[i] = num_traits<T>::to_double(Tt[i]);
269 Ad[i] = num_traits<T>::to_double(arrival[i]);
270 }
271 auto trapz = [&](const std::vector<double>& y) {
272 double s = 0;
273 for (std::size_t i = 1; i < n; ++i) s += 0.5 * (y[i] + y[i - 1]) * (td[i] - td[i - 1]);
274 return s;
275 };
276 const double span = td[n - 1] - td[0];
277 const double Lbar = span > 0 ? trapz(Ld) / span : Ld[0];
278 const double Ubar = span > 0 ? trapz(Ud) / span : Ud[0];
279 const double Tbar = span > 0 ? trapz(Td) / span : Td[0];
280 const double Abar = span > 0 ? trapz(Ad) / span : Ad[0];
281 out.QN(qi, 0) = Lbar;
282 out.UN(qi, 0) = Ubar;
283 out.TN(qi, 0) = Tbar;
284 out.TN(src, 0) = Abar / VqD;
285 out.RN(qi, 0) = Tbar > 0 ? Lbar / Tbar : 0.0;
286 out.XN[0] = Tbar;
287 out.CN[0] = out.RN(qi, 0) * VqD;
288 if (traj != nullptr) {
289 traj->clear();
290 traj->reserve(n);
291 for (std::size_t i = 0; i < n; ++i) {
293 p.t = td[i];
294 p.QN = Matrix<double>(M, K, 0.0);
295 p.UN = Matrix<double>(M, K, 0.0);
296 p.TN = Matrix<double>(M, K, 0.0);
297 p.QN(qi, 0) = Ld[i];
298 p.UN(qi, 0) = Ud[i];
299 p.TN(qi, 0) = Td[i];
300 p.TN(src, 0) = Ad[i] / VqD;
301 traj->push_back(p);
302 }
303 }
304 };
305
306 auto requirePatience = [&]() {
307 if (!h.present)
308 throw UnsupportedError("solver_fluid_qsys: the '" + m +
309 "' method needs a reneging patience law on the queue "
310 "(Queue.setPatience)");
311 };
312 auto requireFiniteServers = [&]() {
313 if (!std::isfinite(nserv) || nserv < 1)
314 throw UnsupportedError("solver_fluid_qsys: the '" + m +
315 "' method needs a finite number of servers");
316 };
317 auto linspaceT = [](double a, double b, std::size_t n) {
318 std::vector<T> v(n);
319 for (std::size_t i = 0; i < n; ++i)
320 v[i] = num_traits<T>::from_double(a + (b - a) * static_cast<double>(i) /
321 static_cast<double>(n - 1));
322 return v;
323 };
324
325 if (m == "ggisgi") {
326 requirePatience();
328 lambda, mu, static_cast<unsigned>(std::llround(nserv)), h.ccdf);
331 } else if (m == "ggingi") {
332 requirePatience();
333 requireFiniteServers();
335 lambda, mu, static_cast<unsigned>(std::llround(nserv)), ca, cs, h.ccdf, h.pdf,
336 serviceCcdf);
337 const double pa = num_traits<T>::to_double(r.probAbandon);
338 stationary(num_traits<T>::to_double(r.meanNumber), lamD * (1.0 - pa),
339 std::min(num_traits<T>::to_double(r.meanNumberInService) / nserv, 1.0));
340 } else if (m == "tvms") {
341 requirePatience();
342 requireFiniteServers();
344 double t0 = 0, t1 = 0;
345 detail::fluid_qsys_horizon(sn, opt, t0, t1);
346 // CONSTANT STAFFING. Nothing in a Network declares a time-varying server
347 // count, so s(t) is the station's own s; the time variation the method
348 // is for enters through lambda(t) alone. A staffing schedule would need
349 // a model feature that does not exist, and inventing one here would make
350 // the solver answer a model the user did not build.
351 const T sT = num_traits<T>::from_double(nserv);
353 tvopt.pdf = h.pdf;
355 rf.lambda, [sT](const T&) { return sT; }, [mu](const T&) { return mu; }, h.ccdf,
356 num_traits<T>::from_double(t1 - t0), tvopt);
357 std::vector<T> times = r.times;
358 for (std::size_t i = 0; i < times.size(); ++i)
359 times[i] = T(times[i] + num_traits<T>::from_double(t0));
360 std::vector<T> served(r.B.size());
361 for (std::size_t i = 0; i < r.B.size(); ++i) served[i] = T(mu * r.B[i]);
362 transient(times, r.X, r.utilization, served, r.arrivalRate);
363 } else if (m == "mtginf") {
365 double t0 = 0, t1 = 0;
366 detail::fluid_qsys_horizon(sn, opt, t0, t1);
368 rf.lambda, serviceCcdf, ES, linspaceT(t0, t1, 200),
369 -std::numeric_limits<double>::infinity(), ES2);
370 // An infinite-server station serves everything that arrives, so the
371 // throughput is the arrival rate and the busy-server count is what a
372 // utilization column can carry.
373 transient(r.times, r.meanNumber, r.meanNumber, r.arrivalRate, r.arrivalRate);
374 } else if (m == "mol") {
375 requireFiniteServers();
377 double t0 = 0, t1 = 0;
378 detail::fluid_qsys_horizon(sn, opt, t0, t1);
379 const double cap = sn.cap[qi];
380 // A finite buffer beyond the servers is not part of the loss model the
381 // approximation is for; only s servers and no waiting room is.
382 const bool useDelay = std::isfinite(cap) && cap > nserv;
384 rf.lambda, serviceCcdf, ES, static_cast<unsigned>(std::llround(nserv)),
385 linspaceT(t0, t1, 200), -std::numeric_limits<double>::infinity(), useDelay);
386 std::vector<T> util(r.meanBusyMOL.size());
387 std::vector<T> served(r.meanBusyMOL.size());
388 const T sT = num_traits<T>::from_double(nserv);
389 for (std::size_t i = 0; i < r.meanBusyMOL.size(); ++i) {
390 util[i] = T(r.meanBusyMOL[i] / sT);
391 served[i] = T(mu * r.meanBusyMOL[i]);
392 }
393 transient(r.times, r.meanBusyMOL, util, served, r.arrivalRate);
394 } else {
395 throw UnsupportedError("solver_fluid_qsys: the '" + m +
396 "' method is not a single-station fluid limit");
397 }
398 out.method = m;
399 return out;
400 } // if constexpr has_transcendental
401}
402
403} // namespace fluid
404} // namespace line
405
406#endif // LINE_SOLVERS_FLUID_FLUID_QSYS_H
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
Cumulative distribution of the inter-arrival time of a MAP.
PatienceHandles< T > sn_patience_handles(const qn::NetworkStruct< T > &sn, std::size_t ist, std::size_t r)
Build the patience handles of station ist (0-based), class r.
ArrivalRateFun< T > sn_arrival_rate_fun(const qn::NetworkStruct< T > &sn, std::size_t ist, std::size_t r)
Build lambda(t) for station ist (0-based), class r.
std::string fluid_qsys_horizon_supports(const std::string &method, const FluidOptions &opt)
The horizon rule the time-varying limits impose, as a public predicate a REPORT can ask: empty when m...
Definition fluid_qsys.h:143
bool fluid_is_time_varying_limit(const std::string &m)
The three single-station limits that report a TRAJECTORY rather than a stationary point,...
Definition fluid_qsys.h:128
FluidSolution solver_fluid_qsys(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, std::vector< FluidTranPoint > *traj=nullptr)
Solve a single-station model with one of the closed-form fluid limits.
Definition fluid_qsys.h:165
mam::Map< T > dist_to_map(const Distrib< T > &d)
std::vector< T > map_cdf(const Map< T > &m, const std::vector< T > &points)
Cumulative distribution of the inter-arrival time at the given points.
Definition map_cdf.h:63
QsysTvFluidResult< Tv > qsys_gtmtst_fluid(const std::function< Tv(const Tv &)> &lambdaFun, const std::function< Tv(const Tv &)> &sFun, const std::function< Tv(const Tv &)> &muFun, const std::function< Tv(const Tv &)> &patienceCcdf, const Tv &T, const TvFluidOptions< Tv > &opts=TvFluidOptions< Tv >())
The Gt/Mt/st+GI many-server fluid queue, and the network of them.
QsysFluidAbandonResult< T > qsys_ggisgi_fluid(const T &lambda, const T &mu, unsigned s, const std::function< T(const T &)> &patienceCcdf, const std::function< T(const T &)> &servingCcdf=std::function< T(const T &)>(), const std::vector< T > &agePoints=std::vector< T >(), double tol=1e-12, double maxTime=std::numeric_limits< double >::quiet_NaN())
Steady state of the G/GI/s+GI fluid model.
QsysMtginfResult< T > qsys_mtginf(const std::function< T(const T &)> &lambdaFun, const std::function< T(const T &)> &serviceCcdf, const T &ES, const std::vector< T > &tvals, double startTime=-std::numeric_limits< double >::infinity(), double ES2=std::numeric_limits< double >::quiet_NaN(), const std::function< T(const T &)> &servicePdf=std::function< T(const T &)>(), double tol=1e-12, std::size_t panels=4000, double maxAge=1e12)
Exact time-varying analysis of the Mt/G/infinity queue.
QsysMolResult< T > qsys_mtgs0_mol(const std::function< T(const T &)> &lambdaFun, const std::function< T(const T &)> &serviceCcdf, const T &ES, unsigned s, const std::vector< T > &tvals, double startTime=-std::numeric_limits< double >::infinity(), bool delay=false)
Modified-offered-load and pointwise-stationary approximations for a time-varying multiserver system.
QsysTgaResult< T > qsys_ggingi_tga(const T &lambda, const T &mu, unsigned n, const T &ca, const T &cs, const std::function< T(const T &)> &patienceCcdf, const std::function< T(const T &)> &patiencePdf=std::function< T(const T &)>(), const std::function< T(const T &)> &serviceCcdf=std::function< T(const T &)>())
Truncated Gaussian approximation (TGA-G) for the G/GI/n+GI queue.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
Truncated Gaussian approximation (TGA-G) for the G/GI/n+GI queue.
Steady state of the G/GI/s+GI fluid model.
The Gt/Mt/st+GI many-server fluid queue, and the network of them.
Exact time-varying analysis of the Mt/G/infinity queue.
Modified-offered-load and pointwise-stationary approximations for a time-varying multiserver system.
Port of matlab/src/api/sn/sn_arrival_rate_fun.m.
Port of matlab/src/api/sn/sn_patience_handles.m.
SolverFluid: the closing method, a port of solver_fluid.m, solver_fluid_iteration....
lambda(t), with whether it actually varies and the cycle length.
std::function< T(const T &)> lambda
the rate as a function of time
The patience law of one station-class pair, in the forms the solvers consume.
std::function< T(const T &)> pdf
The patience density.
std::function< T(const T &)> ccdf
F^c(t) = P(patience > t).
bool present
Whether the pair declares reneging at all; everything below is unset when false.
Controls, defaulting to SolverOptions('Fluid') in the reference.
What the analyzer returns, in the same shape as the MVA solver's result.
std::vector< double > XN
std::vector< double > CN
One point of a transient trajectory: the metrics at time t.
bool has_map() const
True when the type carries a (D0,D1) pair of its own.
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Steady state of the G/GI/s+GI fluid model.
MOL and PSA measures of a time-varying multiserver system.
std::vector< T > times
the evaluation times
std::vector< T > meanBusyMOL
carried load m(t)(1-B), or min(m,s) for the delay model
std::vector< T > arrivalRate
lambda(t)
Time-varying measures of the Mt/G/infinity queue.
Definition qsys_mtginf.h:57
std::vector< T > meanNumber
m(t), the Poisson mean
Definition qsys_mtginf.h:59
std::vector< T > times
the evaluation times
Definition qsys_mtginf.h:58
std::vector< T > arrivalRate
lambda(t)
Definition qsys_mtginf.h:61
Steady-state measures of the G/GI/n+GI truncated Gaussian approximation.
T meanNumber
E[X] = E[B] + E[Q].
T probAbandon
P(patience < wait).
Trajectory of the Gt/Mt/st+GI fluid queue; every vector is on the time grid.
std::vector< T > utilization
B/s.
std::vector< T > arrivalRate
lambda on the grid
std::vector< T > B
fluid in service
std::vector< T > times
the time grid
Options of qsys_gtmtst_fluid, all with the MATLAB defaults.
std::function< T(const T &)> pdf
patience density; differenced when empty