LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
pfqn_qdlin.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_QDLIN_H
6#define LINE_API_PFQN_QDLIN_H
7
8/**
9 * @file
10 * @ingroup api_pfqn
11 * QD-LIN: the Linearizer arm of AMVA-LD, on a plain demand matrix.
12 *
13 * Array-level twin of what SolverMVA computes for method='qdlin': the
14 * Linearizer of Chandy and Neuse, Commun. ACM 25(2), 1982, run inside the
15 * queue-dependent AMVA framework of Casale, Perez and Wang (IFIP PERFORMANCE
16 * 2015), so the load-dependent term g_k is evaluated at the CORRECTED
17 * arrival-instant queue rather than at the plain one.
18 *
19 * Port of python/line_solver/api/pfqn/qdlin.py, which is itself a transcription
20 * of solver_amvald restricted to the domain a demand matrix describes: closed
21 * classes only, one chain per class, unit visits, PS queueing stations and one
22 * optional delay carrying Z. Within that domain it reproduces the native-Python
23 * SolverMVA(model,'qdlin') to machine precision on random instances, which is
24 * what this kernel is for; it is NOT an independent re-derivation.
25 *
26 * TWO PROPERTIES OF THE REFERENCE ARE REPRODUCED DELIBERATELY, not inherited by
27 * accident, and a caller comparing against a textbook Linearizer sees both:
28 *
29 * 1. THE GAMMA CORRECTION IS CLASS-AGGREGATE, STORED IN SLICE 0. solver_amvald
30 * allocates the (K, M, K) per-class Linearizer array for qdlin but writes
31 * the class-aggregate correction into it with a two-subscript assignment,
32 * gamma(s,k) = sum_r Q_s(k,r)/(Nt-1) - sum_r Q(k,r)/Nt, which MATLAB
33 * linear-indexes to (s,k,1). Slices 1..K-1 stay zero while every reader
34 * indexes gamma per class, so the correction reaching the residence time is
35 * N_0*gamma(r,k,0) - [r==0]*gamma(r,k,0): the aggregate correction scaled by
36 * the population of CHAIN 0 alone, with the self term removed only for
37 * chain 0. It coincides with the queue-dependent AMVA form (Nt-1)*gamma_agg
38 * iff K = 1, so single-chain models are unaffected and multichain ones are
39 * not. method='lin' takes the per-class form instead.
40 * 2. A SINGLE-SERVER STATION STILL CARRIES A SOFTMIN TERM. The multiserver
41 * factor is pfqn_lldfun(1 + arrival-instant total, {}, nservers), whose
42 * softmin at c = 1 is not exactly 1, so qdlin does not reduce to a textbook
43 * single-server AMVA even when every station has one server.
44 *
45 * MU AND NSERVERS ARE DIFFERENT MECHANISMS, unlike in pfqn_qdamva, which folds
46 * the multiserver curve into mu. Here mu is sn.lldscaling, an interpolated rate
47 * multiplier per station, and nservers is the server count feeding the softmin
48 * term. A c-server station is nservers[k] = c, NOT a mu row of min(1..smax, c);
49 * passing the latter reproduces Queue.setLoadDependence, a different station.
50 *
51 * THE WAIT-FACTOR FLOOR IS A SEPARATE KNOB from the convergence tolerance, and
52 * is the native-Python solver's: MATLAB and the C++ solver_amvald do not clamp
53 * the wait factor at all. It is load-bearing for qdlin, whose class-aggregate
54 * correction drives the factor negative at a lightly loaded station.
55 *
56 * Arithmetic: TRANSCENDENTAL-GATED, inherited whole from pfqn_lldfun.
57 */
58
59#include <algorithm>
60#include <cmath>
61#include <cstddef>
62#include <limits>
63#include <vector>
64
66#include "line/num/number.h"
67#include "line/util/error.h"
68#include "line/util/matrix.h"
69
70namespace line {
71namespace pfqn {
72
73/** What `pfqn_qdlin` returns: the fixed point and how it was reached. */
74template <class T>
76 Matrix<T> Q; ///< (M x R) mean queue lengths at the queueing stations
77 Matrix<T> U; ///< (M x R) per-class utilizations, the analytic T*S/c
78 Matrix<T> R; ///< (M x R) per-class residence times
79 Matrix<T> X; ///< (1 x R) per-class throughputs
80 Matrix<T> C; ///< (1 x R) per-class cycle times, think time included
81 std::size_t iter = 0; ///< number of forward evaluations performed
82};
83
84namespace detail {
85
86/** One forward evaluation, solver_amvald_forward restricted to PS and INF. */
87template <class T>
88void qdlin_forward(const Matrix<T>& ST, const std::vector<double>& srv,
89 const std::vector<char>& isdelay, const Matrix<T>& mu,
90 const std::vector<Matrix<T>>& gamma, const Matrix<T>& Qin,
91 const std::vector<T>& Nin, const std::vector<std::size_t>& nnz, double wtol,
92 Matrix<T>& W, Matrix<T>& STeff) {
93 const std::size_t Ms = static_cast<std::size_t>(ST.rows());
94 const std::size_t K = static_cast<std::size_t>(ST.cols());
95 const T zero = num_traits<T>::from_int(0);
96 const T one = num_traits<T>::from_int(1);
97
98 T Ntin = zero;
99 for (std::size_t r = 0; r < K; ++r) Ntin += Nin[r];
100 const T delta = (num_traits<T>::to_double(Ntin) > 0.0) ? T((Ntin - one) / Ntin) : one;
101
102 std::vector<T> dcl(K, one);
103 for (std::size_t r : nnz) dcl[r] = T((Nin[r] - one) / Nin[r]);
104
105 // Arrival-instant queue lengths, class-aggregate and per class.
106 std::vector<T> interp(Ms, zero);
107 Matrix<T> totArvl(Ms, K, zero);
108 for (std::size_t k = 0; k < Ms; ++k) {
109 T sumQk = zero;
110 for (std::size_t r : nnz) sumQk += Qin(k, r);
111 interp[k] = T(delta * sumQk);
112 for (std::size_t r : nnz) totArvl(k, r) = T(dcl[r] * Qin(k, r) + sumQk - Qin(k, r));
113 }
114
115 // Load-dependent term, evaluated at the gamma-corrected arrival-instant queue.
116 Matrix<T> lldterm(Ms, K, one);
117 const std::vector<double> noservers;
118 if (!nnz.empty()) {
119 std::vector<T> arg(Ms, zero);
120 for (std::size_t r : nnz) {
121 for (std::size_t k = 0; k < Ms; ++k) {
122 T corr = zero;
123 for (std::size_t c : nnz) corr += T(Nin[c] * gamma[r](k, c));
124 corr -= gamma[r](k, r);
125 arg[k] = T(one + interp[k] + corr);
126 }
127 const std::vector<T> g = pfqn_lldfun(arg, mu, noservers);
128 for (std::size_t k = 0; k < Ms; ++k) lldterm(k, r) = g[k];
129 }
130 } else {
131 std::vector<T> arg(Ms, zero);
132 for (std::size_t k = 0; k < Ms; ++k) arg[k] = T(one + interp[k]);
133 const std::vector<T> g = pfqn_lldfun(arg, mu, noservers);
134 for (std::size_t k = 0; k < Ms; ++k)
135 for (std::size_t r = 0; r < K; ++r) lldterm(k, r) = g[k];
136 }
137
138 // Multiserver term; the 'default' rule leaves PS on the softmin arm.
139 std::vector<T> msarg(Ms, zero);
140 if (!nnz.empty() && num_traits<T>::to_double(Ntin) > 0.0) {
141 const T shrink = T((Ntin - one) / Ntin);
142 for (std::size_t k = 0; k < Ms; ++k) {
143 T acc = zero;
144 for (std::size_t s : nnz) {
145 T gs = zero;
146 for (std::size_t r : nnz) gs += T(shrink * Nin[r] * gamma[s](k, r));
147 acc += gs;
148 }
149 const T mean = T(acc / num_traits<T>::from_int(static_cast<long>(nnz.size())));
150 msarg[k] = T(one + interp[k] + mean);
151 }
152 } else {
153 for (std::size_t k = 0; k < Ms; ++k) msarg[k] = T(one + interp[k]);
154 }
155 const Matrix<T> nolld;
156 const std::vector<T> msterm = pfqn_lldfun(msarg, nolld, srv);
157
158 STeff = Matrix<T>(Ms, K, zero);
159 for (std::size_t r : nnz)
160 for (std::size_t k = 0; k < Ms; ++k)
161 STeff(k, r) = T(ST(k, r) * lldterm(k, r) * msterm[k]);
162
163 W = Matrix<T>(Ms, K, zero);
164 const T floor = num_traits<T>::from_double(wtol);
165 for (std::size_t r : nnz) {
166 for (std::size_t k = 0; k < Ms; ++k) {
167 if (isdelay[k]) {
168 W(k, r) = STeff(k, r);
169 continue;
170 }
171 T corr = zero;
172 for (std::size_t c : nnz) corr += T(Nin[c] * gamma[r](k, c));
173 corr -= gamma[r](k, r);
174 const T factor = T(one + totArvl(k, r) + corr);
175 W(k, r) = T(STeff(k, r) * (factor < floor ? floor : factor));
176 }
177 }
178}
179
180} // namespace detail
181
182/**
183 * @brief QD-LIN: the Linearizer arm of AMVA-LD, on a plain demand matrix.
184 *
185 * @param L (M x R) service demand matrix, queueing stations only.
186 * @param N (R) population vector, finite.
187 * @param Z (R) think time vector; a delay station carrying it is
188 * appended to the station list when any entry is positive,
189 * exactly as the equivalent Network would hold one. Empty
190 * means no think time.
191 * @param mu (M x smax) load-dependent rate multipliers, sn.lldscaling;
192 * empty means none.
193 * @param nservers (M) server counts; empty means one server everywhere.
194 * @param tol convergence tolerance on the queue lengths, LINE's iter_tol.
195 * @param maxiter iteration budget, LINE's iter_max. The outer sweep and each
196 * inner sweep are capped at sqrt(maxiter) and the total number
197 * of forward evaluations at min(maxiter, 10000).
198 * @param wtol floor on the AMVA wait factor, LINE's options.tol. A
199 * DIFFERENT knob from tol, with its own default: SolverMVA
200 * passes iter_tol to the fixed point but never sets
201 * options.tol, so the floor stays at the lineDefaults 1e-4
202 * while the fixed point converges to 1e-6.
203 */
204template <class T>
205QdLinResult<T> pfqn_qdlin(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z,
206 const Matrix<T>& mu, const std::vector<double>& nservers,
207 double tol = 1e-6, std::size_t maxiter = 1000, double wtol = 1e-4) {
209 "pfqn_qdlin requires transcendental arithmetic");
210 const std::size_t M = static_cast<std::size_t>(L.rows());
211 const std::size_t K = static_cast<std::size_t>(L.cols());
212 if (N.size() != K)
213 throw InputError("pfqn_qdlin: the population vector must have one entry per class");
214 if (!Z.empty() && Z.size() != K)
215 throw InputError("pfqn_qdlin: the think-time vector must have one entry per class");
216 if (!nservers.empty() && nservers.size() != M)
217 throw InputError("pfqn_qdlin: the server-count vector must have one entry per station");
218 for (std::size_t r = 0; r < K; ++r)
219 if (!std::isfinite(num_traits<T>::to_double(N[r])))
220 throw InputError(
221 "pfqn_qdlin: an infinite population is not supported, closed classes only");
222
223 const T zero = num_traits<T>::from_int(0);
224 const T one = num_traits<T>::from_int(1);
225
226 QdLinResult<T> out;
227 out.Q = Matrix<T>(M, K, zero);
228 out.U = Matrix<T>(M, K, zero);
229 out.R = Matrix<T>(M, K, zero);
230 out.X = Matrix<T>(1, K, zero);
231 out.C = Matrix<T>(1, K, zero);
232
233 T Ntot = zero;
234 for (std::size_t r = 0; r < K; ++r) Ntot += N[r];
235 if (!(num_traits<T>::to_double(Ntot) > 0.0)) return out;
236
237 // Station list: the delay, when there is one, then the queueing stations.
238 bool hasDelay = false;
239 for (std::size_t r = 0; r < K && !Z.empty(); ++r)
240 if (num_traits<T>::to_double(Z[r]) > 0.0) hasDelay = true;
241 const std::size_t off = hasDelay ? 1 : 0;
242 const std::size_t Ms = M + off;
243
244 Matrix<T> ST(Ms, K, zero);
245 std::vector<double> srv(Ms, 1.0);
246 std::vector<char> isdelay(Ms, 0);
247 if (hasDelay) {
248 for (std::size_t r = 0; r < K; ++r) ST(0, r) = Z[r];
249 srv[0] = std::numeric_limits<double>::infinity();
250 isdelay[0] = 1;
251 }
252 for (std::size_t k = 0; k < M; ++k) {
253 for (std::size_t r = 0; r < K; ++r) ST(k + off, r) = L(k, r);
254 srv[k + off] = nservers.empty() ? 1.0 : nservers[k];
255 }
256 Matrix<T> muFull;
257 if (!mu.empty()) {
258 const std::size_t smax = static_cast<std::size_t>(mu.cols());
259 muFull = Matrix<T>(Ms, smax, one);
260 for (std::size_t k = 0; k < M; ++k)
261 for (std::size_t j = 0; j < smax; ++j) muFull(k + off, j) = mu(k, j);
262 }
263
264 std::vector<std::size_t> nnz;
265 for (std::size_t r = 0; r < K; ++r)
266 if (num_traits<T>::to_double(N[r]) > 0.0) nnz.push_back(r);
267
268 // Balanced initialization, as in solver_amvald.
269 Matrix<T> Q(Ms, K, zero);
270 const T Msf = num_traits<T>::from_int(static_cast<long>(Ms));
271 const T share = T(one / Msf);
272 for (std::size_t r : nnz)
273 for (std::size_t k = 0; k < Ms; ++k) Q(k, r) = T(share * N[r]);
274 std::vector<T> X(K, zero);
275 for (std::size_t r = 0; r < K; ++r) {
276 T col = zero;
277 for (std::size_t k = 0; k < Ms; ++k) col += ST(k, r);
278 if (num_traits<T>::to_double(col) > 0.0) X[r] = T(one / col);
279 }
280
281 Matrix<T> Umat(Ms, K, zero);
282 for (std::size_t r : nnz)
283 for (std::size_t k = 0; k < Ms; ++k)
284 Umat(k, r) = std::isfinite(srv[k])
285 ? T(ST(k, r) * X[r] / num_traits<T>::from_double(srv[k]))
286 : T(ST(k, r) * X[r]);
287
288 std::vector<Matrix<T>> gamma(K, Matrix<T>(Ms, K, zero));
289 Matrix<T> Tput(Ms, K, zero);
290
291 const double omicron = 0.5;
292 const T om = num_traits<T>::from_double(omicron);
293 const T omc = num_traits<T>::from_double(1.0 - omicron);
294 const double maxSweep = std::sqrt(static_cast<double>(maxiter));
295 const std::size_t maxTotiter = std::min<std::size_t>(maxiter, 10000);
296
297 Matrix<T> W, STeff(Ms, K, zero);
298 Matrix<T> Qouter = Q;
299 std::size_t outerIter = 0;
300 while (static_cast<double>(outerIter) < maxSweep && out.iter <= maxTotiter) {
301 if (outerIter >= 2) {
302 double gap = 0.0;
303 for (std::size_t k = 0; k < Ms; ++k)
304 for (std::size_t r = 0; r < K; ++r)
305 gap = std::max(gap, std::fabs(num_traits<T>::to_double(Q(k, r)) -
306 num_traits<T>::to_double(Qouter(k, r))));
307 if (gap <= tol) break;
308 }
309 ++outerIter;
310 Qouter = Q;
311
312 // Linearizer recursion: one sweep at each reduced population N - 1_s.
313 bool exhausted = false;
314 for (std::size_t s = 0; s < K && !exhausted; ++s) {
315 if (!(num_traits<T>::to_double(N[s]) > 0.0)) continue;
316 std::vector<T> Ns = N;
317 Ns[s] = T(Ns[s] - one);
318 const T shrink = T((Ntot - one) / Ntot);
319 Matrix<T> Qs = Q;
320 for (std::size_t k = 0; k < Ms; ++k)
321 for (std::size_t r = 0; r < K; ++r) Qs(k, r) = T(Qs(k, r) * shrink);
322 std::vector<T> Xs = X;
323 for (std::size_t r = 0; r < K; ++r) Xs[r] = T(Xs[r] * shrink);
324
325 std::size_t iterS = 0;
326 Matrix<T> QsPrev = Qs;
327 while (static_cast<double>(iterS) <= maxSweep) {
328 if (iterS >= 2) {
329 double gap = 0.0;
330 for (std::size_t k = 0; k < Ms; ++k)
331 for (std::size_t r = 0; r < K; ++r)
332 gap = std::max(gap, std::fabs(num_traits<T>::to_double(Qs(k, r)) -
333 num_traits<T>::to_double(QsPrev(k, r))));
334 if (gap <= tol) break;
335 }
336 ++iterS;
337 QsPrev = Qs;
338 const std::vector<T> XsPrev = Xs;
339
340 detail::qdlin_forward(ST, srv, isdelay, muFull, gamma, QsPrev, Ns, nnz, wtol, W,
341 STeff);
342 ++out.iter;
343 if (out.iter >= maxTotiter) {
344 exhausted = true;
345 break;
346 }
347
348 for (std::size_t r : nnz) {
349 T wsum = zero;
350 for (std::size_t k = 0; k < Ms; ++k) wsum += W(k, r);
351 if (num_traits<T>::to_double(wsum) == 0.0 ||
352 !(num_traits<T>::to_double(Ns[r]) > 0.0)) {
353 Xs[r] = zero;
354 } else if (num_traits<T>::to_double(wsum) > 1e-14) {
355 Xs[r] = T(om * Ns[r] / wsum + omc * XsPrev[r]);
356 } else {
357 Xs[r] = XsPrev[r];
358 }
359 for (std::size_t k = 0; k < Ms; ++k)
360 Qs(k, r) = T(om * Xs[r] * W(k, r) + omc * QsPrev(k, r));
361 }
362 }
363
364 // Class-aggregate correction into slice 0, see the header.
365 if (num_traits<T>::to_double(Ntot) > 1.0) {
366 for (std::size_t k = 0; k < Ms; ++k) {
367 T a = zero, b = zero;
368 for (std::size_t r = 0; r < K; ++r) {
369 a += QsPrev(k, r);
370 b += Qouter(k, r);
371 }
372 gamma[s](k, 0) = T(a / (Ntot - one) - b / Ntot);
373 }
374 } else {
375 for (std::size_t k = 0; k < Ms; ++k) gamma[s](k, 0) = zero;
376 }
377 }
378 if (exhausted) break;
379
380 // Sweep at the full population N.
381 std::size_t innerIter = 0;
382 Matrix<T> Qprev = Q;
383 while (static_cast<double>(innerIter) <= maxSweep) {
384 if (innerIter >= 2) {
385 double gap = 0.0;
386 for (std::size_t k = 0; k < Ms; ++k)
387 for (std::size_t r = 0; r < K; ++r)
388 gap = std::max(gap, std::fabs(num_traits<T>::to_double(Q(k, r)) -
389 num_traits<T>::to_double(Qprev(k, r))));
390 if (gap <= tol) break;
391 }
392 ++innerIter;
393 Qprev = Q;
394 const std::vector<T> Xprev = X;
395 const Matrix<T> Uprev = Umat;
396
397 detail::qdlin_forward(ST, srv, isdelay, muFull, gamma, Qprev, N, nnz, wtol, W, STeff);
398 ++out.iter;
399 if (out.iter >= maxTotiter) {
400 exhausted = true;
401 break;
402 }
403
404 for (std::size_t r : nnz) {
405 T wsum = zero;
406 for (std::size_t k = 0; k < Ms; ++k) wsum += W(k, r);
407 if (num_traits<T>::to_double(wsum) == 0.0) {
408 X[r] = zero;
409 } else {
410 out.C(0, r) = wsum;
411 if (num_traits<T>::to_double(wsum) > 1e-14)
412 X[r] = T(om * N[r] / wsum + omc * Xprev[r]);
413 else
414 X[r] = Xprev[r];
415 }
416 for (std::size_t k = 0; k < Ms; ++k) {
417 Q(k, r) = T(om * X[r] * W(k, r) + omc * Qprev(k, r));
418 Tput(k, r) = X[r];
419 Umat(k, r) = T(om * STeff(k, r) * X[r] + omc * Uprev(k, r));
420 }
421 }
422 }
423 if (exhausted) break;
424 }
425 // Utilization capping, as in solver_amvald: a queueing station whose class
426 // utilizations sum above one has them renormalized in proportion to STeff.
427 // Delay stations are exempt.
428 for (std::size_t k = 0; k < Ms; ++k) {
429 if (isdelay[k]) continue;
430 T usum = zero;
431 for (std::size_t r = 0; r < K; ++r) usum += Umat(k, r);
432 if (!(num_traits<T>::to_double(usum) > 1.0)) continue;
433 T denom = zero;
434 for (std::size_t r = 0; r < K; ++r) denom += T(STeff(k, r) * X[r]);
435 if (!(num_traits<T>::to_double(denom) > 0.0)) continue;
436 for (std::size_t r = 0; r < K; ++r)
437 if (num_traits<T>::to_double(STeff(k, r)) > 0.0)
438 Umat(k, r) = T(STeff(k, r) * X[r] / denom);
439 }
440
441 // WHICH UTILIZATION SolverMVA REPORTS DEPENDS ON THE MODEL. Its analyzer
442 // forwards the iterated Uchain to sn_deaggregate_chain_results ONLY under
443 // lld, cd or jd scaling; with none of those the deaggregation recomputes
444 // T*S/c from the NOMINAL demand instead, and the two differ by the
445 // iteration residual. Reproduced on the same test, mu being the only one of
446 // the three a demand matrix can carry.
447 const bool iteratedU = !mu.empty();
448 for (std::size_t r : nnz) {
449 out.X(0, r) = X[r];
450 for (std::size_t k = 0; k < M; ++k) {
451 const std::size_t ks = k + off;
452 out.Q(k, r) = Q(ks, r);
453 out.U(k, r) = iteratedU ? Umat(ks, r)
454 : (std::isfinite(srv[ks])
455 ? T(ST(ks, r) * X[r] /
457 : T(ST(ks, r) * X[r]));
458 out.R(k, r) = (num_traits<T>::to_double(Tput(ks, r)) > 0.0)
459 ? T(Q(ks, r) / Tput(ks, r))
460 : zero;
461 }
462 }
463 return out;
464}
465
466/** Overload without a load-dependent lattice or explicit server counts. */
467template <class T>
468QdLinResult<T> pfqn_qdlin(const Matrix<T>& L, const std::vector<T>& N, const std::vector<T>& Z,
469 double tol = 1e-6, std::size_t maxiter = 1000, double wtol = 1e-4) {
470 return pfqn_qdlin(L, N, Z, Matrix<T>(), std::vector<double>(), tol, maxiter, wtol);
471}
472
473} // namespace pfqn
474} // namespace line
475
476#endif // LINE_API_PFQN_QDLIN_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
bool empty() const
Definition matrix.h:92
The exception types the port throws.
Dense matrix and non-owning view.
std::vector< T > pfqn_lldfun(const std::vector< T > &n, const Matrix< T > &lldscaling, const std::vector< double > &nservers)
AMVA-QD limited-load-dependence function.
Definition pfqn_lldfun.h:81
QdLinResult< T > pfqn_qdlin(const Matrix< T > &L, const std::vector< T > &N, const std::vector< T > &Z, const Matrix< T > &mu, const std::vector< double > &nservers, double tol=1e-6, std::size_t maxiter=1000, double wtol=1e-4)
QD-LIN: the Linearizer arm of AMVA-LD, on a plain demand matrix.
Definition pfqn_qdlin.h:205
Number-type abstraction for the templated API port.
AMVA-QD limited-load-dependence function.
What pfqn_qdlin returns: the fixed point and how it was reached.
Definition pfqn_qdlin.h:75
std::size_t iter
number of forward evaluations performed
Definition pfqn_qdlin.h:81
Matrix< T > X
(1 x R) per-class throughputs
Definition pfqn_qdlin.h:79
Matrix< T > Q
(M x R) mean queue lengths at the queueing stations
Definition pfqn_qdlin.h:76
Matrix< T > U
(M x R) per-class utilizations, the analytic T*S/c
Definition pfqn_qdlin.h:77
Matrix< T > R
(M x R) per-class residence times
Definition pfqn_qdlin.h:78
Matrix< T > C
(1 x R) per-class cycle times, think time included
Definition pfqn_qdlin.h:80