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.fluid` stationary point of the G/GI/s+GI fluid model (Liu and Whitt,
25 * Operations Research 60(5), 2012)
26 * `ggingi.tga` 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 `fluid.`, so the aliases `fluid.ggisgi` and `fluid.tga`
67 * arrive here as the bare `ggisgi` and `tga` while the primary names
68 * `ggisgi.fluid` and `ggingi.tga` pass through untouched. All four are listed;
69 * `fluid_qsys_canonical` maps them onto the two primary names, which is what
70 * the analyzer switches on and what `FluidSolution::method` reports.
71 */
72inline bool fluid_qsys_handles(const std::string& m) {
73 return m == "ggisgi.fluid" || m == "ggisgi" || m == "ggingi.tga" || m == "tga" ||
74 m == "tvms" || m == "mtginf" || m == "mol";
75}
76
77/** The primary name of an alias. */
78inline std::string fluid_qsys_canonical(const std::string& m) {
79 if (m == "ggisgi") return "ggisgi.fluid";
80 if (m == "tga") return "ggingi.tga";
81 return m;
82}
83
84/**
85 * The integration window of a time-varying single-station limit, asked as a
86 * predicate rather than thrown.
87 *
88 * A horizon is a solver OPTION and not a model feature, so the feature registry
89 * has no name for it and a report has to ask this predicate directly.
90 * `fluid_qsys_horizon` asks the same one on the solve path, which is what keeps
91 * the report and the run from disagreeing about whether a method can be asked
92 * for.
93 *
94 * @param opt the fluid knobs, read for the end of the horizon
95 * @return an empty string when the window is a finite non-empty interval
96 */
97inline std::string fluid_qsys_horizon_reason(const FluidOptions& opt) {
98 const double t1 = opt.timespan_end;
99 if (!std::isfinite(t1) || t1 <= 0.0)
100 return "solver_fluid_qsys: a time-varying fluid method needs a finite horizon; set "
101 "options.timespan_end";
102 return std::string();
103}
104
105/**
106 * The integration window, [0, timespan_end].
107 *
108 * The C++ FluidOptions carries only the END of the horizon, where MATLAB and
109 * Python carry a pair; the start is 0 for every fluid route in this port, which
110 * is also what `runAnalyzer.m` forces when the start is not finite. A
111 * non-positive or infinite end has no trajectory to report, and rather than
112 * substituting `fluid_default_horizon`'s 30/min_rate -- a stationary-model rule
113 * of thumb that says nothing about the PERIOD of a time-varying arrival -- the
114 * caller is asked for one.
115 */
116template <class T>
117void fluid_qsys_horizon(const qn::NetworkStruct<T>&, const FluidOptions& opt, double& t0,
118 double& t1) {
119 const std::string reason = fluid_qsys_horizon_reason(opt);
120 if (!reason.empty()) throw UnsupportedError(reason);
121 t0 = 0.0;
122 t1 = opt.timespan_end;
123}
124
125} // namespace detail
126
127/**
128 * The three single-station limits that report a TRAJECTORY rather than a
129 * stationary point, and so need a finite horizon; `ggisgi` and `tga` are
130 * stationary and are not among them.
131 *
132 * @param m the method name, already unqualified
133 * @return true when the method integrates over a finite horizon
134 */
135inline bool fluid_is_time_varying_limit(const std::string& m) {
136 return m == "tvms" || m == "mtginf" || m == "mol";
137}
138
139/**
140 * The horizon rule the time-varying limits impose, as a public predicate a
141 * REPORT can ask: empty when `method` may be asked for with these options.
142 *
143 * `solver_fluid_qsys` refuses through the same body, so a pair the report
144 * offers is a pair the limit runs.
145 *
146 * @param method the method name, qualified or not
147 * @param opt the fluid knobs, read for the end of the horizon
148 * @return an empty string when the method may run, else the refusal
149 */
150inline std::string fluid_qsys_horizon_supports(const std::string& method,
151 const FluidOptions& opt) {
152 // The `fluid.` prefix is stripped here rather than through
153 // `detail::fluid_unqualify`, which lives in fluid_runner.h: that header
154 // includes this one, so the dependency only goes the one way.
155 const std::string m = (method.size() > 6 && method.compare(0, 6, "fluid.") == 0)
156 ? method.substr(6)
157 : method;
158 if (!fluid_is_time_varying_limit(m)) return std::string();
159 const std::string reason = detail::fluid_qsys_horizon_reason(opt);
160 if (reason.empty()) return std::string();
161 return "the '" + method + "' method reports a trajectory. " + reason;
162}
163
164/**
165 * Solve a single-station model with one of the closed-form fluid limits.
166 *
167 * The steady-state row of a TIME-VARYING model is the TIME AVERAGE over the
168 * horizon, which is what a stationary reader of a periodic system measures; the
169 * trajectory itself is available from `solver_fluid_qsys_transient`.
170 */
171template <class T>
173 std::vector<FluidTranPoint>* traj = nullptr) {
174 if constexpr (!num_traits<T>::has_transcendental) {
175 throw UnsupportedError(
176 "solver_fluid_qsys: the single-station fluid limits integrate and bisect, so they need "
177 "transcendental arithmetic; rerun with --arith double or --arith real");
178 } else {
179 const std::size_t M = sn.nstations;
180 const std::size_t K = sn.nclasses;
181 FluidSolution out;
182 out.QN = Matrix<double>(M, K, 0.0);
183 out.UN = Matrix<double>(M, K, 0.0);
184 out.RN = Matrix<double>(M, K, 0.0);
185 out.TN = Matrix<double>(M, K, 0.0);
186 out.CN.assign(K, 0.0);
187 out.XN.assign(K, 0.0);
188 out.iters = 1;
189
190 std::size_t src = 0;
191 std::size_t qi = 0;
192 bool haveSrc = false;
193 bool haveQ = false;
194 for (std::size_t i = 0; i < sn.nof_nodes(); ++i) {
195 if (sn.nodes[i].nodetype == qn::NodeType::Source) {
196 src = sn.nodes[i].station - 1;
197 haveSrc = true;
198 } else if (sn.nodes[i].nodetype == qn::NodeType::Queue ||
199 sn.nodes[i].nodetype == qn::NodeType::Delay) {
200 qi = sn.nodes[i].station - 1;
201 haveQ = true;
202 }
203 }
204 // THE SHAPE THESE LIMITS ARE STATED FOR, refused by name rather than
205 // answered on a model they do not describe: one open class through one
206 // queueing station. The MVA qsys analyzer is reached by a structural
207 // dispatch that guarantees it; these methods are selected by NAME, so the
208 // check has to live here.
209 if (!haveSrc || !haveQ)
210 throw UnsupportedError(
211 "solver_fluid_qsys: the single-station fluid limits need a Source and a queueing "
212 "station");
213 if (K != 1 || sn.nclosedjobs() > 0)
214 throw UnsupportedError("solver_fluid_qsys: the '" + opt.method +
215 "' method is a single-station limit: it needs one open class "
216 "through one Source and one queueing station");
217
218 const std::size_t qstateful = sn.stateful_of_station(qi + 1);
219 const T Vq = sn.visits[0](qstateful - 1, 0);
220 const T lambda = T(sn.rates(src, 0) * Vq);
221 const T mu = sn.rates(qi, 0);
222 const double nserv = sn.stations[qi].nservers;
223 const T scvS = sn.scv(qi, 0);
224 const T ca = num_traits<T>::from_double(std::sqrt(num_traits<T>::to_double(sn.scv(src, 0))));
225 const T cs = num_traits<T>::from_double(std::sqrt(num_traits<T>::to_double(scvS)));
227
228 // The service ccdf, needed by the two Mt/G methods: they are exact in the
229 // service DISTRIBUTION, not in its mean, which is the whole point of the
230 // Eick-Massey-Whitt lag.
231 const lang::Distrib<T>& svc = sn.service[qi][0];
232 std::function<T(const T&)> serviceCcdf;
233 if (svc.has_map()) {
234 const mam::Map<T> sm = lang::dist_to_map(svc);
235 const T one = num_traits<T>::from_int(1);
236 serviceCcdf = [sm, one](const T& x) {
237 std::vector<T> pts(1, x);
238 return T(one - mam::map_cdf(sm, pts)[0]);
239 };
240 } else {
241 serviceCcdf = [mu](const T& x) { return qsys::detail::num_exp(T(-mu * x)); };
242 }
243 const T ES = T(num_traits<T>::from_int(1) / mu);
244 const double ES2 =
247
248 const std::string m = detail::fluid_qsys_canonical(opt.method);
249 const double VqD = num_traits<T>::to_double(Vq);
250 const double lamD = num_traits<T>::to_double(lambda);
251 const double muD = num_traits<T>::to_double(mu);
252
253 // Little's law on the CARRIED rate, as every LINE solver reports a station
254 // that loses work.
255 auto stationary = [&](double Lsys, double Tq, double Uq) {
256 const double R = Tq > 0 ? Lsys / Tq : 0.0;
257 out.RN(qi, 0) = R;
258 out.QN(qi, 0) = Lsys;
259 out.UN(qi, 0) = Uq;
260 out.TN(qi, 0) = Tq;
261 out.TN(src, 0) = lamD / VqD;
262 out.XN[0] = Tq;
263 out.CN[0] = R * VqD;
264 };
265
266 auto transient = [&](const std::vector<T>& t, const std::vector<T>& Lt,
267 const std::vector<T>& Ut, const std::vector<T>& Tt,
268 const std::vector<T>& arrival) {
269 const std::size_t n = t.size();
270 std::vector<double> td(n), Ld(n), Ud(n), Td(n), Ad(n);
271 for (std::size_t i = 0; i < n; ++i) {
272 td[i] = num_traits<T>::to_double(t[i]);
273 Ld[i] = num_traits<T>::to_double(Lt[i]);
274 Ud[i] = num_traits<T>::to_double(Ut[i]);
275 Td[i] = num_traits<T>::to_double(Tt[i]);
276 Ad[i] = num_traits<T>::to_double(arrival[i]);
277 }
278 auto trapz = [&](const std::vector<double>& y) {
279 double s = 0;
280 for (std::size_t i = 1; i < n; ++i) s += 0.5 * (y[i] + y[i - 1]) * (td[i] - td[i - 1]);
281 return s;
282 };
283 const double span = td[n - 1] - td[0];
284 const double Lbar = span > 0 ? trapz(Ld) / span : Ld[0];
285 const double Ubar = span > 0 ? trapz(Ud) / span : Ud[0];
286 const double Tbar = span > 0 ? trapz(Td) / span : Td[0];
287 const double Abar = span > 0 ? trapz(Ad) / span : Ad[0];
288 out.QN(qi, 0) = Lbar;
289 out.UN(qi, 0) = Ubar;
290 out.TN(qi, 0) = Tbar;
291 out.TN(src, 0) = Abar / VqD;
292 out.RN(qi, 0) = Tbar > 0 ? Lbar / Tbar : 0.0;
293 out.XN[0] = Tbar;
294 out.CN[0] = out.RN(qi, 0) * VqD;
295 if (traj != nullptr) {
296 traj->clear();
297 traj->reserve(n);
298 for (std::size_t i = 0; i < n; ++i) {
300 p.t = td[i];
301 p.QN = Matrix<double>(M, K, 0.0);
302 p.UN = Matrix<double>(M, K, 0.0);
303 p.TN = Matrix<double>(M, K, 0.0);
304 p.QN(qi, 0) = Ld[i];
305 p.UN(qi, 0) = Ud[i];
306 p.TN(qi, 0) = Td[i];
307 p.TN(src, 0) = Ad[i] / VqD;
308 traj->push_back(p);
309 }
310 }
311 };
312
313 auto requirePatience = [&]() {
314 if (!h.present)
315 throw UnsupportedError("solver_fluid_qsys: the '" + m +
316 "' method needs a reneging patience law on the queue "
317 "(Queue.setPatience)");
318 };
319 auto requireFiniteServers = [&]() {
320 if (!std::isfinite(nserv) || nserv < 1)
321 throw UnsupportedError("solver_fluid_qsys: the '" + m +
322 "' method needs a finite number of servers");
323 };
324 auto linspaceT = [](double a, double b, std::size_t n) {
325 std::vector<T> v(n);
326 for (std::size_t i = 0; i < n; ++i)
327 v[i] = num_traits<T>::from_double(a + (b - a) * static_cast<double>(i) /
328 static_cast<double>(n - 1));
329 return v;
330 };
331
332 if (m == "ggisgi.fluid") {
333 requirePatience();
335 lambda, mu, static_cast<unsigned>(std::llround(nserv)), h.ccdf);
338 } else if (m == "ggingi.tga") {
339 requirePatience();
340 requireFiniteServers();
342 lambda, mu, static_cast<unsigned>(std::llround(nserv)), ca, cs, h.ccdf, h.pdf,
343 serviceCcdf);
344 const double pa = num_traits<T>::to_double(r.probAbandon);
345 stationary(num_traits<T>::to_double(r.meanNumber), lamD * (1.0 - pa),
346 std::min(num_traits<T>::to_double(r.meanNumberInService) / nserv, 1.0));
347 } else if (m == "tvms") {
348 requirePatience();
349 requireFiniteServers();
351 double t0 = 0, t1 = 0;
352 detail::fluid_qsys_horizon(sn, opt, t0, t1);
353 // CONSTANT STAFFING. Nothing in a Network declares a time-varying server
354 // count, so s(t) is the station's own s; the time variation the method
355 // is for enters through lambda(t) alone. A staffing schedule would need
356 // a model feature that does not exist, and inventing one here would make
357 // the solver answer a model the user did not build.
358 const T sT = num_traits<T>::from_double(nserv);
360 tvopt.pdf = h.pdf;
362 rf.lambda, [sT](const T&) { return sT; }, [mu](const T&) { return mu; }, h.ccdf,
363 num_traits<T>::from_double(t1 - t0), tvopt);
364 std::vector<T> times = r.times;
365 for (std::size_t i = 0; i < times.size(); ++i)
366 times[i] = T(times[i] + num_traits<T>::from_double(t0));
367 std::vector<T> served(r.B.size());
368 for (std::size_t i = 0; i < r.B.size(); ++i) served[i] = T(mu * r.B[i]);
369 transient(times, r.X, r.utilization, served, r.arrivalRate);
370 } else if (m == "mtginf") {
372 double t0 = 0, t1 = 0;
373 detail::fluid_qsys_horizon(sn, opt, t0, t1);
375 rf.lambda, serviceCcdf, ES, linspaceT(t0, t1, 200),
376 -std::numeric_limits<double>::infinity(), ES2);
377 // An infinite-server station serves everything that arrives, so the
378 // throughput is the arrival rate and the busy-server count is what a
379 // utilization column can carry.
380 transient(r.times, r.meanNumber, r.meanNumber, r.arrivalRate, r.arrivalRate);
381 } else if (m == "mol") {
382 requireFiniteServers();
384 double t0 = 0, t1 = 0;
385 detail::fluid_qsys_horizon(sn, opt, t0, t1);
386 const double cap = sn.cap[qi];
387 // A finite buffer beyond the servers is not part of the loss model the
388 // approximation is for; only s servers and no waiting room is.
389 const bool useDelay = std::isfinite(cap) && cap > nserv;
391 rf.lambda, serviceCcdf, ES, static_cast<unsigned>(std::llround(nserv)),
392 linspaceT(t0, t1, 200), -std::numeric_limits<double>::infinity(), useDelay);
393 std::vector<T> util(r.meanBusyMOL.size());
394 std::vector<T> served(r.meanBusyMOL.size());
395 const T sT = num_traits<T>::from_double(nserv);
396 for (std::size_t i = 0; i < r.meanBusyMOL.size(); ++i) {
397 util[i] = T(r.meanBusyMOL[i] / sT);
398 served[i] = T(mu * r.meanBusyMOL[i]);
399 }
400 transient(r.times, r.meanBusyMOL, util, served, r.arrivalRate);
401 } else {
402 throw UnsupportedError("solver_fluid_qsys: the '" + m +
403 "' method is not a single-station fluid limit");
404 }
405 out.method = m;
406 return out;
407 } // if constexpr has_transcendental
408}
409
410} // namespace fluid
411} // namespace line
412
413#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:150
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:135
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:172
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.
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