LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fluid_ddpp.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_DDPP_H
6#define LINE_SOLVERS_FLUID_FLUID_DDPP_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Refined mean-field (O(1/N)) steady state of the matrix fluid drift,
12 * `method = "ddpp"` (alias `fld.ddpp`). Port of `solver_fluid_ddpp.m` and
13 * `fluid_ddpp_admits.m`.
14 *
15 * The model is read as a density-dependent population process whose size N is
16 * the total closed population: the state is the job count of every (station,
17 * class, phase) coordinate, a jump moving a job from a to b has rate
18 * theta_a(x) * W(a,b), with theta_a = x_a * min(1, c_i / X_i) (x_a at an
19 * infinite server). The family scales every population and finite server count
20 * together, so the fixed point x* and the correction V are N-independent in job
21 * counts. Gast and Van Houdt (POMACS 1(2), 2017) give E[X] = x* + V + O(1/N),
22 * with J W + W J' + Q = 0 and V = -J^{-1} (1/2) H:W, both solved on an
23 * orthonormal basis of the jump span (Allmeier and Gast, PER 49(4), 2022). The
24 * rates are refined to the same order, so the corrected throughputs satisfy flow
25 * balance exactly.
26 *
27 * The correction is nonzero only at a saturated MULTICLASS PS/FCFS share: a
28 * single-class network gives V = 0 (piecewise-linear drift for exponential
29 * service, vanishing Hessian contraction for phase-type service, so the result
30 * is insensitive beyond the mean). The fixed point is found by the same
31 * pseudo-transient continuation as the MATLAB, JAR and native Python ports.
32 */
33
34#include <algorithm>
35#include <chrono>
36#include <cmath>
37#include <complex>
38#include <cstddef>
39#include <limits>
40#include <string>
41#include <vector>
42
49#include "line/util/eig.h"
50#include "line/util/error.h"
51#include "line/util/linalg.h"
52#include "line/util/lu.h"
53#include "line/util/matrix.h"
54#include "line/util/sylvester.h"
55
56namespace line {
57namespace fluid {
58
59/** The verdict of `fluid_ddpp_admits`: ok, or the refusal and its identifier. */
61 bool ok = false;
62 std::string category; ///< "unsupported" or "numerical" when refused
63 std::string kind; ///< the LINE:<category>:<kind> kind
64 std::string reason;
65
66 /** Throws the refusal as the error class its category names. */
67 [[noreturn]] void raise() const {
68 if (category == "numerical") throw NumericError(ErrorKind{kind}, "solver_fluid_ddpp: " + reason);
69 throw UnsupportedError(ErrorKind{kind}, "solver_fluid_ddpp: " + reason);
70 }
71};
72
73namespace detail {
74
75template <class T>
76bool fluid_ddpp_active(const qn::NetworkStruct<T>& sn, std::size_t i, std::size_t r) {
77 if (sn.stations[i].sched == lang::SchedStrategy::EXT) return false;
78 return fluid_service_defined(sn, i, r);
79}
80
81inline std::vector<double> fluid_ddpp_theta(const std::vector<double>& x,
82 const std::vector<std::vector<std::size_t>>& members,
83 const std::vector<double>& c) {
84 std::vector<double> th = x;
85 for (std::size_t i = 0; i < members.size(); ++i) {
86 if (members[i].empty() || !std::isfinite(c[i])) continue;
87 double X = 0.0;
88 for (std::size_t a : members[i]) X += x[a];
89 if (X > c[i])
90 for (std::size_t a : members[i]) th[a] = c[i] * x[a] / X;
91 }
92 return th;
93}
94
95inline Matrix<double> fluid_ddpp_dtheta(const std::vector<double>& x,
96 const std::vector<std::vector<std::size_t>>& members,
97 const std::vector<double>& c) {
98 Matrix<double> D = eye<double>(x.size());
99 for (std::size_t i = 0; i < members.size(); ++i) {
100 if (members[i].empty() || !std::isfinite(c[i])) continue;
101 double X = 0.0;
102 for (std::size_t a : members[i]) X += x[a];
103 if (X > c[i])
104 for (std::size_t a : members[i])
105 for (std::size_t b : members[i])
106 D(a, b) = c[i] * ((a == b ? 1.0 / X : 0.0) - x[a] / (X * X));
107 }
108 return D;
109}
110
111/** (1/2) sum_{jk} d2theta_a/dx_j dx_k Sigma_jk, nonzero at saturated stations only. */
112inline std::vector<double> fluid_ddpp_hesscontract(const std::vector<double>& x,
113 const std::vector<std::vector<std::size_t>>& members,
114 const std::vector<double>& c,
115 const Matrix<double>& Sigma) {
116 std::vector<double> bh(x.size(), 0.0);
117 for (std::size_t i = 0; i < members.size(); ++i) {
118 if (members[i].empty() || !std::isfinite(c[i])) continue;
119 double X = 0.0;
120 for (std::size_t a : members[i]) X += x[a];
121 if (!(X > c[i])) continue;
122 std::vector<double> row(members[i].size(), 0.0);
123 double tot = 0.0;
124 for (std::size_t p = 0; p < members[i].size(); ++p) {
125 for (std::size_t b : members[i]) row[p] += Sigma(members[i][p], b);
126 tot += row[p];
127 }
128 for (std::size_t p = 0; p < members[i].size(); ++p) {
129 const std::size_t a = members[i][p];
130 bh[a] = c[i] * (-row[p] / (X * X) + x[a] * tot / (X * X * X));
131 }
132 }
133 return bh;
134}
135
136/** Largest step in (0,1] that keeps the state nonnegative, backed off by 0.9. */
137inline double fluid_ddpp_steplimit(const std::vector<double>& x, const std::vector<double>& dx) {
138 double m = std::numeric_limits<double>::infinity();
139 for (std::size_t a = 0; a < x.size(); ++a)
140 if (dx[a] < 0.0 && x[a] + dx[a] < 0.0) m = std::min(m, -x[a] / dx[a]);
141 return std::isfinite(m) ? std::min(1.0, 0.9 * m) : 1.0;
142}
143
144inline std::vector<double> fluid_ddpp_solve(const Matrix<double>& A, std::vector<double> b) {
145 Matrix<double> LU = A;
146 const std::vector<std::size_t> piv = lu_factor(LU);
147 lu_solve(LU, piv, b);
148 return b;
149}
150
151inline double fluid_ddpp_norminf(const std::vector<double>& v) {
152 double m = 0.0;
153 for (double z : v) m = std::max(m, std::fabs(z));
154 return m;
155}
156
157inline double fluid_ddpp_norm2(const std::vector<double>& v) {
158 double s = 0.0;
159 for (double z : v) s += z * z;
160 return std::sqrt(s);
161}
162
163/** 1-norm reciprocal condition estimate by explicit inverse; 0 when singular. */
164inline double fluid_ddpp_rcond(const Matrix<double>& A) {
165 const auto norm1 = [](const Matrix<double>& B) {
166 double m = 0.0;
167 for (std::size_t j = 0; j < B.cols(); ++j) {
168 double s = 0.0;
169 for (std::size_t i = 0; i < B.rows(); ++i) s += std::fabs(B(i, j));
170 m = std::max(m, s);
171 }
172 return m;
173 };
174 try {
175 const Matrix<double> Ai = inverse(A);
176 const double na = norm1(A), ni = norm1(Ai);
177 if (!std::isfinite(ni) || na == 0.0) return 0.0;
178 return 1.0 / (na * ni);
179 } catch (const Error&) {
180 return 0.0;
181 }
182}
183
184} // namespace detail
185
186/**
187 * Port of `fluid_ddpp_admits.m`: the model class of 'ddpp', decided before the
188 * fixed point is solved. Closed classes only, INF/PS/FCFS stations (FCFS with
189 * exponential service at one rate for every class), renewal PH service, steady
190 * state, no load dependence, and the moment_maxstate cap.
191 */
192template <class T>
195 const auto refuse = [&](const char* cat, const std::string& kind, const std::string& why) {
196 v.ok = false;
197 v.category = cat;
198 v.kind = kind;
199 v.reason = why;
200 return v;
201 };
202 for (const auto& nd : sn.nodes)
203 if (nd.nodetype == qn::NodeType::Transition || nd.nodetype == qn::NodeType::Place)
204 return refuse("unsupported", "Transition",
205 "the 'ddpp' method has no drift for a stochastic Petri net. Use method 'dae'");
206 for (const auto& nd : sn.nodes)
207 if (nd.nodetype == qn::NodeType::Cache)
208 return refuse("unsupported", "Cache",
209 "the 'ddpp' method does not support caching stations. Use method 'rmf'");
210 for (const auto& nd : sn.nodes)
211 if (nd.nodetype == qn::NodeType::Fork || nd.nodetype == qn::NodeType::Join)
212 return refuse("unsupported", "Fork", "the 'ddpp' method does not support fork-join models");
213 if (sn.has_open_classes())
214 return refuse("unsupported", "OpenClass",
215 "the 'ddpp' method supports closed models only: its system size N is the total "
216 "closed population. Use method 'minnormal' for open or mixed models");
217 if (std::isfinite(opt.timespan_end))
218 return refuse("unsupported", "Analysis",
219 "the 'ddpp' method corrects the STATIONARY mean only, so it has no transient. "
220 "Leave timespan_end infinite, or use method 'matrix' for a trajectory");
221 if (detail::fluid_has_time_varying_rates(opt))
222 return refuse("unsupported", "Analysis",
223 "the 'ddpp' method needs an autonomous drift, but a rate schedule makes the "
224 "rates time-varying");
225 const std::size_t M = sn.nstations, K = sn.nclasses;
226 for (std::size_t i = 0; i < M; ++i)
227 for (const T& s : sn.stations[i].lldscaling)
228 if (std::fabs(num_traits<T>::to_double(s) - 1.0) > 1e-14)
229 return refuse("unsupported", "LoadDependence",
230 "the 'ddpp' method does not evaluate load dependence. Use method 'closing'");
231 std::size_t nstate = 0;
232 for (std::size_t i = 0; i < M; ++i) {
233 const lang::SchedStrategy sched = sn.stations[i].sched;
234 if (!(sched == lang::SchedStrategy::INF || sched == lang::SchedStrategy::PS ||
236 return refuse("unsupported", qn::feature_name(qn::feature_of_sched(sched)),
237 "the 'ddpp' method supports INF, PS and FCFS stations only, and station " +
238 std::to_string(i + 1) + " uses " + lang::sched_to_text(sched));
239 double fcfsRate = std::numeric_limits<double>::quiet_NaN();
240 for (std::size_t r = 0; r < K; ++r) {
241 if (!detail::fluid_service_defined(sn, i, r)) continue;
242 const lang::Distrib<T>& d = sn.service[i][r];
243 const lang::ProcessType pt = d.type;
247 return refuse("unsupported", "Distribution",
248 "the 'ddpp' method needs a renewal phase-type service, and class " +
249 std::to_string(r + 1) + " at station " + std::to_string(i + 1) +
250 " has a " + lang::process_to_text(pt) + " process");
251 const std::size_t nph = d.D0.rows();
252 nstate += nph;
253 if (sched == lang::SchedStrategy::FCFS) {
254 if (nph > 1)
255 return refuse("unsupported", "SchedStrategy_FCFS",
256 "the 'ddpp' method takes an FCFS station only with exponential service, "
257 "where its stationary counts are those of PS, and class " +
258 std::to_string(r + 1) + " at station " + std::to_string(i + 1) +
259 " has a " + std::to_string(nph) + "-phase service");
260 const double rate = -num_traits<T>::to_double(d.D0(0, 0));
261 if (std::isnan(fcfsRate)) {
262 fcfsRate = rate;
263 } else if (std::fabs(rate - fcfsRate) > 1e-10 * std::max(1.0, fcfsRate)) {
264 return refuse("unsupported", "SchedStrategy_FCFS",
265 "the 'ddpp' method takes an FCFS station only with one service rate for "
266 "every class, where its stationary counts are those of PS, and station " +
267 std::to_string(i + 1) + " serves at rates " +
268 std::to_string(fcfsRate) + " and " + std::to_string(rate));
269 }
270 }
271 }
272 }
273 const std::size_t maxstate = opt.moment_maxstate > 0 ? opt.moment_maxstate : 200;
274 if (nstate > maxstate)
275 return refuse("numerical", "StateSpace",
276 "the phase-resolved state has " + std::to_string(nstate) +
277 " coordinates, above the moment_maxstate limit of " + std::to_string(maxstate) +
278 " that bounds the Lyapunov solve");
279 v.ok = true;
280 return v;
281}
282
283/** What the expansion found beside the refined table: the fluid point and V. */
285 std::vector<double> x; ///< fluid fixed point x*
286 std::vector<double> V; ///< the O(1/N) correction, in job counts
288 double N = 0.0; ///< total closed population
289 double residual = 0.0;
290};
291
292/**
293 * Port of `solver_fluid_ddpp.m`. Returns the refined QN, UN, RN, TN (M x K),
294 * with `xvec` = x* + V. `report`, when given, receives the fluid point and V.
295 * With `probe` it stops once the fixed point has passed the kink and stability
296 * checks and returns an empty solution (see `fluid_ddpp_hyperbolic`).
297 */
298template <class T>
300 FluidDdppReport* report = nullptr, bool probe = false) {
302 if (!adm.ok) adm.raise();
303 const std::size_t M = sn.nstations, K = sn.nclasses;
304 const double eps = std::numeric_limits<double>::epsilon();
305
306 // state encoding: one coordinate per (station, class, phase)
307 std::vector<std::vector<std::size_t>> start(M, std::vector<std::size_t>(K, 0));
308 std::vector<std::vector<std::size_t>> len(M, std::vector<std::size_t>(K, 0));
309 std::size_t n = 0;
310 for (std::size_t i = 0; i < M; ++i)
311 for (std::size_t r = 0; r < K; ++r) {
312 start[i][r] = n;
313 if (detail::fluid_ddpp_active(sn, i, r)) {
314 len[i][r] = sn.service[i][r].D0.rows();
315 n += len[i][r];
316 }
317 }
318 std::vector<std::size_t> st(n), cl(n);
319 std::vector<double> exitRate(n, 0.0);
320 std::vector<std::vector<std::vector<double>>> pie(M, std::vector<std::vector<double>>(K));
321 for (std::size_t i = 0; i < M; ++i)
322 for (std::size_t r = 0; r < K; ++r) {
323 if (len[i][r] == 0) continue;
324 const lang::Distrib<T>& d = sn.service[i][r];
325 pie[i][r] = detail::fluid_pie(d);
326 for (std::size_t k = 0; k < len[i][r]; ++k) {
327 const std::size_t a = start[i][r] + k;
328 st[a] = i;
329 cl[a] = r;
330 double s = 0.0;
331 for (std::size_t kk = 0; kk < d.D1.cols(); ++kk) s += num_traits<T>::to_double(d.D1(k, kk));
332 exitRate[a] = s;
333 }
334 }
335
336 // jump rates per unit theta, self-loops dropped; station routing by stochastic complement
337 const std::size_t S = sn.nof_stateful();
338 const bool have_rt = sn.rt.rows() == S * K;
339 Matrix<double> P(M * K, M * K, 0.0);
340 if (have_rt) {
341 std::vector<std::size_t> keep;
342 keep.reserve(M * K);
343 for (std::size_t i = 0; i < M; ++i) {
344 const std::size_t isf = sn.stateful_of_station(i + 1) - 1;
345 for (std::size_t r = 0; r < K; ++r) keep.push_back(isf * K + r);
346 }
347 Matrix<double> rt_full(S * K, S * K, 0.0);
348 for (std::size_t a = 0; a < S * K; ++a)
349 for (std::size_t b = 0; b < S * K; ++b) rt_full(a, b) = num_traits<T>::to_double(sn.rt(a, b));
350 P = mc::dtmc_stochcomp(rt_full, keep);
351 }
352 Matrix<double> Woff(n, n, 0.0);
353 for (std::size_t i = 0; i < M; ++i)
354 for (std::size_t r = 0; r < K; ++r) {
355 if (len[i][r] == 0) continue;
356 const lang::Distrib<T>& d = sn.service[i][r];
357 for (std::size_t k = 0; k < len[i][r]; ++k) {
358 const std::size_t a = start[i][r] + k;
359 for (std::size_t kk = 0; kk < len[i][r]; ++kk) {
360 const double v = num_traits<T>::to_double(d.D0(k, kk));
361 if (kk != k && v > 0.0) Woff(a, start[i][r] + kk) += v;
362 }
363 if (exitRate[a] <= 0.0) continue;
364 for (std::size_t j = 0; j < M; ++j)
365 for (std::size_t s = 0; s < K; ++s) {
366 const double p = P(i * K + r, j * K + s);
367 if (!(p > 0.0) || len[j][s] == 0) continue;
368 for (std::size_t kk = 0; kk < len[j][s]; ++kk) {
369 const std::size_t b = start[j][s] + kk;
370 const double pj = kk < pie[j][s].size() ? pie[j][s][kk] : 0.0;
371 if (b != a && pj > 0.0) Woff(a, b) += exitRate[a] * p * pj;
372 }
373 }
374 }
375 }
376 Matrix<double> WdT(n, n, 0.0); // transpose of Wd = Woff - diag(rowsum)
377 double rateScale = 1e-12;
378 for (std::size_t a = 0; a < n; ++a) {
379 double rs = 0.0;
380 for (std::size_t b = 0; b < n; ++b) {
381 WdT(b, a) = Woff(a, b);
382 rs += Woff(a, b);
383 }
384 WdT(a, a) -= rs;
385 for (std::size_t b = 0; b < n; ++b) rateScale = std::max(rateScale, std::fabs(WdT(b, a)));
386 }
387
388 std::vector<double> c(M);
389 std::vector<std::vector<std::size_t>> members(M);
390 for (std::size_t i = 0; i < M; ++i) c[i] = sn.stations[i].nservers;
391 for (std::size_t a = 0; a < n; ++a) members[st[a]].push_back(a);
392
393 // reduced basis: orthonormal span of the jump directions (column-major find order)
394 std::size_t nj = 0;
395 for (std::size_t b = 0; b < n; ++b)
396 for (std::size_t a = 0; a < n; ++a)
397 if (Woff(a, b) > 0.0) ++nj;
398 Matrix<double> Djump(n, nj, 0.0);
399 {
400 std::size_t e = 0;
401 for (std::size_t b = 0; b < n; ++b)
402 for (std::size_t a = 0; a < n; ++a)
403 if (Woff(a, b) > 0.0) {
404 Djump(a, e) = -1.0;
405 Djump(b, e) += 1.0;
406 ++e;
407 }
408 }
409 const Matrix<double> Pb = detail::fluid_orth(Djump);
410 const std::size_t d = Pb.cols();
411 const Matrix<double> PbT = Pb.transpose();
412
413 // initial state: each class population on its reference station, entry phases
414 std::vector<double> x(n, 0.0);
415 double Ntot = 0.0;
416 const std::vector<double> njobs = sn.njobs();
417 for (std::size_t r = 0; r < K; ++r) {
418 const double Nr = njobs[r];
419 if (!(std::isfinite(Nr) && Nr > 0.0)) continue;
420 const std::size_t ref = sn.classes[r].refstat - 1;
421 if (ref >= M || len[ref][r] == 0)
422 throw InputError(ErrorKind{"Model"},
423 "solver_fluid_ddpp: class " + std::to_string(r + 1) +
424 " has no service process at its reference station, so its population "
425 "has no coordinate to start from");
426 for (std::size_t k = 0; k < len[ref][r]; ++k)
427 x[start[ref][r] + k] += Nr * (k < pie[ref][r].size() ? pie[ref][r][k] : 0.0);
428 Ntot += Nr;
429 }
430
431 // fixed point: pseudo-transient continuation, then Newton on the reduced system
432 const std::size_t iterMax = std::max<std::size_t>(100, opt.iter_max);
433 const double tolF = 1e-12 * rateScale * std::max(1.0, Ntot);
434 double h = 1.0 / rateScale;
435 const double hmax = 1e10 / rateScale;
436 std::vector<double> F = mulvec(WdT, detail::fluid_ddpp_theta(x, members, c));
437 double nF = detail::fluid_ddpp_norminf(F);
438 std::size_t iters = 0;
439 while (nF > tolF && iters < iterMax) {
440 ++iters;
441 Matrix<double> A = matmul(WdT, detail::fluid_ddpp_dtheta(x, members, c));
442 for (std::size_t a = 0; a < n; ++a)
443 for (std::size_t b = 0; b < n; ++b) A(a, b) = -A(a, b);
444 for (std::size_t a = 0; a < n; ++a) A(a, a) += 1.0 / h;
445 const std::vector<double> dx = detail::fluid_ddpp_solve(A, F);
446 const double alpha = detail::fluid_ddpp_steplimit(x, dx);
447 for (std::size_t a = 0; a < n; ++a) x[a] = std::max(0.0, x[a] + alpha * dx[a]);
448 std::vector<double> Fnew = mulvec(WdT, detail::fluid_ddpp_theta(x, members, c));
449 const double nFnew = detail::fluid_ddpp_norminf(Fnew);
450 const double ratio = nF / std::max(nFnew, std::numeric_limits<double>::min());
451 h = std::min(hmax, h * std::min(10.0, std::max(0.5, ratio)));
452 F = Fnew;
453 nF = nFnew;
454 }
455 for (int polish = 0; polish < 3 && d > 0; ++polish) {
456 const Matrix<double> Jr = matmul(matmul(PbT, matmul(WdT, detail::fluid_ddpp_dtheta(x, members, c))), Pb);
457 if (detail::fluid_ddpp_rcond(Jr) < eps) break;
458 const std::vector<double> rdx = detail::fluid_ddpp_solve(Jr, mulvec(PbT, F));
459 std::vector<double> dx = mulvec(Pb, rdx);
460 for (double& z : dx) z = -z;
461 const double alpha = detail::fluid_ddpp_steplimit(x, dx);
462 for (std::size_t a = 0; a < n; ++a) x[a] = std::max(0.0, x[a] + alpha * dx[a]);
463 F = mulvec(WdT, detail::fluid_ddpp_theta(x, members, c));
464 }
465 nF = detail::fluid_ddpp_norminf(F);
466 if (!(nF <= 1e3 * tolF))
467 throw NumericError(ErrorKind{"NonConvergence"},
468 "solver_fluid_ddpp: the fluid fixed point was not found in " +
469 std::to_string(iters) + " continuation steps (drift residual " +
470 std::to_string(nF) + " against a tolerance of " +
471 std::to_string(1e3 * tolF) + ")");
472
473 // the expansion needs a smooth, hyperbolic fixed point
474 for (std::size_t i = 0; i < M; ++i) {
475 if (!std::isfinite(c[i]) || members[i].empty()) continue;
476 double Xi = 0.0;
477 for (std::size_t a : members[i]) Xi += x[a];
478 if (std::fabs(Xi - c[i]) <= 1e-6 * std::max(1.0, c[i]))
480 "solver_fluid_ddpp: the fluid fixed point puts " + std::to_string(Xi) +
481 " jobs at station " + std::to_string(i + 1) + ", which has " + std::to_string(c[i]) +
482 " servers: the drift has a kink there, so the 1/N expansion has no Jacobian to expand "
483 "around. Use method 'minnormal' or an exact solver");
484 }
485 const Matrix<double> Dth = detail::fluid_ddpp_dtheta(x, members, c);
486 const Matrix<double> J = matmul(WdT, Dth);
487 Matrix<double> Jr(0, 0, 0.0);
488 if (d > 0) {
489 Jr = matmul(matmul(PbT, J), Pb);
490 double maxRe = -std::numeric_limits<double>::infinity();
491 for (const std::complex<double>& z : eig_values(Jr)) maxRe = std::max(maxRe, z.real());
492 if (maxRe >= -std::sqrt(eps) * rateScale)
494 "solver_fluid_ddpp: the fluid fixed point is not exponentially stable on the reachable "
495 "subspace (largest Jacobian eigenvalue has real part " + std::to_string(maxRe) +
496 "), so the stationary covariance and the 1/N correction do not exist");
497 }
498 if (probe) return FluidSolution();
499
500 // noise covariance and its Lyapunov solve on the jump span
501 const std::vector<double> th0 = detail::fluid_ddpp_theta(x, members, c);
502 Matrix<double> Qn(n, n, 0.0);
503 for (std::size_t a = 0; a < n; ++a)
504 for (std::size_t b = 0; b < n; ++b) {
505 const double rj = th0[a] * Woff(a, b);
506 if (rj == 0.0) continue;
507 Qn(a, a) += rj;
508 Qn(b, b) += rj;
509 Qn(a, b) -= rj;
510 Qn(b, a) -= rj;
511 }
512 Matrix<double> Sigma(n, n, 0.0);
513 if (d > 0) {
514 Matrix<double> Qr = matmul(matmul(PbT, Qn), Pb);
515 detail::fluid_symmetrize(Qr);
516 Matrix<double> negQr(d, d, 0.0);
517 for (std::size_t a = 0; a < d; ++a)
518 for (std::size_t b = 0; b < d; ++b) negQr(a, b) = -Qr(a, b);
519 Matrix<double> Wr = sylvester_schur(Jr, Jr.transpose(), negQr);
520 detail::fluid_symmetrize(Wr);
521 Sigma = matmul(matmul(Pb, Wr), PbT);
522 detail::fluid_symmetrize(Sigma);
523 }
524
525 // Hessian contraction and the correction
526 const std::vector<double> bh = detail::fluid_ddpp_hesscontract(x, members, c, Sigma);
527 std::vector<double> V(n, 0.0);
528 if (d > 0) {
529 const std::vector<double> rv = detail::fluid_ddpp_solve(Jr, mulvec(PbT, mulvec(WdT, bh)));
530 const std::vector<double> pv = mulvec(Pb, rv);
531 for (std::size_t a = 0; a < n; ++a) V[a] = -pv[a];
532 }
533 const double normV = detail::fluid_ddpp_norm2(V), normX = detail::fluid_ddpp_norm2(x);
534 if (normV > 0.5 * std::max(normX, std::sqrt(eps)))
535 throw NumericError(ErrorKind{"Accuracy"},
536 "solver_fluid_ddpp: the 1/N correction (norm " + std::to_string(normV) +
537 ") is not small against the fluid fixed point (norm " + std::to_string(normX) +
538 "), so the expansion is outside its range of validity at this population");
539 const std::vector<double> dthV = mulvec(Dth, V);
540 std::vector<double> thR(n), xR(n);
541 for (std::size_t a = 0; a < n; ++a) {
542 thR[a] = th0[a] + dthV[a] + bh[a];
543 xR[a] = x[a] + V[a];
544 }
545
546 // metrics
547 const auto metrics = [&](const std::vector<double>& xs, const std::vector<double>& th,
549 QN = Matrix<double>(M, K, 0.0);
550 UN = Matrix<double>(M, K, 0.0);
551 TN = Matrix<double>(M, K, 0.0);
552 for (std::size_t a = 0; a < n; ++a) {
553 QN(st[a], cl[a]) += xs[a];
554 TN(st[a], cl[a]) += exitRate[a] * th[a];
555 if (std::isfinite(c[st[a]])) UN(st[a], cl[a]) += th[a] / c[st[a]];
556 }
557 for (std::size_t i = 0; i < M; ++i)
558 if (sn.stations[i].sched == lang::SchedStrategy::INF)
559 for (std::size_t r = 0; r < K; ++r) UN(i, r) = QN(i, r);
560 };
561 FluidSolution out;
562 metrics(xR, thR, out.QN, out.UN, out.TN);
563 out.RN = Matrix<double>(M, K, 0.0);
564 for (std::size_t i = 0; i < M; ++i)
565 for (std::size_t r = 0; r < K; ++r)
566 if (out.TN(i, r) > 0.0) out.RN(i, r) = out.QN(i, r) / out.TN(i, r);
567 out.xvec = xR;
568 out.iters = iters;
569 out.method = "ddpp";
570 out.XN.assign(K, 0.0);
571 out.CN.assign(K, 0.0);
572 for (std::size_t k = 0; k < K; ++k) {
573 const std::size_t rs = sn.classes[k].refstat;
574 if (rs >= 1 && rs <= M) out.XN[k] = out.TN(rs - 1, k);
575 double q = 0.0;
576 for (std::size_t i = 0; i < M; ++i) q += out.QN(i, k);
577 if (out.XN[k] > 0.0) out.CN[k] = q / out.XN[k];
578 }
579 if (report) {
580 report->x = x;
581 report->V = V;
582 metrics(x, th0, report->QNfluid, report->UNfluid, report->TNfluid);
583 report->N = Ntot;
584 report->residual = nF;
585 }
586 return out;
587}
588
589/**
590 * Does the fluid fixed point admit the 1/N expansion? `fluid_ddpp_admits` is
591 * static, but smoothness and exponential stability of the fixed point are a
592 * property of the numbers: a closed Delay -> FCFS repairman with N = 3, think
593 * rate 1 and service rate 2 puts exactly one job at the single server, on the
594 * kink of min(1, c/X). The support gate asks this after the static predicate,
595 * so findSolver does not offer a run that would refuse. Runs the solver up to
596 * its hyperbolicity checks. Port of `fluid_ddpp_hyperbolic.m`.
597 */
598template <class T>
601 try {
602 solver_fluid_ddpp(sn, opt, nullptr, true);
603 } catch (const FluidNonHyperbolicError& e) {
604 v.category = "numerical";
605 v.kind = "FluidNonHyperbolic";
606 v.reason = e.what();
607 return v;
608 } catch (const NumericError& e) {
609 v.category = "numerical";
610 v.kind = "NonConvergence";
611 v.reason = e.what();
612 return v;
613 }
614 v.ok = true;
615 return v;
616}
617
618} // namespace fluid
619} // namespace line
620
621#endif // LINE_SOLVERS_FLUID_FLUID_DDPP_H
InputError(const std::string &what)
Definition error.h:105
std::size_t cols() const
Definition matrix.h:90
Matrix transpose() const
Definition matrix.h:110
The algorithm cannot produce a trustworthy answer on this instance: LINE:numerical:<kind>.
Definition error.h:110
NumericError(const std::string &what)
Definition error.h:112
UnsupportedError(const std::string &what)
Definition error.h:119
Raised when the moment closure cannot serve this model: the linearization at the fixed point is not h...
FluidNonHyperbolicError(const std::string &what)
A network plus its refreshed NetworkStruct.
Eigenvalues and singular values, backed by LAPACK.
The exception types the port throws.
The language-feature gate: what a MODEL uses against what a SOLVER declares.
The second-order fluid methods: fluid_moment_terms.m, fluid_lyapunov.m, fluid_drift_jacobian....
The one exception the fluid fallback ladder catches.
The fluid drift: a port of solver_fluid_odes.m and the ode_jumps_new / ode_rate_base / ode_rates_clos...
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
LU factorization with partial pivoting, templated on the number type.
Dense matrix and non-owning view.
FluidDdppAdmits fluid_ddpp_admits(const qn::NetworkStruct< T > &sn, const FluidOptions &opt)
Port of fluid_ddpp_admits.m: the model class of 'ddpp', decided before the fixed point is solved.
Definition fluid_ddpp.h:193
FluidDdppAdmits fluid_ddpp_hyperbolic(const qn::NetworkStruct< T > &sn, const FluidOptions &opt)
Does the fluid fixed point admit the 1/N expansion?
Definition fluid_ddpp.h:599
FluidSolution solver_fluid_ddpp(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, FluidDdppReport *report=nullptr, bool probe=false)
Port of solver_fluid_ddpp.m.
Definition fluid_ddpp.h:299
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:188
ProcessType
Distribution kinds, with the values of MATLAB ProcessType.
Definition lang_types.h:518
@ NHPP
The time-INHOMOGENEOUS families of Ko and Pender (ORL 45, 2017): an NHPP is a rate schedule lambda(t)...
Definition lang_types.h:571
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:242
const char * process_to_text(ProcessType p)
The MATLAB ProcessType name, as sn.procid prints it.
Definition lang_types.h:649
double z(double v)
Definition lqn_focus.h:261
double tot(const std::vector< double > &pops)
double norm1(const Matrix< T > &A)
Matrix< T > dtmc_stochcomp(const Matrix< T > &P, const std::vector< std::size_t > &keep)
Stochastic complement of a DTMC partition, a port of matlab/lib/kpctoolbox/mc/dtmc_stochcomp....
Feature feature_of_sched(SchedStrategy s)
SchedStrategy.toFeature.
const char * feature_name(Feature f)
The canonical registry name, byte for byte the MATLAB field name.
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
void lu_solve(const Matrix< T > &LU, const std::vector< std::size_t > &piv, std::vector< T > &b)
Solve LUx = Pb in place on b, using the factors from lu_factor.
Definition lu.h:94
std::vector< std::size_t > lu_factor(Matrix< T > &A)
In-place LU of A (n x n).
Definition lu.h:48
Matrix< T > inverse(const Matrix< T > &A)
Inverse by LU with one factorization and n back substitutions.
Definition linalg.h:72
Matrix< T > matmul(const Matrix< T > &A, const Matrix< T > &B)
Matrix product A B.
Definition linalg.h:36
std::vector< T > mulvec(const Matrix< T > &A, const std::vector< T > &v)
Matrix times column vector, A v.
Definition linalg.h:62
std::vector< std::complex< double > > eig_values(const Matrix< double > &A)
Eigenvalues of a general real square matrix, in LAPACK's order.
Definition eig.h:46
Matrix< double > sylvester_schur(const Matrix< double > &A, const Matrix< double > &B, const Matrix< double > &C)
A X + X B = C by Bartels-Stewart, at double.
Definition sylvester.h:149
Matrix< T > eye(std::size_t n)
Identity of order n.
Definition linalg.h:28
A queueing network and its refreshed NetworkStruct.
SolverFluid: the closing method, a port of solver_fluid.m, solver_fluid_iteration....
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
The verdict of fluid_ddpp_admits: ok, or the refusal and its identifier.
Definition fluid_ddpp.h:60
void raise() const
Throws the refusal as the error class its category names.
Definition fluid_ddpp.h:67
std::string category
"unsupported" or "numerical" when refused
Definition fluid_ddpp.h:62
std::string kind
the LINE:<category>:<kind> kind
Definition fluid_ddpp.h:63
What the expansion found beside the refined table: the fluid point and V.
Definition fluid_ddpp.h:284
double N
total closed population
Definition fluid_ddpp.h:288
std::vector< double > V
the O(1/N) correction, in job counts
Definition fluid_ddpp.h:286
std::vector< double > x
fluid fixed point x*
Definition fluid_ddpp.h:285
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 > xvec
the converged fluid state
std::vector< double > CN
Matrix< T > D0
The (D0,D1) pair when the type carries one directly.
Definition lang_types.h:915
The Sylvester equation A X + X B = C, and MATLAB's lyap(A,B,C).