LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_sjn.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_PFQN_SJN_H
6#define LINE_API_PFQN_SJN_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * Closed networks with non-preemptive shortest-job-next (SJN/SJF) stations.
12 *
13 * Port of matlab/src/api/pfqn/pfqn_mvasjn.m and pfqn_amvasjn.m together with
14 * the six private helpers they share (sjn_args, sjn_fit, sjn_setup, sjn_quad,
15 * sjn_station, sjn_cap), cross-checked against
16 * jar/src/main/java/jline/api/pfqn/mva/{Pfqn_mvasjn,Pfqn_amvasjn,SjnSupport}.java.
17 *
18 * THE EQUATION. A tagged customer whose service requirement is x waits, by the
19 * arrival theorem, for the residual life of the job in service, for the work of
20 * the queued jobs that will be served before it, and for the work of the jobs
21 * that overtake it while it waits (Kant 1992, eqs. 1-7):
22 *
23 * W(x,n) = [ (1+CV^2) s U(n-1)/2 + X(n-1) phi(x,n-1) ] / [ 1 - X(n-1) theta(x) ]
24 * theta(x) = int_0^x t f(t) dt, phi(x,n) = int_0^x W(t,n) t f(t) dt
25 * R(n) = s + int_0^inf W(x,n) f(x) dx
26 *
27 * W(.,n) needs only phi(.,n-1), so the profile is carried alongside the
28 * population recursion. `pfqn_mvasjn` does exactly that over the whole lattice;
29 * `pfqn_amvasjn` replaces the lattice by a Schweitzer closure applied to the
30 * SIZE-RESOLVED queue length lam_k W_k(x) f_k(x) rather than to its integral,
31 * which is why the two share `sjn_station` unchanged and differ only in the
32 * deflation vector beta handed to it.
33 *
34 * THE SIZE DENSITY IS NOT AN INPUT. Only its mean and SCV are, and the density
35 * is reconstructed by the two-moment branching-Erlang fit the reference
36 * prescribes. That is what makes theta and the tail integrals closed form and
37 * lets the x-integrals run on a FIXED grid: W(.,n) is needed again at the next
38 * population step, so a rule that samples at arbitrary abscissae cannot be
39 * used. Beyond the grid edge Lx the profile is closed by the analytic tail
40 * W(x,n) = a - b exp(-c (x - Lx)) of eqs. (11)-(14).
41 *
42 * ARITHMETIC: DOUBLE, NOT TEMPLATED. Unlike its pfqn neighbours this header is
43 * not generic in the number type. The recursion evaluates the REGULARIZED
44 * INCOMPLETE GAMMA in both its branches, for which the port has no T-generic
45 * implementation, and its accuracy is set by a 33-point Simpson grid rather
46 * than by the arithmetic, so a wider T would buy nothing. Callers gate on
47 * num_traits<T>::has_transcendental and convert at the boundary; the exact
48 * (Rational) path refuses SJN by name in solver_mva_sjn.h.
49 *
50 * WHY NOT REUSE mam::gammainc_lower. It returns only P(a,x). The tail
51 * correction needs Q(a,x) at magnitudes around 1e-300, where 1 - P is exactly
52 * zero, so an upper branch that is computed rather than subtracted is
53 * mandatory. `detail::gammainc` below returns both from the one continued
54 * fraction, which is also what MATLAB's gammainc(...,'upper') does.
55 *
56 * WARNINGS BECOME FLAGS. The reference calls line_warning when the utilization
57 * cap binds and when the fixed point runs out of iterations. This port has no
58 * warning channel, so `SjnResult::capped` and `SjnResult::converged` carry the
59 * same information to the caller. The hard `line_error` cases stay exceptions.
60 *
61 * Reference: K. Kant, "MVA approximations for SJN scheduling", Performance
62 * Evaluation 15(1):41-61, 1992.
63 */
64
65#include <algorithm>
66#include <cmath>
67#include <cstddef>
68#include <limits>
69#include <string>
70#include <vector>
71
73#include "line/util/error.h"
74#include "line/util/matrix.h"
75
76namespace line {
77namespace pfqn {
78
79/** Options of the SJN solvers, the fields sjn_args fills in. */
80struct SjnOptions {
81 std::size_t ns = 32; ///< grid subdivisions, must be even
82 double Lfactor = 8.0; ///< grid extent in units of the largest mean service time
83 std::vector<int> prio; ///< distinct levels, 1 = highest; empty for the pooled reading
84 double tol = 1e-8;
85 std::size_t iter_max = 1000;
86 double umax = 0.999; ///< utilization cap, strictly below one
87};
88
89/** The conditional waiting time profile at one SJN station, the reference's WX. */
90struct SjnProfile {
91 std::size_t station = 0; ///< 1-based row index into L
92 std::vector<double> x; ///< the grid
93 Matrix<double> W; ///< (ngrid x R)
94 Matrix<double> tail; ///< (R x 3), the tail parameters a, b, c
95};
96
97/** Return block of pfqn_mvasjn and pfqn_amvasjn. */
98struct SjnResult {
99 std::vector<double> XN; ///< (R)
100 Matrix<double> QN, UN, CN; ///< (M x R)
101 std::vector<SjnProfile> WX;
102 std::size_t iter = 1;
103 bool converged = true; ///< always true for the lattice recursion
104 bool capped = false; ///< the utilization cap was binding somewhere
105};
106
107/**
108 * The conditional waiting time equation has no solution at some population.
109 *
110 * Raised by pfqn_mvasjn only: the cap rescales the profile that the next
111 * population step reads back, so the correction compounds along the lattice
112 * and the recursion oscillates. pfqn_amvasjn solves the same profile by a
113 * fixed point and does cap, the iteration being self-consistent.
114 */
116public:
117 explicit SjnStarvationError(const std::string& what)
118 : NumericError(ErrorKind{"SjnStarvation"}, what) {}
119};
120
121namespace detail {
122
123/**
124 * Regularized incomplete gamma, both branches at once.
125 *
126 * Series below the transition point, continued fraction above it (Numerical
127 * Recipes 6.2). The branch that the chosen expansion computes DIRECTLY is
128 * returned without a subtraction, so the small one keeps its relative accuracy
129 * down to the underflow limit, which the tail integrals depend on.
130 */
131struct GammaInc {
132 double p; ///< lower P(a,x)
133 double q; ///< upper Q(a,x)
134};
135
136inline GammaInc gammainc(double a, double x) {
137 if (a <= 0.0) throw InputError("pfqn_sjn: the incomplete gamma needs a positive order");
138 if (x < 0.0) throw InputError("pfqn_sjn: the incomplete gamma needs a non-negative argument");
139 if (x == 0.0) return GammaInc{0.0, 1.0};
140 const double gln = std::lgamma(a);
141 const double lead = std::exp(-x + a * std::log(x) - gln);
142 if (x < a + 1.0) {
143 double ap = a, del = 1.0 / a, sum = del;
144 for (int n = 0; n < 1000; ++n) {
145 ap += 1.0;
146 del *= x / ap;
147 sum += del;
148 if (std::fabs(del) < std::fabs(sum) * 1e-16) break;
149 }
150 const double p = sum * lead;
151 return GammaInc{p, 1.0 - p};
152 }
153 const double tiny = 1e-300;
154 double b = x + 1.0 - a, c = 1.0 / tiny, d = 1.0 / b, h = d;
155 for (int i = 1; i <= 1000; ++i) {
156 const double an = -static_cast<double>(i) * (static_cast<double>(i) - a);
157 b += 2.0;
158 d = an * d + b;
159 if (std::fabs(d) < tiny) d = tiny;
160 c = b + an / c;
161 if (std::fabs(c) < tiny) c = tiny;
162 d = 1.0 / d;
163 const double del = d * c;
164 h *= del;
165 if (std::fabs(del - 1.0) < 1e-16) break;
166 }
167 const double q = lead * h;
168 return GammaInc{1.0 - q, q};
169}
170
171/** Erlang mixture fitted to a mean and an SCV, the reference's sjn_fit. */
172struct SjnFit {
173 std::vector<double> w;
174 std::vector<int> k;
175 std::vector<double> mu;
176 bool empty() const { return w.empty(); }
177};
178
179/**
180 * Branching Erlang (Erlang(k-1) and Erlang(k) sharing a rate) below CV^2 = 1,
181 * balanced-means hyperexponential above it, exponential at it.
182 */
183inline SjnFit sjn_fit(double s, double cv2) {
184 SjnFit f;
185 if (s <= 0.0) return f;
186 if (cv2 < 0.0) throw InputError("pfqn_sjn: negative squared coefficient of variation");
187 if (std::fabs(cv2 - 1.0) < 1e-8) {
188 f.w = {1.0};
189 f.k = {1};
190 f.mu = {1.0 / s};
191 } else if (cv2 < 1.0) {
192 const double kd = std::ceil(1.0 / cv2);
193 const double p = (kd * cv2 - std::sqrt(kd * (1.0 + cv2) - kd * kd * cv2)) / (1.0 + cv2);
194 const double mu = (kd - p) / s;
195 f.w = {p, 1.0 - p};
196 f.k = {static_cast<int>(kd) - 1, static_cast<int>(kd)};
197 f.mu = {mu, mu};
198 } else {
199 const double p = 0.5 * (1.0 + std::sqrt((cv2 - 1.0) / (cv2 + 1.0)));
200 f.w = {p, 1.0 - p};
201 f.k = {1, 1};
202 f.mu = {2.0 * p / s, 2.0 * (1.0 - p) / s};
203 }
204 return f;
205}
206
207/** Density of the mixture on a grid, the reference's sjn_quad('pdf'). */
208inline std::vector<double> sjn_pdf(const SjnFit& f, const std::vector<double>& x) {
209 // realmin, not the smallest denormal: MATLAB's max(x,realmin) is the model.
210 const double floorx = std::numeric_limits<double>::min();
211 std::vector<double> y(x.size(), 0.0);
212 for (std::size_t j = 0; j < f.w.size(); ++j) {
213 const double kd = static_cast<double>(f.k[j]), mu = f.mu[j];
214 for (std::size_t i = 0; i < x.size(); ++i)
215 y[i] += f.w[j] * std::exp(kd * std::log(mu) +
216 (kd - 1.0) * std::log(std::max(x[i], floorx)) - mu * x[i] -
217 std::lgamma(kd));
218 }
219 return y;
220}
221
222/** int_0^x t f(t) dt in closed form, the reference's sjn_quad('theta'). */
223inline std::vector<double> sjn_theta(const SjnFit& f, const std::vector<double>& x) {
224 std::vector<double> y(x.size(), 0.0);
225 for (std::size_t j = 0; j < f.w.size(); ++j) {
226 const double kd = static_cast<double>(f.k[j]), mu = f.mu[j];
227 for (std::size_t i = 0; i < x.size(); ++i)
228 y[i] += f.w[j] * (kd / mu) * gammainc(kd + 1.0, mu * x[i]).p;
229 }
230 return y;
231}
232
233/** int_x^inf f(t) dt in closed form, the reference's sjn_quad('ccdf'). */
234inline double sjn_ccdf(const SjnFit& f, double x) {
235 double y = 0.0;
236 for (std::size_t j = 0; j < f.w.size(); ++j)
237 y += f.w[j] * gammainc(static_cast<double>(f.k[j]), f.mu[j] * x).q;
238 return y;
239}
240
241/**
242 * int_Lx^inf t^order exp(-c (t-Lx)) f(t) dt, the reference's sjn_quad('tailmom').
243 * Evaluated in logarithms so that exp(c Lx) cannot overflow against an
244 * underflowing incomplete gamma.
245 */
246inline double sjn_tailmom(const SjnFit& f, double Lx, double c, int order) {
247 double y = 0.0;
248 for (std::size_t j = 0; j < f.w.size(); ++j) {
249 const double kd = static_cast<double>(f.k[j]), mu = f.mu[j];
250 const double rate = mu + c;
251 const double g = gammainc(kd + static_cast<double>(order), rate * Lx).q;
252 if (g <= 0.0) continue;
253 double lg = c * Lx + kd * std::log(mu / rate) + std::log(g);
254 if (order == 1) lg += std::log(kd / rate);
255 y += f.w[j] * std::exp(lg);
256 }
257 return y;
258}
259
260/** Composite Simpson over an even number of subdivisions. */
261inline double sjn_simpson(const std::vector<double>& y, double dx) {
262 const std::size_t n = y.size();
263 double odd = 0.0, even = 0.0;
264 for (std::size_t i = 1; i + 1 < n; i += 2) odd += y[i];
265 for (std::size_t i = 2; i + 1 < n; i += 2) even += y[i];
266 return dx / 3.0 * (y[0] + y[n - 1] + 4.0 * odd + 2.0 * even);
267}
268
269/**
270 * Cumulative Simpson: full panels at the even nodes and a half panel at the odd
271 * ones, so the primitive is available at EVERY grid node. Quadrature at
272 * arbitrary abscissae could not provide that, the profile being needed again at
273 * the next population step.
274 */
275inline std::vector<double> sjn_cumsimpson(const std::vector<double>& y, double dx) {
276 const std::size_t n = y.size();
277 std::vector<double> I(n, 0.0);
278 for (std::size_t i = 2; i < n; i += 2)
279 I[i] = I[i - 2] + dx / 3.0 * (y[i - 2] + 4.0 * y[i - 1] + y[i]);
280 for (std::size_t i = 1; i < n; i += 2) {
281 if (i + 1 < n)
282 I[i] = I[i - 1] + dx / 12.0 * (5.0 * y[i - 1] + 8.0 * y[i] - y[i + 1]);
283 else
284 I[i] = I[i - 1] + dx / 12.0 * (-y[i - 2] + 8.0 * y[i - 1] + 5.0 * y[i]);
285 }
286 return I;
287}
288
289/** Grid of one SJN station and the population-independent integrals over it. */
290struct SjnGrid {
291 double Lx = 0.0, dx = 0.0;
292 std::vector<double> x;
293 Matrix<double> f, theta; ///< (ngrid x R)
294 std::vector<double> tail0, tail1;
295 std::vector<SjnFit> fit;
296};
297
298/** Port of sjn_setup. */
299inline SjnGrid sjn_setup(const std::vector<double>& S, const std::vector<double>& scv,
300 std::size_t ns, double Lfactor) {
301 const std::size_t R = S.size(), ngrid = ns + 1;
302 double smax = 0.0;
303 for (double s : S) smax = std::max(smax, s);
304 if (smax <= 0.0)
305 throw InputError("pfqn_sjn: the station has zero service demand in every class");
306 SjnGrid G;
307 G.Lx = Lfactor * smax;
308 G.dx = G.Lx / static_cast<double>(ns);
309 G.x.assign(ngrid, 0.0);
310 for (std::size_t i = 0; i < ngrid; ++i) G.x[i] = static_cast<double>(i) * G.dx;
311 G.f = Matrix<double>(ngrid, R, 0.0);
312 G.theta = Matrix<double>(ngrid, R, 0.0);
313 G.tail0.assign(R, 0.0);
314 G.tail1.assign(R, 0.0);
315 G.fit.assign(R, SjnFit());
316 for (std::size_t r = 0; r < R; ++r) {
317 G.fit[r] = sjn_fit(S[r], scv[r]);
318 if (G.fit[r].empty()) continue;
319 const std::vector<double> fr = sjn_pdf(G.fit[r], G.x);
320 const std::vector<double> tr = sjn_theta(G.fit[r], G.x);
321 for (std::size_t i = 0; i < ngrid; ++i) {
322 G.f(i, r) = fr[i];
323 G.theta(i, r) = tr[i];
324 }
325 G.tail0[r] = sjn_ccdf(G.fit[r], G.Lx);
326 G.tail1[r] = S[r] - tr[ngrid - 1];
327 }
328 return G;
329}
330
331/** State of one SJN station at the reference population, the reference's `st`. */
332struct SjnState {
333 std::vector<double> lam, U, Q;
334 const Matrix<double>* W = nullptr; ///< (ngrid x R)
335 const Matrix<double>* phi = nullptr; ///< (ngrid x R)
336 std::vector<double> phiinf;
337};
338
339/** Outcome of one evaluation of the conditional waiting time equation. */
340struct SjnStationResult {
341 double C = 0.0;
342 std::vector<double> W, phi;
343 double phiinf = 0.0;
344 double tail[3] = {0.0, 0.0, 0.0};
345};
346
347/** `m` is 0-based here; the message reports the reference's 1-based index. */
348inline NumericError sjn_singular(std::size_t m) {
349 return NumericError(ErrorKind{"Instability"},
350 "pfqn_sjn: the SJN recursion at station " + std::to_string(m + 1) +
351 " has no solution: the work brought by jobs no longer than the tagged one saturates the "
352 "server, at which point long jobs starve and the arrival theorem no longer holds. Reduce "
353 "the load at that station or model it with SolverCTMC or SolverLDES");
354}
355
356/**
357 * Port of sjn_station: one evaluation of the SJN conditional waiting time
358 * equation for a tagged customer of class r at station m.
359 *
360 * lam_k W_k(x) f_k(x) is the density, in the job size x, of the queued class-k
361 * customers, so deflating it by beta_k turns the same equation into either the
362 * exact recursion (beta = 1, the state already being the one at n - e_r) or the
363 * Schweitzer closure (beta_r = (N_r-1)/N_r, the state being the one at N).
364 */
365inline SjnStationResult sjn_station(std::size_t m, std::size_t r, const SjnGrid& G,
366 const std::vector<double>& S, const std::vector<double>& scv,
367 const std::vector<double>& V, const SjnState& st,
368 const std::vector<double>& beta, bool useprio,
369 const std::vector<int>& prio) {
370 const std::size_t R = S.size(), ngrid = G.x.size();
371 std::vector<double> lamb(R), Ub(R), Qb(R);
372 double RL = 0.0;
373 for (std::size_t k = 0; k < R; ++k) {
374 lamb[k] = beta[k] * st.lam[k];
375 Ub[k] = beta[k] * st.U[k];
376 Qb[k] = beta[k] * st.Q[k];
377 RL += (1.0 + scv[k]) * S[k] * Ub[k] / 2.0;
378 }
379 std::vector<double> num(ngrid), den(ngrid);
380 double numinf = 0.0, deninf = 0.0;
381 if (useprio) {
382 double base = RL, uhi = 0.0;
383 for (std::size_t k = 0; k < R; ++k)
384 if (prio[k] < prio[r]) {
385 base += S[k] * (Qb[k] - Ub[k]);
386 uhi += Ub[k];
387 }
388 for (std::size_t i = 0; i < ngrid; ++i) {
389 num[i] = base + lamb[r] * (*st.phi)(i, r);
390 den[i] = 1.0 - uhi - lamb[r] * G.theta(i, r);
391 }
392 numinf = base + lamb[r] * st.phiinf[r];
393 deninf = 1.0 - uhi - lamb[r] * S[r];
394 } else {
395 double usum = 0.0, phiinfsum = 0.0;
396 for (std::size_t k = 0; k < R; ++k) {
397 usum += lamb[k] * S[k];
398 phiinfsum += lamb[k] * st.phiinf[k];
399 }
400 for (std::size_t i = 0; i < ngrid; ++i) {
401 double n = RL, d = 1.0;
402 for (std::size_t k = 0; k < R; ++k) {
403 n += lamb[k] * (*st.phi)(i, k);
404 d -= lamb[k] * G.theta(i, k);
405 }
406 num[i] = n;
407 den[i] = d;
408 }
409 numinf = RL + phiinfsum;
410 deninf = 1.0 - usum;
411 }
412 for (std::size_t i = 0; i < ngrid; ++i)
413 if (den[i] <= 0.0) throw sjn_singular(m);
414 if (deninf <= 0.0) throw sjn_singular(m);
415
416 SjnStationResult out;
417 out.W.assign(ngrid, 0.0);
418 for (std::size_t i = 0; i < ngrid; ++i) out.W[i] = num[i] / den[i];
419 const double Winf = numinf / deninf;
420 // eq. (14), generalised by differentiating the recursion at the grid edge
421 double slope;
422 if (useprio) {
423 slope = G.Lx * lamb[r] * G.f(ngrid - 1, r) *
424 ((*st.W)(ngrid - 1, r) + out.W[ngrid - 1]) / den[ngrid - 1];
425 } else {
426 double acc = 0.0;
427 for (std::size_t k = 0; k < R; ++k)
428 acc += lamb[k] * G.f(ngrid - 1, k) * ((*st.W)(ngrid - 1, k) + out.W[ngrid - 1]);
429 slope = G.Lx * acc / den[ngrid - 1];
430 }
431 double a = Winf, b = Winf - out.W[ngrid - 1], c;
432 if (b <= 0.0) {
433 b = 0.0;
434 c = 0.0;
435 } else if (slope < 0.0) {
436 throw NumericError(ErrorKind{"Instability"}, "pfqn_sjn: the conditional waiting time at SJN station " +
437 std::to_string(m + 1) +
438 " decreases in the job size, which the discipline forbids: the "
439 "recursion has become numerically unstable");
440 } else {
441 c = slope / b;
442 }
443 out.tail[0] = a;
444 out.tail[1] = b;
445 out.tail[2] = c;
446
447 std::vector<double> integrand(ngrid);
448 for (std::size_t i = 0; i < ngrid; ++i) integrand[i] = out.W[i] * G.x[i] * G.f(i, r);
449 out.phi = sjn_cumsimpson(integrand, G.dx);
450 out.phiinf = out.phi[ngrid - 1] + a * G.tail1[r] - b * sjn_tailmom(G.fit[r], G.Lx, c, 1);
451 for (std::size_t i = 0; i < ngrid; ++i) integrand[i] = out.W[i] * G.f(i, r);
452 const double Wbar =
453 sjn_simpson(integrand, G.dx) + a * G.tail0[r] - b * sjn_tailmom(G.fit[r], G.Lx, c, 0);
454 out.C = V[r] * (S[r] + Wbar);
455 return out;
456}
457
458/** Throughputs implied by the residence times, keeping Little's law exact. */
459inline std::vector<double> sjn_thru(const Matrix<double>& C, const std::vector<double>& N,
460 const std::vector<double>& Z) {
461 const std::size_t M = C.rows(), R = N.size();
462 std::vector<double> X(R, 0.0);
463 for (std::size_t r = 0; r < R; ++r) {
464 if (N[r] <= 0.0) continue;
465 double den = Z[r];
466 for (std::size_t m = 0; m < M; ++m) den += C(m, r);
467 X[r] = N[r] / den;
468 }
469 return X;
470}
471
472/** Outcome of the utilization cap. */
473struct SjnCapResult {
474 std::vector<double> X, kappa;
475 bool bound = false;
476};
477
478/**
479 * Port of sjn_cap: enforce U <= umax at every SJN station by inflating its
480 * waiting time, and return the throughputs the capped residence times imply.
481 *
482 * The SJN response time equation is an open-system one: its denominator is
483 * 1 - U(x), and it has no solution once that reaches one. A closed network
484 * never reaches it in reality, but the approximation can, because it
485 * underestimates the residence time at a congested SJN station and the
486 * resulting throughput then exceeds the station capacity. What is imposed is
487 * the utilization law sum_r X_r L_mr <= umax, an exact property of the network
488 * and not a property of the approximation. It acts on the EXCESS C - L, never
489 * on the throughput, so that X (Z + sum_m C) = N still holds exactly and no
490 * jobs are lost.
491 */
492inline SjnCapResult sjn_cap(Matrix<double>& C, const Matrix<double>& L,
493 const std::vector<double>& N, const std::vector<double>& Z,
494 const std::vector<std::size_t>& sjnset, double umax) {
495 const std::size_t nsjn = sjnset.size(), R = N.size();
496 SjnCapResult out;
497 out.kappa.assign(nsjn, 1.0);
498 out.X = sjn_thru(C, N, Z);
499 if (nsjn == 0) return out;
500 if (umax >= 1.0)
501 throw InputError("pfqn_sjn: the utilization cap must be strictly below one, the response "
502 "time equation is singular at one");
503 // rho at station m had the excess been inflated by kappa, C left untouched
504 const auto rho_at = [&](std::size_t m, const std::vector<double>& Wq, double kappa) {
505 std::vector<double> saved(R);
506 for (std::size_t r = 0; r < R; ++r) {
507 saved[r] = C(m, r);
508 C(m, r) = L(m, r) + kappa * Wq[r];
509 }
510 const std::vector<double> X = sjn_thru(C, N, Z);
511 double rho = 0.0;
512 for (std::size_t r = 0; r < R; ++r) rho += X[r] * L(m, r);
513 for (std::size_t r = 0; r < R; ++r) C(m, r) = saved[r];
514 return rho;
515 };
516 for (int sweep = 0; sweep < 20; ++sweep) {
517 bool viol = false;
518 for (std::size_t q = 0; q < nsjn; ++q) {
519 const std::size_t m = sjnset[q];
520 double rho = 0.0;
521 for (std::size_t r = 0; r < R; ++r) rho += out.X[r] * L(m, r);
522 if (rho <= umax) continue;
523 viol = true;
524 out.bound = true;
525 std::vector<double> Wq(R);
526 for (std::size_t r = 0; r < R; ++r) Wq[r] = C(m, r) - L(m, r);
527 double hi = 2.0;
528 while (rho_at(m, Wq, hi) > umax) {
529 hi *= 2.0;
530 if (hi > 1e12)
531 throw NumericError(ErrorKind{"Instability"},
532 "pfqn_sjn: station " + std::to_string(m + 1) +
533 " cannot be brought under the utilization cap by any waiting time: its "
534 "service demands alone saturate it at this population");
535 }
536 double lo = 1.0;
537 for (int b = 0; b < 200; ++b) {
538 const double mid = (lo + hi) / 2.0;
539 if (rho_at(m, Wq, mid) > umax)
540 lo = mid;
541 else
542 hi = mid;
543 }
544 out.kappa[q] *= hi;
545 for (std::size_t r = 0; r < R; ++r) C(m, r) = L(m, r) + hi * Wq[r];
546 out.X = sjn_thru(C, N, Z);
547 }
548 if (!viol) return out;
549 }
550 throw NumericError(ErrorKind{"NonConvergence"}, "pfqn_sjn: the utilization cap did not settle across the SJN stations");
551}
552
553/** Normalised arguments, the reference's sjn_args. */
554struct SjnArgs {
555 std::size_t M = 0, R = 0;
556 std::vector<double> N, Z;
557 Matrix<double> scv, V, S;
558 std::vector<std::size_t> sjnset; ///< 0-based rows of L
559 SjnOptions opt;
560};
561
562/** Port of sjn_args. */
563inline SjnArgs sjn_args(const Matrix<double>& L, const std::vector<double>& N,
564 const std::vector<double>& Z, const Matrix<double>& scv,
565 const std::vector<std::size_t>& sjnset, const Matrix<double>& V,
566 const SjnOptions& options) {
567 SjnArgs a;
568 a.M = L.rows();
569 a.R = L.cols();
570 a.opt = options;
571 if (N.size() != a.R)
572 throw InputError("pfqn_sjn: demand matrix and population vector have different number of "
573 "classes");
574 a.N.assign(a.R, 0.0);
575 for (std::size_t r = 0; r < a.R; ++r) {
576 a.N[r] = std::round(N[r]);
577 if (a.N[r] < 0.0) throw InputError("pfqn_sjn: negative class populations");
578 }
579 a.Z = Z.empty() ? std::vector<double>(a.R, 0.0) : Z;
580 if (a.Z.size() != a.R) throw InputError("pfqn_sjn: Z has the wrong length");
581 a.scv = scv.empty() ? Matrix<double>(a.M, a.R, 1.0) : scv;
582 if (a.scv.rows() != a.M || a.scv.cols() != a.R)
583 throw InputError("pfqn_sjn: scv has the wrong shape");
584 a.sjnset = sjnset;
585 for (std::size_t i = 0; i < a.sjnset.size(); ++i) {
586 if (a.sjnset[i] >= a.M)
587 throw InputError("pfqn_sjn: sjnset contains a station index outside 1..M");
588 for (std::size_t j = i + 1; j < a.sjnset.size(); ++j)
589 if (a.sjnset[i] == a.sjnset[j])
590 throw InputError("pfqn_sjn: sjnset repeats a station index");
591 }
592 a.V = V.empty() ? Matrix<double>(a.M, a.R, 1.0) : V;
593 if (a.V.rows() != a.M || a.V.cols() != a.R)
594 throw InputError("pfqn_sjn: V has the wrong shape");
595 // the job size the discipline compares is one VISIT's service time, not the
596 // demand accumulated over all visits
597 a.S = Matrix<double>(a.M, a.R, 0.0);
598 for (std::size_t m = 0; m < a.M; ++m)
599 for (std::size_t r = 0; r < a.R; ++r)
600 if (a.V(m, r) > 0.0) a.S(m, r) = L(m, r) / a.V(m, r);
601 if (a.opt.ns == 0 || a.opt.ns % 2 != 0)
602 throw InputError("pfqn_sjn: options.ns must be even, composite Simpson integrates over "
603 "panels of two subdivisions");
604 if (a.opt.umax <= 0.0 || a.opt.umax >= 1.0)
605 throw InputError("pfqn_sjn: options.umax must lie strictly between zero and one");
606 if (!a.opt.prio.empty()) {
607 if (a.opt.prio.size() != a.R)
608 throw InputError("pfqn_sjn: options.prio must have one priority level per class");
609 for (std::size_t i = 0; i < a.R; ++i)
610 for (std::size_t j = i + 1; j < a.R; ++j)
611 if (a.opt.prio[i] == a.opt.prio[j])
612 throw InputError("pfqn_sjn: options.prio must assign distinct levels, ties "
613 "across classes are not covered by the SJN priority "
614 "equations");
615 }
616 return a;
617}
618
619} // namespace detail
620
621/**
622 * Exact-lattice MVA for closed networks with SJN stations, the unidirectional
623 * scheme of Kant 1992.
624 *
625 * The recursion is explicit -- W(.,n) needs only phi(.,n-1) -- so the profile
626 * is carried alongside the population recursion and the whole lattice of
627 * prod(N+1) states is stepped through. Two multiclass readings of "shortest
628 * job" are selected by options.prio: POOLED (empty, the default) compares the
629 * jobs of every class by size directly and collapses to eq. (6) for a single
630 * class; PRIORITY (distinct levels, 1 = highest) is method A of eq. (21), SJN
631 * applying only within a class. Method B, which evaluates the denominator at
632 * the non-integral population n - Q(n), is not implemented in either codebase.
633 *
634 * @param L (M x R) demands at the queueing stations
635 * @param N (R) populations
636 * @param Z (R) think times, empty for none
637 * @param scv (M x R) squared coefficients of variation, empty for ones
638 * @param sjnset 0-based rows of L that schedule by SJN
639 * @param V (M x R) visit ratios, empty for ones
640 * @param options quadrature grid, tolerances and iteration caps of the recursion
641 */
642inline SjnResult pfqn_mvasjn(const Matrix<double>& L, const std::vector<double>& N,
643 const std::vector<double>& Z, const Matrix<double>& scv,
644 const std::vector<std::size_t>& sjnset, const Matrix<double>& V,
645 const SjnOptions& options) {
646 const detail::SjnArgs a = detail::sjn_args(L, N, Z, scv, sjnset, V, options);
647 const std::size_t M = a.M, R = a.R, nsjn = a.sjnset.size();
648 const bool useprio = !a.opt.prio.empty();
649 const std::size_t ns = a.opt.ns, ngrid = ns + 1;
650
651 std::vector<detail::SjnGrid> G(nsjn);
652 for (std::size_t q = 0; q < nsjn; ++q) {
653 std::vector<double> Sq(R), scvq(R);
654 for (std::size_t r = 0; r < R; ++r) {
655 Sq[r] = a.S(a.sjnset[q], r);
656 scvq[r] = a.scv(a.sjnset[q], r);
657 }
658 G[q] = detail::sjn_setup(Sq, scvq, ns, a.opt.Lfactor);
659 }
660
661 // MATLAB reaches the same limit as an out-of-memory on prod(N+1); say so
662 // rather than wrap the index arithmetic silently
663 double lattice = 1.0;
664 for (std::size_t r = 0; r < R; ++r) lattice *= a.N[r] + 1.0;
665 if (lattice > 1e9)
666 throw InputError("pfqn_mvasjn: the population lattice has " + std::to_string(lattice) +
667 " states and does not fit; use pfqn_amvasjn (method 'amva')");
668 std::vector<std::size_t> stride(R, 1);
669 std::size_t npop = 1;
670 for (std::size_t r = 0; r < R; ++r) {
671 stride[r] = npop;
672 npop *= static_cast<std::size_t>(a.N[r]) + 1;
673 }
674
675 // one slab per lattice point: X, [Q,U,C] per station, and per SJN station
676 // the profile W, its primitive phi, phi at infinity and the tail parameters
677 std::vector<double> Xp(npop * R, 0.0), Qp(npop * M * R, 0.0), Up(npop * M * R, 0.0),
678 Cp(npop * M * R, 0.0);
679 std::vector<Matrix<double>> Wp(nsjn * npop), Pp(nsjn * npop);
680 std::vector<double> Ip(nsjn * npop * R, 0.0), Tp(nsjn * npop * R * 3, 0.0);
681 for (auto& m : Wp) m = Matrix<double>(ngrid, R, 0.0);
682 for (auto& m : Pp) m = Matrix<double>(ngrid, R, 0.0);
683
684 SjnResult res;
685 Matrix<double> Call(M, R, 0.0);
686 for (std::size_t idx = 1; idx < npop; ++idx) {
687 std::vector<double> n(R, 0.0);
688 for (std::size_t r = 0; r < R; ++r)
689 n[r] = static_cast<double>((idx / stride[r]) %
690 (static_cast<std::size_t>(a.N[r]) + 1));
691 Call.fill(0.0);
692 for (std::size_t r = 0; r < R; ++r) {
693 if (n[r] == 0.0) continue;
694 const std::size_t iprev = idx - stride[r];
695 for (std::size_t m = 0; m < M; ++m) {
696 std::size_t q = nsjn;
697 for (std::size_t t = 0; t < nsjn; ++t)
698 if (a.sjnset[t] == m) q = t;
699 if (q == nsjn) {
700 double qsum = 0.0;
701 for (std::size_t k = 0; k < R; ++k) qsum += Qp[(iprev * M + m) * R + k];
702 Call(m, r) = L(m, r) * (1.0 + qsum);
703 continue;
704 }
705 // the population step already supplies the neighbouring profile,
706 // so no deflation is needed
707 const std::vector<double> beta(R, 1.0);
708 detail::SjnState st;
709 st.lam.assign(R, 0.0);
710 st.U.assign(R, 0.0);
711 st.Q.assign(R, 0.0);
712 st.phiinf.assign(R, 0.0);
713 for (std::size_t k = 0; k < R; ++k) {
714 st.lam[k] = Xp[iprev * R + k] * a.V(m, k);
715 st.U[k] = Up[(iprev * M + m) * R + k];
716 st.Q[k] = Qp[(iprev * M + m) * R + k];
717 st.phiinf[k] = Ip[(q * npop + iprev) * R + k];
718 }
719 st.W = &Wp[q * npop + iprev];
720 st.phi = &Pp[q * npop + iprev];
721 std::vector<double> Sm(R), scvm(R), Vm(R);
722 for (std::size_t k = 0; k < R; ++k) {
723 Sm[k] = a.S(m, k);
724 scvm[k] = a.scv(m, k);
725 Vm[k] = a.V(m, k);
726 }
727 const detail::SjnStationResult sr =
728 detail::sjn_station(m, r, G[q], Sm, scvm, Vm, st, beta, useprio, a.opt.prio);
729 Call(m, r) = sr.C;
730 for (std::size_t i = 0; i < ngrid; ++i) {
731 Wp[q * npop + idx](i, r) = sr.W[i];
732 Pp[q * npop + idx](i, r) = sr.phi[i];
733 }
734 Ip[(q * npop + idx) * R + r] = sr.phiinf;
735 for (int t = 0; t < 3; ++t)
736 Tp[((q * npop + idx) * R + r) * 3 + t] = sr.tail[t];
737 }
738 }
739 const detail::SjnCapResult cap = detail::sjn_cap(Call, L, n, a.Z, a.sjnset, a.opt.umax);
740 if (cap.bound) {
741 // the cap has invalidated the profile the next population step reads back
742 std::string npos;
743 for (std::size_t r = 0; r < R; ++r)
744 npos += (r ? " " : "") + std::to_string(static_cast<long long>(n[r]));
745 throw SjnStarvationError(
746 "pfqn_mvasjn: the utilization cap of " + std::to_string(a.opt.umax) +
747 " was binding at an SJN station at population [" + npos +
748 "]: the station is in the starvation regime, where the conditional waiting time "
749 "equation has no solution and the population lattice no valid continuation. Use "
750 "the Schweitzer fixed point (pfqn_amvasjn, method 'amva'), SolverCTMC or "
751 "SolverLDES.");
752 }
753 for (std::size_t r = 0; r < R; ++r) Xp[idx * R + r] = cap.X[r];
754 for (std::size_t m = 0; m < M; ++m)
755 for (std::size_t r = 0; r < R; ++r) {
756 Cp[(idx * M + m) * R + r] = Call(m, r);
757 Qp[(idx * M + m) * R + r] = cap.X[r] * Call(m, r);
758 Up[(idx * M + m) * R + r] = cap.X[r] * L(m, r);
759 }
760 }
761
762 const std::size_t last = npop - 1;
763 res.XN.assign(R, 0.0);
764 res.QN = Matrix<double>(M, R, 0.0);
765 res.UN = Matrix<double>(M, R, 0.0);
766 res.CN = Matrix<double>(M, R, 0.0);
767 for (std::size_t r = 0; r < R; ++r) res.XN[r] = Xp[last * R + r];
768 for (std::size_t m = 0; m < M; ++m)
769 for (std::size_t r = 0; r < R; ++r) {
770 res.QN(m, r) = Qp[(last * M + m) * R + r];
771 res.UN(m, r) = Up[(last * M + m) * R + r];
772 res.CN(m, r) = Cp[(last * M + m) * R + r];
773 }
774 res.WX.resize(nsjn);
775 for (std::size_t q = 0; q < nsjn; ++q) {
776 res.WX[q].station = a.sjnset[q] + 1;
777 res.WX[q].x = G[q].x;
778 res.WX[q].W = Wp[q * npop + last];
779 res.WX[q].tail = Matrix<double>(R, 3, 0.0);
780 for (std::size_t r = 0; r < R; ++r)
781 for (int t = 0; t < 3; ++t)
782 res.WX[q].tail(r, t) = Tp[((q * npop + last) * R + r) * 3 + t];
783 }
784 return res;
785}
786
787/**
788 * Schweitzer fixed point counterpart of pfqn_mvasjn.
789 *
790 * The closure is applied to the SIZE-RESOLVED queue length lam_k W_k(x) f_k(x)
791 * rather than to its integral: removing one customer of class r scales the
792 * class-r density by (N_r-1)/N_r and leaves the other classes unchanged.
793 * Integrating over x recovers the usual Schweitzer rule, so the closure is the
794 * exact analogue of the one used at the ordinary stations. Cost per iteration
795 * is O(M R ns) against the prod(N+1) M R ns of the lattice, and the population
796 * may be arbitrarily large. What is given up is the population dependence of
797 * the SHAPE of W(x): its level may scale but its shape is fixed, whereas the
798 * true profile stiffens with the load. The error therefore concentrates at high
799 * utilization, where the SJN approximation is already weakest.
800 */
801inline SjnResult pfqn_amvasjn(const Matrix<double>& L, const std::vector<double>& N,
802 const std::vector<double>& Z, const Matrix<double>& scv,
803 const std::vector<std::size_t>& sjnset, const Matrix<double>& V,
804 const SjnOptions& options) {
805 const detail::SjnArgs a = detail::sjn_args(L, N, Z, scv, sjnset, V, options);
806 const std::size_t M = a.M, R = a.R, nsjn = a.sjnset.size();
807 const bool useprio = !a.opt.prio.empty();
808 const std::size_t ns = a.opt.ns, ngrid = ns + 1;
809
810 std::vector<detail::SjnGrid> G(nsjn);
811 for (std::size_t q = 0; q < nsjn; ++q) {
812 std::vector<double> Sq(R), scvq(R);
813 for (std::size_t r = 0; r < R; ++r) {
814 Sq[r] = a.S(a.sjnset[q], r);
815 scvq[r] = a.scv(a.sjnset[q], r);
816 }
817 G[q] = detail::sjn_setup(Sq, scvq, ns, a.opt.Lfactor);
818 }
819
820 // start from the product-form Schweitzer solution: a light-load guess would
821 // put the deflated utilization above one, where the SJN denominator has no
822 // solution at all
823 const AmvaResult<double> bs =
824 pfqn_bs(L, a.N, a.Z, std::vector<AmvaSched>(), a.opt.tol, a.opt.iter_max);
825 SjnResult res;
826 res.XN = bs.XN;
827 res.QN = bs.QN;
828 res.UN = bs.UN;
829 res.CN = bs.RN;
830
831 std::vector<Matrix<double>> W(nsjn, Matrix<double>(ngrid, R, 0.0));
832 std::vector<Matrix<double>> P(nsjn, Matrix<double>(ngrid, R, 0.0));
833 Matrix<double> Iinf(nsjn, R, 0.0);
834 std::vector<Matrix<double>> Tail(nsjn, Matrix<double>(R, 3, 0.0));
835
836 std::size_t it = 0;
837 bool converged = false;
838 double delta = 0.0;
839 while (!converged && it < a.opt.iter_max) {
840 ++it;
841 Matrix<double> Cit = res.CN;
842 std::vector<Matrix<double>> Wit = W, Pit = P, Tit = Tail;
843 Matrix<double> Iit = Iinf;
844 for (std::size_t r = 0; r < R; ++r) {
845 if (a.N[r] == 0.0) continue;
846 std::vector<double> beta(R, 1.0);
847 beta[r] = (a.N[r] - 1.0) / a.N[r];
848 for (std::size_t m = 0; m < M; ++m) {
849 std::size_t q = nsjn;
850 for (std::size_t t = 0; t < nsjn; ++t)
851 if (a.sjnset[t] == m) q = t;
852 if (q == nsjn) {
853 double qsum = 0.0;
854 for (std::size_t k = 0; k < R; ++k) qsum += beta[k] * res.QN(m, k);
855 Cit(m, r) = L(m, r) * (1.0 + qsum);
856 continue;
857 }
858 detail::SjnState st;
859 st.lam.assign(R, 0.0);
860 st.U.assign(R, 0.0);
861 st.Q.assign(R, 0.0);
862 st.phiinf.assign(R, 0.0);
863 for (std::size_t k = 0; k < R; ++k) {
864 st.lam[k] = res.XN[k] * a.V(m, k);
865 st.U[k] = res.UN(m, k);
866 st.Q[k] = res.QN(m, k);
867 st.phiinf[k] = Iinf(q, k);
868 }
869 st.W = &W[q];
870 st.phi = &P[q];
871 std::vector<double> Sm(R), scvm(R), Vm(R);
872 for (std::size_t k = 0; k < R; ++k) {
873 Sm[k] = a.S(m, k);
874 scvm[k] = a.scv(m, k);
875 Vm[k] = a.V(m, k);
876 }
877 const detail::SjnStationResult sr =
878 detail::sjn_station(m, r, G[q], Sm, scvm, Vm, st, beta, useprio, a.opt.prio);
879 Cit(m, r) = sr.C;
880 for (std::size_t i = 0; i < ngrid; ++i) {
881 Wit[q](i, r) = sr.W[i];
882 Pit[q](i, r) = sr.phi[i];
883 }
884 Iit(q, r) = sr.phiinf;
885 for (int t = 0; t < 3; ++t) Tit[q](r, t) = sr.tail[t];
886 }
887 }
888 const detail::SjnCapResult cap = detail::sjn_cap(Cit, L, a.N, a.Z, a.sjnset, a.opt.umax);
889 if (cap.bound) {
890 res.capped = true;
891 for (std::size_t q = 0; q < nsjn; ++q) {
892 const double kq = cap.kappa[q];
893 for (std::size_t i = 0; i < ngrid; ++i)
894 for (std::size_t r = 0; r < R; ++r) {
895 Wit[q](i, r) *= kq;
896 Pit[q](i, r) *= kq;
897 }
898 for (std::size_t r = 0; r < R; ++r) {
899 Iit(q, r) *= kq;
900 Tit[q](r, 0) *= kq;
901 Tit[q](r, 1) *= kq;
902 }
903 }
904 }
905 Matrix<double> Qit(M, R, 0.0), Uit(M, R, 0.0);
906 for (std::size_t m = 0; m < M; ++m)
907 for (std::size_t r = 0; r < R; ++r) {
908 Qit(m, r) = cap.X[r] * Cit(m, r);
909 Uit(m, r) = cap.X[r] * L(m, r);
910 }
911 delta = 0.0;
912 for (std::size_t m = 0; m < M; ++m)
913 for (std::size_t r = 0; r < R; ++r)
914 delta = std::max(delta, std::fabs(Qit(m, r) - res.QN(m, r)));
915 for (std::size_t q = 0; q < nsjn; ++q)
916 for (std::size_t i = 0; i < ngrid; ++i)
917 for (std::size_t r = 0; r < R; ++r)
918 delta = std::max(delta, std::fabs(Wit[q](i, r) - W[q](i, r)));
919 res.XN = cap.X;
920 res.QN = Qit;
921 res.UN = Uit;
922 res.CN = Cit;
923 W = Wit;
924 P = Pit;
925 Iinf = Iit;
926 Tail = Tit;
927 converged = delta < a.opt.tol;
928 }
929 res.iter = it;
930 res.converged = converged;
931
932 res.WX.resize(nsjn);
933 for (std::size_t q = 0; q < nsjn; ++q) {
934 res.WX[q].station = a.sjnset[q] + 1;
935 res.WX[q].x = G[q].x;
936 res.WX[q].W = W[q];
937 res.WX[q].tail = Tail[q];
938 }
939 return res;
940}
941
942} // namespace pfqn
943} // namespace line
944
945#endif // LINE_API_PFQN_SJN_H
InputError(const std::string &what)
Definition error.h:105
void fill(const T &x)
Definition matrix.h:108
NumericError(const std::string &what)
Definition error.h:112
SjnStarvationError(const std::string &what)
Definition pfqn_sjn.h:117
The exception types the port throws.
Dense matrix and non-owning view.
@ Tail
the geometric single-class identity
AmvaResult< T > pfqn_bs(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const std::vector< AmvaSched > &type, double tol=1e-6, std::size_t maxiter=1000, const Matrix< T > &QN0=Matrix< T >())
Bard-Schweitzer approximate MVA.
Definition pfqn_bs.h:73
SjnResult pfqn_amvasjn(const Matrix< double > &L, const std::vector< double > &N, const std::vector< double > &Z, const Matrix< double > &scv, const std::vector< std::size_t > &sjnset, const Matrix< double > &V, const SjnOptions &options)
Schweitzer fixed point counterpart of pfqn_mvasjn.
Definition pfqn_sjn.h:801
SjnResult pfqn_mvasjn(const Matrix< double > &L, const std::vector< double > &N, const std::vector< double > &Z, const Matrix< double > &scv, const std::vector< std::size_t > &sjnset, const Matrix< double > &V, const SjnOptions &options)
Exact-lattice MVA for closed networks with SJN stations, the unidirectional scheme of Kant 1992.
Definition pfqn_sjn.h:642
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
Bard-Schweitzer approximate MVA.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
Matrix< T > RN
(M x R) residence time
Definition pfqn_bs.h:52
std::vector< T > XN
(R) throughput
Definition pfqn_bs.h:49
Matrix< T > UN
(M x R) utilization
Definition pfqn_bs.h:51
Matrix< T > QN
(M x R) queue length
Definition pfqn_bs.h:50
Options of the SJN solvers, the fields sjn_args fills in.
Definition pfqn_sjn.h:80
std::size_t ns
grid subdivisions, must be even
Definition pfqn_sjn.h:81
double umax
utilization cap, strictly below one
Definition pfqn_sjn.h:86
double Lfactor
grid extent in units of the largest mean service time
Definition pfqn_sjn.h:82
std::vector< int > prio
distinct levels, 1 = highest; empty for the pooled reading
Definition pfqn_sjn.h:83
std::size_t iter_max
Definition pfqn_sjn.h:85
The conditional waiting time profile at one SJN station, the reference's WX.
Definition pfqn_sjn.h:90
Matrix< double > tail
(R x 3), the tail parameters a, b, c
Definition pfqn_sjn.h:94
Matrix< double > W
(ngrid x R)
Definition pfqn_sjn.h:93
std::vector< double > x
the grid
Definition pfqn_sjn.h:92
std::size_t station
1-based row index into L
Definition pfqn_sjn.h:91
Return block of pfqn_mvasjn and pfqn_amvasjn.
Definition pfqn_sjn.h:98
Matrix< double > CN
(M x R)
Definition pfqn_sjn.h:100
Matrix< double > QN
Definition pfqn_sjn.h:100
Matrix< double > UN
Definition pfqn_sjn.h:100
std::vector< double > XN
(R)
Definition pfqn_sjn.h:99
std::vector< SjnProfile > WX
Definition pfqn_sjn.h:101
bool capped
the utilization cap was binding somewhere
Definition pfqn_sjn.h:104
bool converged
always true for the lattice recursion
Definition pfqn_sjn.h:103