LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
lqn_analyzers.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_LN_LQN_ANALYZERS_H
6#define LINE_SOLVERS_LN_LQN_ANALYZERS_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The @@SolverLN methods that solver_ln.h does not carry.
12 *
13 * solver_ln.h ports the fixed point itself (construct, buildLayers,
14 * buildLayersRecursive, init, initInterlock, converged, analyze, post,
15 * updateMetricsDefault, updatePopulations, updateThinkTimes, updateLayers,
16 * updateRoutingProbabilities, getEntryServiceMatrix, getEnsembleAvg), and
17 * lqn_helpers.h carries lqn_fwd_rendezvous and the lqn_overtake_markov chain.
18 * What is here is the remainder of matlab/src/solvers/LN/@@SolverLN:
19 *
20 * overtake_prob - the reduced three-state CTMC
21 * overtake_prob_markov - the input mapping onto the LQNS overtaking chain
22 * convergedStoch - the Robbins-Monro / Polyak-Ruppert controller
23 *
24 * plus thin wrappers, below, naming the three @@SolverLN entry points whose work
25 * lives on the solver itself: getSensitivityTable, getTranAvg and getCdfRespT.
26 * updateMetricsMomentBased (the `moment3` method) is likewise a member of
27 * SolverLN, because it replaces updateMetricsDefault inside the iteration and
28 * writes the same state.
29 *
30 * WHAT SolverLN CALLS AND WHAT IT DOES NOT. `lqn_overtake_prob_markov` IS wired
31 * in: solver_ln.h computes the `servt_ph1` / `servt_ph2` split and calls it from
32 * update_metrics, where updateMetricsDefault.m:313-351 calls
33 * overtake_prob_markov. It is still written as a free function over quantities
34 * the caller supplies, and the tested phase residence `xj` is still a PARAMETER
35 * rather than something derived here, because the split belongs to the solver's
36 * iterate and inventing it from a phase-1 model would be a guess dressed as a
37 * result. `overtake_prob`, the reduced three-state CTMC, is the alternative the
38 * reference keeps beside it and no path selects.
39 *
40 * `LnStochController` IS wired in, since `layer_solver = 'ssa'` gave the port a
41 * stochastic layer engine: `SolverLN::iterate` builds one whenever that engine
42 * is selected, drives `relax_omega` from it and reports the Polyak-Ruppert
43 * average as the final iterate. It is NOT used for any deterministic engine --
44 * the two convergence tests both rewrite `results`, so only one may be in
45 * force.
46 *
47 * INCLUDE ORDER. This header needs `LayerResult` and `SolverLN` complete, so it
48 * is parsed after solver_ln.h, which includes it at its foot; the declaration
49 * solver_ln.h needs stands near the top of that file. Do not turn the include
50 * below into a forward declaration -- LnStochController copies LayerResults.
51 */
52
53#include <algorithm>
54#include <cmath>
55#include <cstddef>
56#include <limits>
57#include <string>
58#include <vector>
59
61#include "line/num/number.h"
64#include "line/util/error.h"
65#include "line/util/matrix.h"
66
67namespace line {
68namespace ln {
69
70using lang::CallType;
71using lang::Distrib;
73using lqn::LqnStruct;
74
75// ---------------------------------------------------------------------------
76// overtake_prob
77// ---------------------------------------------------------------------------
78
79/** Stationary law of the three-state overtaking chain, in its own order. */
80template <class T>
84
85/**
86 * Stationary law of the reduced overtaking chain of overtake_prob.m.
87 *
88 * The chain is the cycle idle -> phase 1 -> phase 2 -> idle, with rates lambda,
89 * 1/S1 and 1/S2. A cycle visits every state exactly once per traversal, so the
90 * stationary probabilities are proportional to the mean holding times
91 * (1/lambda, S1, S2); the reference reaches the same numbers by solving the
92 * augmented singular system in least squares, which is the same answer arrived
93 * at less directly and only for a floating T. Scaling the ratios by lambda
94 * removes the reciprocal, so nothing here divides by a rate.
95 *
96 * The caller must have established lambda > 0; at lambda = 0 the chain is
97 * absorbed in `idle` and has no unique stationary law.
98 */
99template <class T>
100OvertakeCtmcState<T> lqn_overtake_ctmc(const T& S1, const T& S2, const T& lambda) {
101 const T one = num_traits<T>::from_int(1);
102 const T den = T(one + lambda * (S1 + S2));
104 pi.idle = T(one / den);
105 pi.phase1 = T(lambda * S1 / den);
106 pi.phase2 = T(lambda * S2 / den);
107 return pi;
108}
109
110/**
111 * Probability that an arrival at an entry finds the server in phase 2.
112 *
113 * Port of @@SolverLN/overtake_prob.m. S1 and S2 are the entry's phase-1 and
114 * phase-2 service times, lambda the arrival rate at the entry (the reference
115 * falls back to the task throughput when the entry's own is not yet resolved,
116 * which is the caller's choice to make) and `mult` the task multiplicity.
117 *
118 * PASTA is what licenses reading the answer off the time-stationary law: the
119 * arrival stream is Poisson, so arrivals see time averages and the probability
120 * an arrival finds phase 2 is the probability the chain is in phase 2.
121 *
122 * The multi-server branch is an APPROXIMATION in the reference and stays one
123 * here (overtake_prob.m, lines 77 to 91): the phase-2 fraction of a busy server
124 * scaled by the utilization, saturating at the bare phase-2 fraction once the
125 * offered load reaches one server's worth. It is not a c-server chain and does
126 * not converge to one.
127 */
128template <class T>
129T lqn_overtake_prob(const T& S1, const T& S2, const T& lambda, double mult) {
130 const T zero = num_traits<T>::from_int(0);
131 const T one = num_traits<T>::from_int(1);
132
133 // Each of these makes the phase-2 interval unobservable rather than small,
134 // so the chain is not merely near-degenerate, it does not exist.
138 return zero;
139
140 auto clamp01 = [&](const T& x) { return x < zero ? zero : (x > one ? one : x); };
141
142 if (mult == 1.0) return clamp01(lqn_overtake_ctmc(S1, S2, lambda).phase2);
143
144 // An infinite server is the c -> Inf limit of the same expression: the load
145 // per server vanishes, so an arrival never meets a busy one.
146 if (!std::isfinite(mult)) return zero;
147 const T phase_frac = T(S2 / (S1 + S2));
148 const T rho = T(lambda * (S1 + S2) / num_traits<T>::from_double(mult));
149 if (rho >= one) return clamp01(phase_frac);
150 return clamp01(T(phase_frac * rho));
151}
152
153// ---------------------------------------------------------------------------
154// overtake_prob_markov
155// ---------------------------------------------------------------------------
156
157namespace detail {
158
159/** Entry that owns an activity, via actsof; 0 when the activity has no entry. */
160template <class T>
161std::size_t ln_entry_of_activity(const LqnStruct<T>& lqn, std::size_t aidx) {
162 for (std::size_t e = 1; e <= lqn.nentries; ++e) {
163 const std::size_t eabs = lqn.eshift + e;
164 for (std::size_t a : lqn.actsof[eabs])
165 if (a == aidx) return eabs;
166 }
167 return 0;
168}
169
170} // namespace detail
171
172/**
173 * Overtaking probability at a server entry, through the LQNS phased-server
174 * chain rather than the reduced CTMC above.
175 *
176 * Port of @@SolverLN/overtake_prob_markov.m: the input-mapping layer that turns
177 * the LayeredNetworkStruct plus the current fixed-point iterate into the
178 * per-client-phase slice parameters lqn_overtake_markov consumes, one client
179 * entry at a time, summing the contributions and truncating to 1 as LQNS does
180 * in Markov_Phased_Server::PrOT_e.
181 *
182 * `xj` is the tested server phase's residence time, `self.servt_ph2(eidx)` in
183 * the reference. It is a parameter because the C++ SolverLN has no phase split
184 * to read it from; see the file header.
185 *
186 * `servt` and `tput` are indexed by element 1..nidx and `callresidt` by call
187 * 1..ncalls, which is exactly the layout SolverLN::state_servt, state_tput and
188 * state_callresidt return.
189 *
190 * A trap worth naming: the reference's own comment says the caller activities
191 * are the SYNCHRONOUS ones, but the selection it writes is unfiltered, so an
192 * asynchronous or forwarding caller into the same entry also contributes a
193 * client entry. That is reproduced here; the per-phase call scan below does
194 * filter to SYNC, which is where the distinction actually bites.
195 */
196template <class T>
197T lqn_overtake_prob_markov(const LqnStruct<T>& lqn, const std::vector<T>& servt,
198 const std::vector<T>& callresidt, const std::vector<T>& tput,
199 std::size_t eidx, const T& xj) {
200 const T zero = num_traits<T>::from_int(0);
201 const T one = num_traits<T>::from_int(1);
202
203 if (servt.size() <= lqn.nidx || tput.size() <= lqn.nidx ||
204 callresidt.size() <= lqn.ncalls)
205 throw InputError(
206 "lqn_overtake_prob_markov: servt and tput must be indexed 1..nidx and callresidt "
207 "1..ncalls, as SolverLN's state accessors return them");
208 if (eidx <= lqn.eshift || eidx > lqn.eshift + lqn.nentries)
209 throw InputError("lqn_overtake_prob_markov: eidx is not an entry index");
210
211 if (!(num_traits<T>::to_double(xj) > GlobalConstants::FineTol)) return zero;
212 const std::size_t server_tidx = lqn.parent[eidx];
213
214 // client entries that reach this server entry, in first-caller order
215 std::vector<std::size_t> caller_entries;
216 for (std::size_t c = 1; c <= lqn.ncalls; ++c) {
217 if (lqn.callpair_dst[c] != eidx) continue;
218 const std::size_t ceidx = detail::ln_entry_of_activity(lqn, lqn.callpair_src[c]);
219 if (ceidx == 0) continue;
220 bool known = false;
221 for (std::size_t v : caller_entries)
222 if (v == ceidx) known = true;
223 if (!known) caller_entries.push_back(ceidx);
224 }
225 if (caller_entries.empty()) return zero;
226
227 T prOt = zero;
228 for (std::size_t ceidx : caller_entries) {
229 const std::size_t ctidx = lqn.parent[ceidx];
230 const std::vector<std::size_t>& acts = lqn.actsof[ceidx];
231 if (acts.empty()) continue;
232
233 int maxPhaseA = 1;
234 for (std::size_t aidx : acts) {
235 const std::size_t a = aidx - lqn.ashift;
236 if (a >= 1 && a <= lqn.nacts && lqn.actphase[a] > maxPhaseA) maxPhaseA = lqn.actphase[a];
237 }
238 const std::size_t nStates = std::size_t(maxPhaseA) + 1;
239
240 Matrix<T> clientPhases(nStates, 5, zero);
241 clientPhases(0, 0) = one; // the think slice is never chopped by a call
242 const Distrib<T>& z = lqn.think[ctidx];
243 if (!z.disabled && z.mean > zero) clientPhases(0, 1) = z.mean;
244
245 std::vector<T> y_aj(nStates, zero);
246 for (int p = 1; p <= maxPhaseA; ++p) {
247 T nSlices = one, service = zero, y_ij = zero, y_ik = zero, tk_num = zero;
248 for (std::size_t aidx : acts) {
249 const std::size_t a = aidx - lqn.ashift;
250 if (a < 1 || a > lqn.nacts || lqn.actphase[a] != p) continue;
251 service = T(service + servt[aidx]);
252 for (std::size_t c : lqn.callsof[aidx]) {
253 if (lqn.calltype[c] != CallType::SYNC) continue;
254 const T y = lqn.callproc_mean[c];
255 if (y == zero) continue;
256 // a rendezvous cuts the phase into one more slice, which is
257 // what makes the client interruptible partway through it
258 nSlices = T(nSlices + y);
259 if (lqn.parent[lqn.callpair_dst[c]] == server_tidx) {
260 y_ij = T(y_ij + y);
261 } else {
262 y_ik = T(y_ik + y);
263 tk_num = T(tk_num + y * callresidt[c]);
264 }
265 }
266 }
267 const T t_k = y_ik > zero ? T(tk_num / y_ik) : zero;
268 const std::size_t row = std::size_t(p);
269 clientPhases(row, 0) = nSlices;
270 clientPhases(row, 1) = service;
271 clientPhases(row, 2) = y_ij;
272 clientPhases(row, 3) = y_ik;
273 clientPhases(row, 4) = t_k;
274 y_aj[row] = y_ij;
275 y_aj[0] = T(y_aj[0] + y_ij);
276 }
277 if (y_aj[0] == zero) continue; // this client never reaches the server task
278
279 T prVisit = one;
282 prVisit = T(tput[ceidx] / tput[ctidx]);
283
284 prOt = T(prOt + lqn_overtake_markov(clientPhases, prVisit, xj, y_aj));
285 }
286
287 // Contributions of independent client entries are summed, so nothing stops
288 // the total exceeding one; LQNS truncates rather than renormalizing.
289 if (prOt < zero) return zero;
290 return prOt > one ? one : prOt;
291}
292
293// ---------------------------------------------------------------------------
294// convergedStoch
295// ---------------------------------------------------------------------------
296
297// LnStochConfig itself lives in solver_ln.h: `SolverLN::iterate` names it by
298// value, and a non-dependent name must be complete where the template is
299// parsed, not where it is instantiated. Only the controller below is deferred.
300
301namespace detail {
302
303/**
304 * Running mean prev + (raw - prev)/k, tolerant of a NaN on either side.
305 *
306 * A stochastic layer solver can return NaN for a metric it did not estimate
307 * (an idle class in a short simulation run), and a single such sample would
308 * otherwise poison the average for every remaining iteration.
309 */
310template <class T>
311Matrix<T> ln_polyak(const Matrix<T>& prev, const Matrix<T>& raw, long k) {
312 Matrix<T> out(prev.rows(), prev.cols());
313 const T kk = num_traits<T>::from_int(k);
314 for (std::size_t i = 0; i < prev.rows(); ++i)
315 for (std::size_t j = 0; j < prev.cols(); ++j) {
316 T m = T(prev(i, j) + (raw(i, j) - prev(i, j)) / kk);
317 if (std::isnan(num_traits<T>::to_double(m))) m = raw(i, j);
318 if (std::isnan(num_traits<T>::to_double(m))) m = prev(i, j);
319 out(i, j) = m;
320 }
321 return out;
322}
323
324template <class T>
325std::vector<T> ln_polyak_vec(const std::vector<T>& prev, const std::vector<T>& raw, long k) {
326 std::vector<T> out(prev.size());
327 const T kk = num_traits<T>::from_int(k);
328 for (std::size_t i = 0; i < prev.size(); ++i) {
329 T m = T(prev[i] + (raw[i] - prev[i]) / kk);
330 if (std::isnan(num_traits<T>::to_double(m))) m = raw[i];
331 if (std::isnan(num_traits<T>::to_double(m))) m = prev[i];
332 out[i] = m;
333 }
334 return out;
335}
336
337} // namespace detail
338
339/**
340 * Convergence controller for an ensemble whose layers are solved by a NOISY
341 * method (simulation, or Monte Carlo normalizing constants).
342 *
343 * Port of @@SolverLN/convergedStoch.m. The deterministic test in
344 * SolverLN::converged cannot terminate against noise: the successive-difference
345 * error is bounded below by the standard error of the layer estimates, and the
346 * layer-reset confirmation step only resamples that noise. This replaces it
347 * with a stochastic approximation scheme:
348 *
349 * 1. Burn-in. Plain Picard at the relaxation init chose, to get near the
350 * fixed point fast while the noise still does not matter.
351 * 2. Robbins-Monro. `relax_omega` then decays as a0/k^alpha, so the iterate
352 * converges almost surely under the contraction assumption the
353 * deterministic iteration already makes plus zero-mean bounded-variance
354 * noise (Robbins and Monro, 1951). The caller must actually APPLY
355 * relax_omega to the fed-forward iterate for any of this to hold.
356 * 3. Polyak-Ruppert. Running averages of the layer results and of the
357 * reported iterate, which give the optimal O(1/sqrt(k)) rate and make the
358 * answer insensitive to a0 (Polyak and Juditsky, 1992).
359 * 4. Stopping on the DRIFT OF THE AVERAGE, not of the iterate. That drift
360 * decays like 1/k even under persistent noise, so the test terminates, and
361 * it self-calibrates: noisier layers hold the drift above tolerance longer
362 * and buy themselves more averaging.
363 *
364 * The reference averages QN, UN, RN, TN, AN and WN; LayerResult carries no AN,
365 * so five fields are averaged here and the sixth is not silently invented.
366 */
367template <class T>
369public:
370 explicit LnStochController(const LnStochConfig& cfg)
371 : cfg_(cfg), omega_(cfg.relax_burnin), err_(1, 0.0) {}
372
373 /**
374 * Fold iteration `it` in and say whether the iteration may stop. `it` counts
375 * from 1 and must advance by one per call. `layer_jobs` is the total closed
376 * population of each layer, which normalizes the drift so that layers of
377 * very different size contribute comparably.
378 */
379 bool update(int it, const std::vector<LayerResult<T>>& latest,
380 const std::vector<double>& layer_jobs, const std::vector<T>& servt,
381 const std::vector<T>& residt) {
382 if (it < 1) return false;
383 if (err_.size() <= std::size_t(it)) err_.resize(std::size_t(it) + 1, 0.0);
384
385 // Scheduled one iteration ahead, as in the reference: the step this sets
386 // is the one the NEXT updateMetrics applies.
387 if (it >= cfg_.burnin)
388 omega_ = std::min(1.0, cfg_.a0 / std::pow(std::max(1.0, double(it - cfg_.burnin + 1)),
389 cfg_.alpha));
390
391 if (it <= cfg_.burnin) {
392 err_[std::size_t(it)] = std::numeric_limits<double>::infinity();
393 return false;
394 }
395 if (start_ < 0) start_ = it;
396
397 const long k = k_ + 1;
398 double err = 0.0;
399 if (k == 1) {
400 avg_ = latest;
401 } else {
402 for (std::size_t e = 0; e < latest.size() && e < avg_.size(); ++e) {
403 const LayerResult<T> prev = avg_[e];
404 avg_[e].QN = detail::ln_polyak(prev.QN, latest[e].QN, k);
405 avg_[e].UN = detail::ln_polyak(prev.UN, latest[e].UN, k);
406 avg_[e].RN = detail::ln_polyak(prev.RN, latest[e].RN, k);
407 avg_[e].TN = detail::ln_polyak(prev.TN, latest[e].TN, k);
408 avg_[e].WN = detail::ln_polyak(prev.WN, latest[e].WN, k);
409 const double N = e < layer_jobs.size() ? layer_jobs[e] : 0.0;
410 if (N > 0.0) {
411 double dmax = 0.0;
412 for (std::size_t i = 0; i < prev.QN.rows(); ++i)
413 for (std::size_t j = 0; j < prev.QN.cols(); ++j) {
414 const double d = std::abs(num_traits<T>::to_double(avg_[e].QN(i, j)) -
415 num_traits<T>::to_double(prev.QN(i, j)));
416 if (!std::isnan(d) && d > dmax) dmax = d;
417 }
418 err += dmax / N;
419 }
420 }
421 }
422 k_ = k;
423
424 if (k == 1) {
425 servt_avg_ = servt;
426 residt_avg_ = residt;
427 } else {
428 servt_avg_ = detail::ln_polyak_vec(servt_avg_, servt, k);
429 residt_avg_ = detail::ln_polyak_vec(residt_avg_, residt, k);
430 }
431
432 err_[std::size_t(it)] = err;
433 if (k <= cfg_.conseq) return false;
434 for (long w = 0; w < cfg_.conseq; ++w)
435 if (!(err_[std::size_t(it - w)] < cfg_.iter_tol)) return false;
436 return true;
437 }
438
439 double relax_omega() const { return omega_; }
440 /** Per-iteration drift, 1-based; slot 0 is unused. */
441 const std::vector<double>& iteration_error() const { return err_; }
442 long averaging_count() const { return k_; }
443 /** Iteration at which averaging started, -1 while still in burn-in. */
444 long averaging_start() const { return start_; }
445 const std::vector<LayerResult<T>>& averaged_results() const { return avg_; }
446 const std::vector<T>& averaged_servt() const { return servt_avg_; }
447 const std::vector<T>& averaged_residt() const { return residt_avg_; }
448
449private:
450 LnStochConfig cfg_;
451 double omega_;
452 std::vector<double> err_;
453 long k_ = 0;
454 long start_ = -1;
455 std::vector<LayerResult<T>> avg_;
456 std::vector<T> servt_avg_, residt_avg_;
457};
458
459// ---------------------------------------------------------------------------
460// the remaining @@SolverLN entry points, as free functions over the solver
461// ---------------------------------------------------------------------------
462
463/**
464 * @@SolverLN/getSensitivityTable.m: solve the ensemble, then concatenate each
465 * LAYER solver's own sensitivity table under a leading Layer column.
466 *
467 * The work is `SolverLN::get_sensitivity_table`, which has to be a member -- it
468 * perturbs each layer in place and re-enters `solve_layer` for that layer, so it
469 * needs the fork views, the region routing and the cache refresh that only the
470 * solver holds. This wrapper exists so that the operation is reachable under
471 * the name the reference gives it.
472 */
473template <class T>
477
478/**
479 * @@SolverLN/getTranAvg.m: the block-diagonal aggregate transient over the LQN
480 * layers, in whichever coupling `LnOptions::ln_transient` names.
481 */
482template <class T>
484 return solver.get_tran_avg();
485}
486
487/**
488 * @@SolverLN/getCdfRespT.m: the per-entry response-time distribution, which
489 * only the `moment3` method produces.
490 */
491template <class T>
492std::vector<LnCdf> lqn_cdf_respt(SolverLN<T>& solver) {
493 return solver.get_cdf_respt();
494}
495
496} // namespace ln
497} // namespace line
498
499#endif // LINE_SOLVERS_LN_LQN_ANALYZERS_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
LnStochController(const LnStochConfig &cfg)
const std::vector< double > & iteration_error() const
Per-iteration drift, 1-based; slot 0 is unused.
const std::vector< T > & averaged_residt() const
long averaging_start() const
Iteration at which averaging started, -1 while still in burn-in.
bool update(int it, const std::vector< LayerResult< T > > &latest, const std::vector< double > &layer_jobs, const std::vector< T > &servt, const std::vector< T > &residt)
Fold iteration it in and say whether the iteration may stop.
const std::vector< LayerResult< T > > & averaged_results() const
const std::vector< T > & averaged_servt() const
LnSensTable< T > get_sensitivity_table(const sens::SensOptions &sopt)
Port of @SolverLN/getSensitivityTable: solve the ensemble, then concatenate each layer solver's own t...
Definition solver_ln.h:582
LnTranSolution get_tran_avg()
Port of @SolverLN/getTranAvg: the block-diagonal aggregate transient.
Definition solver_ln.h:563
std::vector< LnCdf > get_cdf_respt()
Port of @SolverLN/getCdfRespT: the per-entry response-time distribution.
Definition solver_ln.h:524
The exception types the port throws.
Standalone LQN routines that SolverLN needs but does not contain.
LayeredNetworkStruct, the flattened description of a layered queueing network.
Dense matrix and non-owning view.
CallType
Call kinds, with the values of MATLAB CallType.
Definition lang_types.h:467
T lqn_overtake_prob(const T &S1, const T &S2, const T &lambda, double mult)
Probability that an arrival at an entry finds the server in phase 2.
LnTranSolution lqn_tran_avg(SolverLN< T > &solver)
@SolverLN/getTranAvg.m: the block-diagonal aggregate transient over the LQN layers,...
T lqn_overtake_prob_markov(const LqnStruct< T > &lqn, const std::vector< T > &servt, const std::vector< T > &callresidt, const std::vector< T > &tput, std::size_t eidx, const T &xj)
Overtaking probability at a server entry, through the LQNS phased-server chain rather than the reduce...
T lqn_overtake_markov(const Matrix< T > &clientPhases, const T &prVisit, const T &xj, const std::vector< T > &y_aj)
Overtaking probability from the LQNS phased-server Markov chain.
OvertakeCtmcState< T > lqn_overtake_ctmc(const T &S1, const T &S2, const T &lambda)
Stationary law of the reduced overtaking chain of overtake_prob.m.
LnSensTable< T > lqn_sensitivity_table(SolverLN< T > &solver, const sens::SensOptions &opt)
@SolverLN/getSensitivityTable.m: solve the ensemble, then concatenate each LAYER solver's own sensiti...
std::vector< LnCdf > lqn_cdf_respt(SolverLN< T > &solver)
@SolverLN/getCdfRespT.m: the per-entry response-time distribution, which only the moment3 method prod...
Number-type abstraction for the templated API port.
SolverLN: layered decomposition of a layered queueing network.
The MATLAB GlobalConstants, as reported by lineStart at its defaults.
Definition lang_types.h:667
static constexpr double FineTol
Definition lang_types.h:668
Per-layer results of one iteration, the [QN,UN,RN,TN,AN,WN] of getAvg.
Definition solver_ln.h:345
getSensitivityTable of the ensemble: the layer tables under a Layer column.
Definition solver_ln.h:414
options.config.stochiter_* of SolverOptions.m, with its defaults.
Definition solver_ln.h:444
The layered transient: one block per layer, plus how it was produced.
Definition solver_ln.h:405
Stationary law of the three-state overtaking chain, in its own order.
The name-value contract of getSensitivityTable.