LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fluid_runner.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_RUNNER_H
6#define LINE_SOLVERS_FLUID_FLUID_RUNNER_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * The fluid solver's outermost entry point: `@@SolverFLD/runAnalyzer.m`'s method
12 * resolution over `solver_fluid_analyzer.m`'s dispatch.
13 *
14 * WHY THIS IS A SEPARATE HEADER AND NOT ANOTHER BRANCH IN `fluid_dispatch`.
15 * Three of the reference's branches are ports that sit ABOVE `solver_fluid.h`
16 * and include it -- `solver_fluid_closing.m` (fluid_closing.h),
17 * `solver_fld_cacheqn_analyzer.m` (fluid_cacheqn.h) and
18 * `@@SolverFLD/exportODEs.m` (fluid_export_odes.h). A dispatcher inside
19 * `solver_fluid.h` therefore cannot call them without a cyclic include, and
20 * lowering them into it would put three unrelated solvers in one file. The
21 * routing goes here instead, in the one place that may include all four, which
22 * is also the pattern the CTMC, MAM, MVA, NC and SSA runners follow.
23 *
24 * WHAT `fluid_dispatch` STILL REFUSES, and why that is not a duplicate gate.
25 * It cannot see the cache analyzer, so a Cache model reaching it directly is
26 * refused BY NAME rather than solved by the wrong method; the refusal names
27 * this header as the entry point that does route it. Both are true statements
28 * about the function that carries them.
29 *
30 * THE CORRECTION IS APPLIED ONCE, HERE. `solver_fluid_analyzer.m` runs its
31 * utilization and response-time correction after the method switch and on every
32 * branch alike, and the per-method functions it calls return the uncorrected
33 * table for exactly that reason. `solver_fluid` is itself that switch plus the
34 * correction, so the branches routed around it are corrected here and the ones
35 * routed through it are not corrected twice.
36 */
37
38#include <cstddef>
39#include <algorithm>
40#include <vector>
41#include <string>
42
60#include "line/util/error.h"
61
62namespace line {
63namespace fluid {
64
65/**
66 * Port of `SolverFLD.listValidMethods`.
67 *
68 * Every method the dispatch below accepts, INCLUDING the `fld.`-qualified
69 * spelling of each: the reference's own dispatch took `fld.tbi`, `fld.dae`
70 * and six more while its list named none of them, and native python listed the
71 * whole set, so the three lists disagreed. `aoi` (the MFQ age-of-information
72 * reading) is an alias of `mfq`.
73 */
74inline std::vector<std::string> fluid_list_valid_methods() {
75 return {"default",
76 "matrix", "fld.matrix", "pnorm", "fld.pnorm",
77 "softmin", "fld.softmin",
78 "statedep", "fld.statedep",
79 "closing", "fld.closing",
80 "minnormal", "fld.minnormal",
81 "refined", "fld.refined",
82 "tbi", "fld.tbi",
83 "diffusion", "fld.diffusion",
84 "mfq", "fld.mfq",
85 "rmf", "fld.rmf",
86 "aoi", "fld.aoi",
87 "kp", "fld.kp",
88 "dae", "fld.dae",
89 // The single-station fluid limits (Source -> Queue -> Sink, one
90 // class), which `fluid_qsys_handles` routes to `solver_fluid_qsys`.
91 "ggisgi", "fld.ggisgi",
92 "ggingi", "fld.ggingi",
93 "tvms", "fld.tvms",
94 "mtginf", "fld.mtginf",
95 "mol", "fld.mol"};
96}
97
98/** Port of `runAnalyzerChecks`' method gate: an unlisted method is refused. */
99inline void fluid_check_method(const std::string& method) {
100 const std::vector<std::string> valid = fluid_list_valid_methods();
101 if (std::find(valid.begin(), valid.end(), method) != valid.end()) return;
102 throw UnsupportedError("SolverFLD: the '" + method + "' method is unsupported by this solver");
103}
104
105
106namespace detail {
107
108/** `sn.nodetype == NodeType.Cache`, which is what selects `rmf`. */
109template <class T>
110bool fluid_has_cache(const qn::NetworkStruct<T>& sn) {
111 for (const qn::NodeDef& nd : sn.nodes)
112 if (nd.nodetype == qn::NodeType::Cache) return true;
113 return false;
114}
115
116/** A DPS station, which the matrix method cannot express. */
117template <class T>
118bool fluid_has_dps(const qn::NetworkStruct<T>& sn) {
119 for (const auto& st : sn.stations)
120 if (st.sched == lang::SchedStrategy::DPS) return true;
121 return false;
122}
123
124/**
125 * The method name without its `fld.` prefix, which names the same method.
126 *
127 * `aoi` is folded onto `mfq` here for the same reason: it names the MFQ
128 * branch's age-of-information reading rather than a method of its own, which
129 * is how native python spells it and how
130 * `@@SolverFLD/getAvgAoI` reaches it (it requires method='mfq').
131 */
132inline std::string fluid_unqualify(const std::string& method) {
133 std::string m = method;
134 if (m.size() > 4 && m.compare(0, 4, "fld.") == 0) m = m.substr(4);
135 if (m == "aoi") return "mfq";
136 return m;
137}
138
139/**
140 * Port of `fluid_minnormal_applicable.m`: whether the moment closure can answer
141 * this model, which is what `default` consults before preferring it.
142 *
143 * THE TEST IS STATIC. It inspects the model and the options, never the solution,
144 * so a feature the model DECLARES is decided here and only here. The one
145 * condition that cannot be static is a NON-HYPERBOLIC fixed point (balanced
146 * bottlenecks, a saturated multiclass station, an overloaded open station): it
147 * exists only once the mean is solved, `fluid_lyapunov` raises
148 * FluidNonHyperbolicError on it, and the runner switches `minnormal` -- resolved
149 * or requested -- to a first-order method on that exception alone, since the
150 * closure has no stationary covariance either way and the mean is still
151 * available.
152 *
153 * Every condition below mirrors a refusal that `fluid_moment_terms` or
154 * `solver_fluid_moments` would otherwise raise, so this function and those
155 * refusals must move together.
156 */
157template <class T>
158bool fluid_minnormal_applicable(const qn::NetworkStruct<T>& sn, const FluidOptions& opt,
159 std::string& reason) {
160 const std::size_t M = sn.nstations, K = sn.nclasses;
161 const FluidLayout L = fluid_layout(sn);
162
163 // Open and mixed models are supported: the covariance projects the EXT source
164 // pool out. What it cannot take is a NON-POISSON arrival stream, whose source
165 // coordinates track the phase of a single arrival process, not a population.
166 for (std::size_t i = 0; i < M; ++i) {
167 if (sn.stations[i].sched != lang::SchedStrategy::EXT) continue;
168 for (std::size_t r = 0; r < K; ++r)
169 if (L.kic[i][r] > 1) {
170 reason = "class " + std::to_string(r + 1) + " has a " +
171 std::to_string(L.kic[i][r]) + "-phase (non-Poisson) arrival process";
172 return false;
173 }
174 }
175 // A cache model is answered through the decomposition analyzer, with the
176 // closure in its network step, so cache nodes no longer decline the method.
177 // What still declines is a replacement strategy with no drift-based fluid
178 // model, mirroring the runtime gate in `fluid_cacheqn_gate`: LRU, HLRU,
179 // CLIMB and QLRU are answered by a characteristic-time fixed point, which is
180 // not a fluid method and has no covariance.
181 for (std::size_t nd = 0; nd < sn.nodes.size(); ++nd) {
182 if (sn.nodes[nd].nodetype != qn::NodeType::Cache) continue;
183 const std::size_t key = nd + 1; // nodeparam is keyed 1-based
184 if (sn.nodeparam.count(key) == 0) {
185 reason = "a cache uses a replacement strategy with no drift-based fluid model";
186 return false;
187 }
188 const lang::ReplacementStrategy rs = sn.nodeparam.at(key).replacestrat;
191 reason = "a cache uses a replacement strategy with no drift-based fluid model";
192 return false;
193 }
194 }
195 // The disciplines with a branch in `ode_rates_closing_factors`; anything else
196 // falls through to g = x, i.e. an infinite server.
197 for (std::size_t i = 0; i < M; ++i) {
198 const lang::SchedStrategy s = sn.stations[i].sched;
199 const bool ok = s == lang::SchedStrategy::INF || s == lang::SchedStrategy::EXT ||
202 if (!ok) {
203 reason = "station " + std::to_string(i + 1) + " uses " + lang::sched_to_text(s) +
204 ", which has no fluid drift branch";
205 return false;
206 }
207 }
208 // The Lyapunov solve is cubic in the phase-resolved state, so the same cap
209 // `solver_fluid_moments` enforces decides selection rather than being hit
210 // later.
211 if (L.nstates > opt.moment_maxstate) {
212 reason = "the phase-resolved state has " + std::to_string(L.nstates) +
213 " coordinates, above the moment_maxstate limit of " +
214 std::to_string(opt.moment_maxstate);
215 return false;
216 }
217 // The moment methods need an autonomous drift.
218 // `fluid_minnormal_applicable.m:99`: a time-varying rate multiplier makes the
219 // drift non-autonomous, so there is no stationary covariance to solve for and
220 // `default` must resolve to a first-order method instead.
221 if (fluid_has_time_varying_rates(opt)) {
222 reason = "a rate schedule (nhpp_sched / rate_traj / rate_sched) makes the drift "
223 "time-varying";
224 return false;
225 }
226 reason.clear();
227 return true;
228}
229
230/**
231 * Port of `fluid_dae_applicable.m`: whether the differential-algebraic route can
232 * answer this model, which is what the fallback ladder consults before trying
233 * `dae` on a declined `minnormal`.
234 *
235 * WHY THIS EXISTS SEPARATELY FROM `fluid_minnormal_applicable`. The two methods
236 * state the SAME closure and differ only in how the coupled equations are
237 * discharged, so a model `minnormal` accepts is almost always one `dae` accepts
238 * too. Almost: `dae` carries a finite-difference Jacobian over the whole unknown
239 * vector rather than one Lyapunov solve, so its state cap is lower; it closes on
240 * the per-station variance only, so DPS and GPS are out; and it has no
241 * decomposition route, so a cache model is out. Those three are exactly the
242 * difference set, and naming them here keeps the ladder from entering a rung
243 * that would refuse the model a moment later.
244 *
245 * The test is STATIC, for the same reason the min-normal one is. The one
246 * condition that cannot be static is the NON-HYPERBOLIC fixed point the ladder
247 * exists to route around -- it exists only once the mean is solved -- and `dae`
248 * fails on it loudly, which is what moves the ladder to its last rung.
249 *
250 * Every condition below mirrors a refusal `solver_fluid_dae` would otherwise
251 * raise, so this function and those refusals must move together.
252 */
253template <class T>
254bool fluid_dae_applicable(const qn::NetworkStruct<T>& sn, const FluidOptions& opt,
255 std::string& reason,
256 const FluidDaeOptions& dopt_in = FluidDaeOptions()) {
257 const FluidDaeOptions dopt = fluid_dae_options(opt, dopt_in);
258 const std::size_t M = sn.nstations;
259 const FluidLayout L = fluid_layout(sn);
260
261 // A cache model is a DECOMPOSITION, and `dae` has no arm for it: `minnormal`
262 // on a cache model routes through `solver_fld_cacheqn_analyzer` with the
263 // closure in its network step, while this route would read the cache nodes as
264 // ordinary stations -- a decomposition has no single drift for the algebraic
265 // constraint to attach to.
266 if (fluid_has_cache(sn)) {
267 reason = "a cache model is answered by the decomposition analyzer, which has no dae route";
268 return false;
269 }
270 // The DPS and GPS shares close on the covariance BETWEEN station coordinates,
271 // not on the station total, so their closure state is a matrix block rather
272 // than the scalar the Newton vector carries. Mirrors the `solver_fluid_dae`
273 // refusal.
274 for (std::size_t i = 0; i < M; ++i) {
275 const lang::SchedStrategy s = sn.stations[i].sched;
277 reason = "station " + std::to_string(i + 1) + " uses " + lang::sched_to_text(s) +
278 ", whose share closes on the covariance between its class coordinates "
279 "rather than on the station variance";
280 return false;
281 }
282 }
283 // The simultaneous solve is quartic overall, against the cubic of one Lyapunov
284 // solve, so its crossover is lower than the 200 `minnormal` permits and it
285 // carries its own cap. Same count the min-normal test forms, so the two limits
286 // are read on the same scale.
287 if (L.nstates > dopt.maxstate) {
288 reason = "the phase-resolved state has " + std::to_string(L.nstates) +
289 " coordinates, above the dae_maxstate limit of " +
290 std::to_string(dopt.maxstate);
291 return false;
292 }
293 reason.clear();
294 return true;
295}
296
297/**
298 * Port of `fluid_resolve_default_method.m`: what `default` stands for on THIS
299 * model.
300 *
301 * Preference order: `rmf` for a cache model, then `minnormal` wherever
302 * `fluid_minnormal_applicable` accepts the model, then the historical choice of
303 * `closing` for DPS and `matrix` otherwise. The second-order closure dominates
304 * the first-order methods on every family measured against the exact CTMC and is
305 * the only method that can represent GPS at all, so it is preferred wherever it
306 * applies.
307 *
308 * THIS RUNS BEFORE THE FEATURE GATE, NOT INSIDE THE DISPATCH SWITCH. The gate
309 * validates `fluid_feature_set(method)`, and `default` is not `minnormal`, so
310 * resolving later would let the gate reject a GPS model that the resolved method
311 * supports. Keeping the decision in one function is what stops the gate and the
312 * dispatch from disagreeing.
313 */
314template <class T>
315std::string fluid_resolve_method(const qn::NetworkStruct<T>& sn, const std::string& method,
316 const FluidOptions& opt, std::string& reason) {
317 const std::string m = fluid_unqualify(method);
318 reason.clear();
319 // `rmf` and `refined` are public aliases. Cache models select the
320 // cache/network RMF decomposition; all other models select the generic
321 // refined mean-field correction about the min-normal fixed point.
322 if (m == "rmf" || m == "refined") {
323 if (fluid_has_cache(sn)) {
324 reason = "the model has cache nodes";
325 return "rmf";
326 }
327 reason = "the model has no cache nodes";
328 return "refined";
329 }
330 if (m != "default") return m;
331 if (fluid_has_cache(sn)) {
332 reason = "the model has cache nodes";
333 return "rmf";
334 }
335 // A BINDING BUFFER OR A CAPACITY REGION ALSO HAS ONE FLUID ROUTE, for the
336 // same reason a Petri net does: nothing else in the fluid tree reads the cap,
337 // the per-class cap or the region limit, so every other method integrates the
338 // capped station as an unbounded one -- which is why
339 // `fluid_check_finite_capacity` refuses them. Resolving `default` to one of
340 // those turned a model this solver CAN answer into an error whose advice was
341 // to type the very method the resolution should have picked. The capacity
342 // test is the gate's own (`binding_capacity_reason`), so the two cannot
343 // disagree, and where the DAE route declines the fall-through leaves the
344 // refusal to the gate, which names the blocking feature.
345 if (!sn.regions.empty() || qn::has_binding_capacity(sn)) {
346 std::string dae_why;
347 if (fluid_dae_applicable(sn, opt, dae_why)) {
348 reason = sn.regions.empty() ? "the model has a binding finite buffer"
349 : "the model has a finite capacity region";
350 return "dae";
351 }
352 }
353 if (fluid_minnormal_applicable(sn, opt, reason)) return "minnormal";
354 return fluid_has_dps(sn) ? "closing" : "matrix";
355}
356
357/** The same, for a caller with no interest in why `minnormal` was declined. */
358template <class T>
359std::string fluid_resolve_method(const qn::NetworkStruct<T>& sn, const std::string& method,
360 const FluidOptions& opt = FluidOptions()) {
361 std::string reason;
362 return fluid_resolve_method(sn, method, opt, reason);
363}
364
365/**
366 * `@@SolverFLD/runAnalyzer.m:169-175`: limited load dependence is honoured only by
367 * the closing family, whose drift multiplies the scheduling share by alpha(n_i).
368 *
369 * The C++ featset DOES emit LoadDependence from the model (unlike MATLAB's
370 * getUsedLangFeatures, which is why the reference has to refuse here explicitly),
371 * so this is the same refusal reached one step earlier -- kept as its own check
372 * because `solver_fluid` is callable without the gate.
373 */
374template <class T>
375void fluid_check_load_dependence(const qn::NetworkStruct<T>& sn, const std::string& m) {
376 if (m == "closing" || m == "minnormal" || m == "refined" || m == "dae") return;
377 for (std::size_t i = 0; i < sn.stations.size(); ++i)
378 for (std::size_t k = 0; k < sn.stations[i].lldscaling.size(); ++k)
379 if (std::fabs(num_traits<T>::to_double(sn.stations[i].lldscaling[k]) - 1.0) >
381 throw UnsupportedError(
382 "solver_fluid_run_analyzer: this model uses load dependence (setLoadDependence), which "
383 "the '" +
384 m +
385 "' method does not evaluate. Use method 'closing' for the mean-field answer, or "
386 "'minnormal'/'refined' for the moment-closure correction");
387}
388
389/**
390 * Can `m` run the fluid fork-join fixed point on this model?
391 *
392 * A fork-join model is not integrated as one drift: `fj_fork_join_transform`
393 * replaces the fork by auxiliary classes and the answer is the fixed point of
394 * solving that transformed model repeatedly. On a CLOSED model the transform
395 * stays closed and every fluid method takes it. On an OPEN one the auxiliary
396 * classes arrive at a Source, and the DAE form has no unknowns for them: the
397 * inner solve fails on the class count rather than returning a drift, so the
398 * method is refused by name instead.
399 *
400 * `refined` is NOT listed here even though it fails the same way, because it is
401 * already refused on every open model, fork-join or not, by its own closed-model
402 * restriction (`qn::fluid_feature_set("refined")`).
403 *
404 * ASKED AS A PREDICATE so that a REPORT can reach the rule too: Fork and
405 * OpenClass are both declared names, so a feature set cannot state a rule that
406 * is their CONJUNCTION.
407 *
408 * @param sn the refreshed struct of the model
409 * @param m the resolved method name
410 * @return an empty string when the method may run the fixed point here
411 */
412template <class T>
413std::string fluid_forkjoin_supports(const qn::NetworkStruct<T>& sn, const std::string& m) {
414 if (m != "dae" && m != "fld.dae") return std::string();
415 if (!sn.has_fork()) return std::string();
416 bool any_open = false;
417 for (std::size_t r = 0; r < sn.classes.size(); ++r)
418 if (!std::isfinite(sn.classes[r].population)) any_open = true;
419 if (!any_open) return std::string();
420 return "solver_fluid_run_analyzer: the dae method has no route through the fork-join fixed "
421 "point on an OPEN model: the transform hands the inner solve a mixed network whose "
422 "auxiliary open classes the DAE form carries no unknowns for. Use method 'minnormal', "
423 "which is the same closure and does run that fixed point";
424}
425
426/**
427 * `@@SolverFLD/runAnalyzer.m:159-183`: the two limits of the DAE route that the
428 * feature gate cannot express, refused before the Newton solve so the message
429 * names the model feature rather than surfacing from inside it.
430 *
431 * A CACHE MODEL is a decomposition, not one system: the caches are solved in
432 * isolation and the network with them relabeled as class switches, so there is
433 * no single drift for the constraint to be attached to. `minnormal` and `rmf`
434 * reach `solver_fld_cacheqn_analyzer`, which carries the closure inside its
435 * network step; no such route exists for the DAE form. Cache IS declared in the
436 * fluid featset -- for those two methods -- so the gate passes it and this is
437 * the only place the distinction can be drawn.
438 *
439 * DPS and GPS are NOT repeated here: the per-method featset already unsets both
440 * for `dae` (solver_feature_sets.h), so the gate refuses them one step earlier
441 * and by feature name, which is the finer message of the two.
442 */
443template <class T>
444void fluid_check_dae(const qn::NetworkStruct<T>& sn, const std::string& m) {
445 if (m != "dae") return;
446 if (fluid_has_cache(sn))
447 throw UnsupportedError(
448 "solver_fluid_run_analyzer: the dae method does not support caching stations: a cache model is "
449 "solved by decomposition, so it has no single drift to constrain. Use "
450 "method 'minnormal' for the same closure, or 'rmf'");
451 const std::string fj = fluid_forkjoin_supports(sn, m);
452 if (!fj.empty()) throw UnsupportedError(fj);
453}
454
455/**
456 * The structural finite-buffer gate `@@SolverFLD/runAnalyzer.m` applies, which
457 * this port did not carry: nothing in the fluid tree reads `sn.cap` or
458 * `sn.classcap`, so a capped station was integrated as an unbounded one and the
459 * table reported more jobs in the buffer than the buffer holds.
460 *
461 * `dae` carries the buffer as an algebraic constraint on the drift, and `mol` is
462 * stated for the Mt/G/s/0 LOSS system, where the server count IS the buffer: for
463 * those two a finite capacity is the model rather than something ignored. Every
464 * other method keeps the guard. There is no feature-registry name for plain
465 * capacity, hence the structural test -- `SolverMVA`, `SolverNC` and `SolverAG`
466 * gate the same way, through the same `check_binding_capacity`.
467 *
468 * `m` is the RESOLVED method, as the reference's gate receives it.
469 */
470template <class T>
471void fluid_check_finite_capacity(const qn::NetworkStruct<T>& sn, const std::string& m) {
472 if (m == "dae" || m == "fld.dae" || m == "mol" || m == "fld.mol") return;
473 qn::check_binding_capacity("SolverFLD", sn);
474}
475
476} // namespace detail
477
478/**
479 * Port of `@@SolverFLD/runAnalyzer.m`: resolve the method, route to the function
480 * the reference routes to, correct once.
481 *
482 * This is what a host should call. `solver_fluid` remains the port of
483 * `solver_fluid_analyzer.m` alone and stays reachable for a caller that wants
484 * one named method and no model-dependent resolution.
485 *
486 * `sn_out`, when given, receives `result.solverSpecific.sn`: the struct the
487 * returned table was integrated on, which the FCFS refit may have re-fitted to
488 * a different phase count. Anything that reads `FluidSolution::xvec` afterwards
489 * must be handed THAT struct, because the vector is laid out by its phases.
490 *
491 * `refreshed_out`, when given, receives the cache decomposition's own struct:
492 * the base one with the cache self-switch renormalized at the CONVERGED hit and
493 * miss split. It is DELIBERATELY NOT `sn_out`: that one is the inner, relabeled
494 * struct the ODE ran on and it lays out `xvec`, whereas this one is the caller's
495 * own topology carrying a solved quantity. Left untouched on every model with no
496 * cache, and on the cache branches that report no split.
497 *
498 * `cache_out`, when given, receives the SAME split as a `CacheMetrics`, which is
499 * the form every other solver states it in (`mva::AvgResult::cache`). The
500 * refreshed struct alone is not enough for a caller: a host that solves through
501 * the CLI reads the hit and miss fractions back onto its own Cache node from
502 * that block, and MATLAB's `CPPLINE.restoreCacheResults` CLEARS the node when it
503 * is absent -- which is how `-s fluid` came to report link()'s offered 1/2-1/2
504 * on cache_replc_rr for a split it had converged to 0.5623/0.4377.
505 */
506template <class T>
507FluidSolution solver_fluid_run_analyzer(const qn::NetworkStruct<T>& sn, const FluidOptions& opt,
508 qn::NetworkStruct<T>* sn_out = nullptr,
509 qn::NetworkStruct<T>* refreshed_out = nullptr,
510 solvers::CacheMetrics<T>* cache_out = nullptr);
511
512namespace detail {
513
514/**
515 * `@@SolverFLD/fldDispatch.m`: one inner solve of the fixed point, as an
516 * `mva::MvaSolution`.
517 *
518 * The driver is `mva::fj_fixed_point`, the one MVA and NC drive; it speaks
519 * MvaSolution, so the fluid table is carried into that shape here rather than
520 * a second fork-join loop being written for the fluid solver. Nothing else is
521 * fluid-specific: the transform emits Source, Delay, Queue, Router and
522 * ClassSwitch, all of which the drift already carries.
523 */
524template <class T>
525mva::MvaSolution<T> fluid_as_mva_solution(const FluidSolution& f) {
526 mva::MvaSolution<T> s;
527 const std::size_t M = f.QN.rows(), K = f.QN.cols();
528 const T zero = num_traits<T>::from_int(0);
529 s.Q = Matrix<T>(M, K, zero);
530 s.U = Matrix<T>(M, K, zero);
531 s.R = Matrix<T>(M, K, zero);
532 s.Tp = Matrix<T>(M, K, zero);
533 for (std::size_t i = 0; i < M; ++i)
534 for (std::size_t k = 0; k < K; ++k) {
535 s.Q(i, k) = num_traits<T>::from_double(f.QN(i, k));
536 s.U(i, k) = num_traits<T>::from_double(f.UN(i, k));
537 s.R(i, k) = num_traits<T>::from_double(f.RN(i, k));
538 s.Tp(i, k) = num_traits<T>::from_double(f.TN(i, k));
539 }
540 s.C.reserve(f.CN.size());
541 for (double v : f.CN) s.C.push_back(num_traits<T>::from_double(v));
542 s.X.reserve(f.XN.size());
543 for (double v : f.XN) s.X.push_back(num_traits<T>::from_double(v));
544 s.method = f.method;
545 s.iter = static_cast<int>(f.iters);
546 // The fluid solver produces no normalizing constant; the reference leaves it
547 // at zero rather than fabricating one.
548 s.lG = 0.0;
549 return s;
550}
551
552/** The merged fork-join table, back in the fluid solver's own result shape. */
553template <class T>
554FluidSolution fluid_from_mva_solution(const mva::MvaSolution<T>& s, const std::string& method) {
555 FluidSolution f;
556 const std::size_t M = s.Q.rows(), K = s.Q.cols();
557 f.QN = Matrix<double>(M, K, 0.0);
558 f.UN = Matrix<double>(M, K, 0.0);
559 f.RN = Matrix<double>(M, K, 0.0);
560 f.TN = Matrix<double>(M, K, 0.0);
561 for (std::size_t i = 0; i < M; ++i)
562 for (std::size_t k = 0; k < K; ++k) {
563 f.QN(i, k) = num_traits<T>::to_double(s.Q(i, k));
564 f.UN(i, k) = num_traits<T>::to_double(s.U(i, k));
565 f.RN(i, k) = num_traits<T>::to_double(s.R(i, k));
566 f.TN(i, k) = num_traits<T>::to_double(s.Tp(i, k));
567 }
568 f.CN.reserve(s.C.size());
569 for (const T& v : s.C) f.CN.push_back(num_traits<T>::to_double(v));
570 f.XN.reserve(s.X.size());
571 for (const T& v : s.X) f.XN.push_back(num_traits<T>::to_double(v));
572 f.iters = static_cast<std::size_t>(s.iter < 0 ? 0 : s.iter);
573 f.method = method;
574 return f;
575}
576
577/** The leading `M` station rows of a metric matrix. */
578inline Matrix<double> fluid_leading_rows(const Matrix<double>& m, std::size_t M) {
579 if (m.rows() <= M) return m;
580 Matrix<double> out(M, m.cols(), 0.0);
581 for (std::size_t i = 0; i < M; ++i)
582 for (std::size_t k = 0; k < m.cols(); ++k) out(i, k) = m(i, k);
583 return out;
584}
585
586/**
587 * The fork-join arm of `solver_fluid_run_analyzer`.
588 *
589 * Only the steady-state means are produced. The transient tables are indexed by
590 * the ORIGINAL stations and classes, whereas each pass integrates a DIFFERENT
591 * transformed network, so a trajectory read off the last pass would not be the
592 * trajectory of the model the caller built; `xvec` is left empty for the same
593 * reason, which is what makes the passage-time and transient entry points
594 * decline a fork-join model rather than answer it from the wrong state.
595 */
596template <class T>
597FluidSolution fluid_fork_join_run(const qn::NetworkStruct<T>& sn, const FluidOptions& opt) {
598 mva::FjMmt<T> tr = mva::fj_fork_join_transform(sn, opt.fork_join);
599 std::vector<T> lam(tr.V.classes.size() + 1,
600 num_traits<T>::from_double(lang::GlobalConstants::FineTol));
601 mva::MvaOptions mopt;
602 mopt.method = opt.method;
603 mopt.tol = opt.tol;
604 mopt.iter_tol = opt.iter_tol;
605 mopt.iter_max = opt.iter_max;
606 mopt.fork_join = opt.fork_join;
607 mopt.base_has_fork = true;
608 FluidOptions inner = opt;
609 const mva::MvaSolution<T> merged =
610 mva::fj_fixed_point(sn, tr, lam, mopt, [&inner](qn::NetworkStruct<T>& V) {
611 return fluid_as_mva_solution<T>(solver_fluid_run_analyzer(V, inner));
612 });
613 FluidSolution out = fluid_from_mva_solution<T>(merged, opt.method);
614 // The driver drops the auxiliary CLASS columns but not the transform's own
615 // STATION row: it appends a Source to carry the auxiliary arrivals, and the
616 // base stations are the prefix of that list. `solver_mva_run_analyzer` trims it in
617 // `filter_metric` against the base struct; this entry point returns the
618 // table directly, so it trims here.
619 out.QN = detail::fluid_leading_rows(out.QN, sn.nstations);
620 out.UN = detail::fluid_leading_rows(out.UN, sn.nstations);
621 out.RN = detail::fluid_leading_rows(out.RN, sn.nstations);
622 out.TN = detail::fluid_leading_rows(out.TN, sn.nstations);
623 return out;
624}
625
626} // namespace detail
627
628namespace detail {
629
630/** Whether the model holds a Transition node, i.e. is a stochastic Petri net. */
631template <class T>
632inline bool fluid_is_petri_net(const qn::NetworkStruct<T>& sn) {
633 for (std::size_t i = 0; i < sn.nodes.size(); ++i)
634 if (sn.nodes[i].nodetype == lang::NodeType::Transition) return true;
635 return false;
636}
637
638/**
639 * The 'dae' method's Petri arm.
640 *
641 * 'dae' is the ONLY fluid method that can carry a net: the P-invariants, the
642 * firing flow of an immediate transition and a bounded place are all EQUATIONS,
643 * and the other methods have nowhere to put them. A method named explicitly is
644 * therefore checked rather than silently redirected, so a caller who asked for
645 * 'closing' on a net is told why it cannot answer.
646 */
647template <class T>
648inline FluidSolution fluid_petri_run(const qn::NetworkStruct<T>& sn, const FluidOptions& opt) {
649 const std::string m = (opt.method.rfind("fld.", 0) == 0) ? opt.method.substr(4) : opt.method;
650 if (!(m == "default" || m == "dae"))
651 throw UnsupportedError(
652 "SolverFluid: method '" + opt.method +
653 "' cannot solve a Petri net: its conserved quantities are P-invariants rather than "
654 "chain populations, an immediate transition is an algebraic FLOW rather than an "
655 "event with a rate, and a bounded place is a linear inequality on the marking. Only "
656 "'dae' states those as equations; every other fluid method builds its drift from the "
657 "station/class/phase encoding, where a Place contributes no coordinate at all, and "
658 "would integrate the net as an empty model and report zeros without a warning");
659 const petri::PetriSolution ps = petri::solver_fluid_petri(sn, petri::PetriOptions());
660 FluidSolution out;
661 out.QN = ps.QN;
662 out.UN = ps.UN;
663 out.RN = ps.RN;
664 out.TN = ps.TN;
665 out.xvec = ps.x;
666 out.iters = ps.iters;
667 out.method = "dae";
668 const std::size_t M = ps.QN.rows(), K = ps.QN.cols();
669 out.CN.assign(K, 0.0);
670 out.XN.assign(K, 0.0);
671 for (std::size_t k = 0; k < K; ++k) {
672 double q = 0.0, x = 0.0;
673 for (std::size_t i = 0; i < M; ++i) {
674 q += ps.QN(i, k);
675 x = std::max(x, ps.TN(i, k));
676 }
677 out.XN[k] = x;
678 out.CN[k] = (x > 1e-14) ? q / x : 0.0;
679 }
680 // The marking covariance of the linear noise approximation: the second
681 // moment no other solver of a Petri net in LINE returns.
682 out.has_moments = true;
683 out.moments.Sigma = ps.Sigma;
684 out.moments.QVar = ps.QVar;
685 out.moments.QStd = ps.QStd;
686 return out;
687}
688
689} // namespace detail
690
691template <class T>
693 qn::NetworkStruct<T>* sn_out,
694 qn::NetworkStruct<T>* refreshed_out,
695 solvers::CacheMetrics<T>* cache_out) {
696 std::string why;
697 if (sn_out) *sn_out = sn;
698 // A MODEL HOLDING ANY Transition NODE IS A DIFFERENT FORMALISM and goes to a
699 // dedicated runner rather than through the queueing dispatch. The queueing
700 // path rewrites UN from sn.rates (NaN at a Place) and RN from the station
701 // scheduling, and would overwrite the Petri conventions the Petri route
702 // reports -- a place's utilization IS its token count, and its throughput is
703 // the token departure rate, not a service completion rate. The branch is
704 // taken before the feature gate, which is written about queueing stations.
705 if (detail::fluid_is_petri_net(sn)) return detail::fluid_petri_run(sn, opt);
706 // runAnalyzerChecks' METHOD gate, before the feature gate and before the
707 // resolution, exactly as the reference orders them: an unknown name is
708 // reported as unknown rather than as whatever the feature gate finds.
709 fluid_check_method(opt.method);
710 const std::string m = detail::fluid_resolve_method(sn, opt.method, opt, why);
711 // runAnalyzerChecks' universal feature gate, on the RESOLVED method. Only
712 // the runner is gated: `solver_fluid` stays the ungated port of
713 // solver_fluid_analyzer.m for a caller that wants one named method.
714 qn::feature_gate("SolverFluid", qn::fluid_feature_set(m), sn);
715 detail::fluid_check_load_dependence(sn, m);
716 detail::fluid_check_dae(sn, m);
717 detail::fluid_check_finite_capacity(sn, m);
718 // Fork-join: the same fixed point MVA and NC drive, with a fluid inner
719 // solve. Taken after the gate so an unsupported feature is still named by
720 // its own message rather than by a failure inside the transformed model.
721 if (sn.has_fork()) {
722 FluidOptions fo = opt;
723 fo.method = m;
724 return detail::fluid_fork_join_run(sn, fo);
725 }
726 FluidOptions o = opt;
727 o.method = m;
728
729 // The reference's FCFS non-exponential refit loop, on the branches that are
730 // routed AROUND `solver_fluid` (which runs it itself). `solve` is the same
731 // per-method function the branch used for its first integration, so the
732 // sweeps stay on one drift; the loop is a no-op for a method the reference
733 // does not refit and for a model with no FCFS station.
734 const auto closing_solve = [](const qn::NetworkStruct<T>& s, const FluidOptions& op) {
735 return solver_fluid_closing(s, op);
736 };
737 const auto moments_solve = [](const qn::NetworkStruct<T>& s, const FluidOptions& op) {
738 return solver_fluid_moments(s, op);
739 };
740 const auto dae_solve = [](const qn::NetworkStruct<T>& s, const FluidOptions& op) {
741 return solver_fluid_dae(s, op);
742 };
743
744 FluidSolution out;
745 if (m == "rmf" || (m == "minnormal" && detail::fluid_has_cache(sn))) {
746 // A cache model is a DECOMPOSITION, not one ODE: the caches are solved
747 // in isolation and the network with them relabeled as class switches.
748 // "minnormal" takes the same route, with the closure inside `netfun`.
749 // The analyzer's refreshed struct is CARRIED, not dropped: the split it
750 // converged on is what every visit-weighted quantity downstream needs.
752 out = cq.sol;
753 if (refreshed_out) *refreshed_out = cq.refreshed;
754 if (cache_out) *cache_out = solvers::cache_metrics_of_matrix(sn, cq.hitprob, cq.missprob);
755 } else if (m == "closing" || m == "statedep" || m == "softmin" || m == "tbi") {
756 out = detail::fluid_fcfs_nonexp_refit(sn, o, solver_fluid_closing(sn, o), closing_solve,
757 sn_out);
758 } else if (m == "minnormal" || m == "refined") {
759 // A non-hyperbolic fixed point cannot be seen before the mean is solved,
760 // so it cannot be declined in advance.
761 //
762 // THE LADDER HAS TWO RUNGS, AND THE FIRST ONE KEEPS THE CLOSURE. Most of
763 // these failures are not a property of the model at all:
764 // `solver_fluid_moments` must start its alternation at sigma2 = 0, where
765 // min(n,c) has no derivative, so a saturated or balanced model's
766 // first-order fixed point lands on the kink, sits on a continuum of
767 // equilibria, and the Jacobian there is neutral. `solver_fluid_dae` seeds
768 // the variance POSITIVE and never adopts sigma2 = 0 as an iterate, so the
769 // smoothed E[min(X,c)] breaks the degeneracy and the fixed point is
770 // isolated and hyperbolic -- it answers the same closure, with a
771 // covariance, where the alternation cannot. Dropping straight to first
772 // order is not merely a lost second moment: on a balanced two-station PS
773 // cycle at N=10 it returns [9 1] against the exact [5 5], because a
774 // first-order method has no reason to prefer one point of the continuum
775 // over another.
776 //
777 // The second rung is the first-order method, taken when `dae` declines the
778 // model in advance (`fluid_dae_applicable`) or fails on the same error,
779 // which is the genuinely non-hyperbolic case: an unstable open station has
780 // no stationary distribution to approximate under any closure. Fall back
781 // whether the closure was RESOLVED from `default` or REQUESTED outright:
782 // it has no stationary covariance either way, so refusing an explicit
783 // request would only deny the caller the mean that is still available.
784 try {
785 out = detail::fluid_fcfs_nonexp_refit(sn, o, solver_fluid_moments(sn, o),
786 moments_solve, sn_out);
787 } catch (const FluidNonHyperbolicError&) {
788 std::string dae_reason;
789 bool solved = false;
790 if (detail::fluid_dae_applicable(sn, o, dae_reason)) {
791 FluidOptions daeopt = o;
792 daeopt.method = "dae";
793 try {
794 out = detail::fluid_fcfs_nonexp_refit(sn, daeopt, solver_fluid_dae(sn, daeopt),
795 dae_solve, sn_out);
796 o = daeopt;
797 solved = true;
798 } catch (const FluidNonHyperbolicError&) {
799 // genuinely neutral or unstable: the last rung answers it
800 }
801 }
802 if (!solved) {
803 o.method = detail::fluid_has_dps(sn) ? "closing" : "matrix";
805 detail::fluid_check_load_dependence(sn, o.method);
806 // `solver_fluid` is the analyzer, refit included, so the matrix arm
807 // is complete; the closing arm is routed around it and runs the loop
808 // here.
809 if (o.method != "closing") return solver_fluid(sn, o, sn_out);
810 out = detail::fluid_fcfs_nonexp_refit(sn, o, solver_fluid_closing(sn, o),
811 closing_solve, sn_out);
812 detail::fluid_analyzer_correct(sn, out.QN, out.UN, out.RN, out.TN);
813 detail::fluid_snap_all(out.QN, out.UN, out.RN, out.TN);
814 return out;
815 }
816 }
817 } else if (m == "dae") {
818 // The same closure `minnormal` computes, stated and solved as ONE
819 // differential-algebraic system: population conservation is an equation
820 // rather than a consequence of the drift, and a region cap is an
821 // algebraic inequality beside it. No non-hyperbolic fallback, unlike the
822 // branch above: the constraint rows supply exactly the rank the drift
823 // Jacobian is missing along the conserved directions, so the Lyapunov
824 // step that fails there is not what this route solves through. Refusing
825 // rather than falling back is also the only honest answer once a REGION
826 // is present -- the first-order methods would return the unconstrained
827 // population, which is a different model, not a coarser answer to this
828 // one.
829 out = detail::fluid_fcfs_nonexp_refit(sn, o, solver_fluid_dae(sn, o), dae_solve, sn_out);
830 } else if (m == "kp") {
831 out = solver_fluid_kp(sn, o);
832 } else if (detail::fluid_qsys_handles(m)) {
833 // The single-station fluid limits are closed forms, not integrations of
834 // the network drift: they take the whole model in one call, so they are
835 // returned unmodified -- neither the FCFS refit nor the analyzer
836 // correction below applies to a closed form.
837 return solver_fluid_qsys(sn, o);
838 } else {
839 // `solver_fluid` is the switch, the refit AND the correction, so its
840 // branches are complete already.
841 return solver_fluid(sn, o, sn_out);
842 }
843 detail::fluid_analyzer_correct(sn, out.QN, out.UN, out.RN, out.TN);
844 detail::fluid_snap_all(out.QN, out.UN, out.RN, out.TN);
845 return out;
846}
847
848/**
849 * `-a tran` / `@@SolverFLD/getTranAvg` with the method HONOURED, which is the
850 * one place the reference does not force `closing`.
851 *
852 * `solver_fluid_tran_avg` integrates the first-order closing drift whatever
853 * `opt.method` says, and that IS the reference's rule for every steady-state
854 * device -- matrix, pnorm, softmin and the rest have no trajectory of their own,
855 * so the reference substitutes one and warns. `dae` is the exception
856 * (`getTranAvg.m:155-166`): it integrates the closure itself, with conservation
857 * as an algebraic equation, so substituting the first-order drift for it would
858 * hand back a DIFFERENT method's trajectory under this method's name.
859 *
860 * Every other method keeps the byte-identical path it had, gate included -- this
861 * routes, it does not re-gate.
862 */
863template <class T>
864std::vector<FluidTranPoint> solver_fluid_run_transient(const qn::NetworkStruct<T>& sn,
865 const FluidOptions& opt,
866 std::size_t points = 101) {
867 // The single-station fluid limits produce their OWN trajectory over the
868 // horizon -- that is what they are -- so substituting the first-order
869 // closing drift for them would answer a different model with a
870 // time-averaged rate.
871 if (detail::fluid_qsys_handles(detail::fluid_unqualify(opt.method))) {
872 FluidOptions o = opt;
873 o.method = detail::fluid_unqualify(opt.method);
875 std::vector<FluidTranPoint> traj;
876 solver_fluid_qsys(sn, o, &traj);
877 return traj;
878 }
879 if (detail::fluid_unqualify(opt.method) != "dae")
880 return solver_fluid_tran_avg(sn, opt, points);
881 qn::feature_gate("SolverFluid", qn::fluid_feature_set("dae"), sn);
882 detail::fluid_check_dae(sn, std::string("dae"));
884}
885
886/**
887 * Port of `@@SolverFLD/getCdfRespT`: the response-time law of every (station,
888 * class) pair, read off the marked-fluid trajectory started from the steady
889 * state.
890 *
891 * THE SOLVE COMES FIRST AND IS NOT THE CALLER'S. The reference clears its
892 * cached result, re-runs `getAvg` to fill `odeStateVec`, and hands the passage
893 * time BOTH that vector and `result.solverSpecific.sn` -- the struct the FCFS
894 * refit may have re-fitted -- because the vector is laid out by the phase counts
895 * of that struct and not of the model's. Bundling the two here is what keeps a
896 * caller from pairing a state vector with the wrong struct, which would be a
897 * length error at best and a law of the wrong model at worst.
898 *
899 * A Source has no response time and is left empty, as is a pair whose class the
900 * station does not serve; an empty entry is an ABSENT law, never a degenerate
901 * one.
902 */
903template <class T>
904std::vector<std::vector<FluidPassage> > solver_fluid_cdf_respt(const qn::NetworkStruct<T>& sn,
905 const FluidOptions& opt,
906 std::size_t points = 201) {
909 if (r.xvec.empty())
910 throw UnsupportedError(
911 "solver_fluid_cdf_respt: the '" + r.method +
912 "' method reports no fluid state vector to mark a job in, so it has no passage time; "
913 "use 'closing', 'matrix' or a moment closure");
914 std::vector<std::vector<FluidPassage> > RD(
915 snr.nstations, std::vector<FluidPassage>(snr.nclasses));
916 for (std::size_t i = 0; i < snr.nstations; ++i) {
917 if (snr.stations[i].nodetype == qn::NodeType::Source) continue;
918 for (std::size_t c = 0; c < snr.nclasses; ++c) {
919 if (snr.disabled[i][c]) continue;
920 RD[i][c] = fluid_passage_time(snr, r.xvec, i + 1, c + 1, opt.tol, points, r.closure);
921 }
922 }
923 return RD;
924}
925
926/**
927 * Port of `@@SolverFLD/exportODEs.m` at the runner's own method resolution, so
928 * the exported system is the one `solver_fluid_run_analyzer` would integrate.
929 *
930 * `rmf` has no single exported drift -- the cache decomposition rewrites the
931 * routing between passes, so there is one fluid system per sweep and none of
932 * them is "the" model's -- and is refused by name rather than exported at the
933 * routing of whichever sweep happened to be last.
934 */
935template <class T>
937 const std::string& notation = "scalar",
938 const std::string& model_name = "model") {
939 const std::string m = detail::fluid_resolve_method(sn, opt.method);
940 if (m == "rmf")
941 throw UnsupportedError(
942 "solver_fluid_export_odes: a Cache model is solved by alternating a cache drift with a "
943 "queueing drift whose routing is rewritten at every sweep, so it has no one ODE system "
944 "to export; export the model without its Cache node, or read the sweeps through "
945 "solver_fld_cacheqn_tran");
946 FluidOptions o = opt;
947 o.method = m;
948 return fluid_export_odes(sn, o, notation, model_name);
949}
950
951} // namespace fluid
952} // namespace line
953
954#endif // LINE_SOLVERS_FLUID_FLUID_RUNNER_H
What a solver observed about the Cache nodes of a model.
UnsupportedError(const std::string &what)
Definition error.h:51
Raised when the moment closure cannot serve this model: the linearization at the fixed point is not h...
A network plus its refreshed NetworkStruct.
std::vector< std::vector< bool > > disabled
std::vector< Station< T > > stations
stations[k-1] is the k-th station
The exception types the port throws.
The fork-join fixed point that drives one inner MVA solve.
The Heidelberger-Trivedi fork-join transform, options.config.fork_join='ht'.
The INTEGRATED caching-queueing network under the fluid solver: ports of solver_fld_cacheqn_analyzer....
Port of solver_fluid_initsol.m, and of the entry point of solver_fluid_closing.m that consumes it.
The min-normal closure as a DIFFERENTIAL-ALGEBRAIC system: solver_fluid_dae.m.
@@SolverFLD/exportODEs.m: the fluid ODE system as a standalone LaTeX document, in a form meant to be ...
Port of solver_fluid_kp.m: the fluid AND diffusion limits of the (MAP_t/Ph_t/inf)^N network of Y.
The second-order fluid methods: fluid_moment_terms.m, fluid_lyapunov.m, fluid_drift_jacobian....
The one exception the fluid fallback ladder catches.
Response-time distribution by tagged fluid: a port of solver_fluid_passage_time.m,...
Fluid analysis of a stochastic Petri net: one simultaneous algebraic solve per active set.
Port of matlab/src/solvers/FLD/solver_fluid_qsys_analyzer.m: the single-station fluid limits.
Port of ode_eliminate_immediate.m, eliminate_immediate_matrix.m and ode_solve_stiff....
PetriSolution solver_fluid_petri(const qn::NetworkStruct< T > &sn, const PetriOptions &opt=PetriOptions())
Fluid analysis of a stochastic Petri net.
FluidCacheqnSolution< T > solver_fld_cacheqn_analyzer(const qn::NetworkStruct< T > &sn, const FluidOptions &opt)
Port of solver_fld_cacheqn_analyzer.m.
void fluid_check_method(const std::string &method)
Port of runAnalyzerChecks' method gate: an unlisted method is refused.
double fluid_default_horizon(const qn::NetworkStruct< T > &sn, const FluidOptions &opt)
The horizon a transient runs to when the caller gives none.
FluidSolution solver_fluid_kp(const qn::NetworkStruct< T > &sn, const FluidOptions &opt)
Port of solver_fluid_kp.m: the steady table at the horizon.
Definition fluid_kp.h:1055
FluidLayout fluid_layout(const qn::NetworkStruct< T > &sn)
Port of the layout half of solver_fluid_odes.m.
Definition fluid_odes.h:282
FluidSolution solver_fluid_dae(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, const FluidDaeOptions &dopt_in=FluidDaeOptions())
solver_fluid_dae.m: the min-normal closure solved as one system.
Definition fluid_dae.h:2153
FluidSolution solver_fluid_closing(const qn::NetworkStruct< T > &sn, const FluidOptions &opt)
Port of solver_fluid_closing.m: the closing family's entry point.
std::vector< std::string > fluid_list_valid_methods()
Port of SolverFLD.listValidMethods.
std::string solver_fluid_export_odes(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, const std::string &notation="scalar", const std::string &model_name="model")
Port of @@SolverFLD/exportODEs.m at the runner's own method resolution, so the exported system is the...
FluidDaeOptions fluid_dae_options(const FluidOptions &opt, const FluidDaeOptions &dopt)
The controls the DAE route actually reads: the struct a caller pinned, with whatever options....
Definition fluid_dae.h:903
FluidSolution solver_fluid(const qn::NetworkStruct< T > &sn_in, const FluidOptions &opt, qn::NetworkStruct< T > *sn_out=nullptr)
Port of solver_fluid_analyzer.m: dispatch on the method, refit the non-exponential FCFS stations the ...
FluidSolution solver_fluid_qsys(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, std::vector< FluidTranPoint > *traj=nullptr)
Solve a single-station model with one of the closed-form fluid limits.
Definition fluid_qsys.h:165
std::vector< FluidTranPoint > solver_fluid_tran_avg(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, std::size_t points=101)
getTranAvg on the first-order closing drift, over that horizon.
FluidPassage fluid_passage_time(const qn::NetworkStruct< T > &sn, const std::vector< double > &x_steady, std::size_t ist, std::size_t cls, double tol=1e-4, std::size_t points=201, const FluidClosure &closure=FluidClosure())
Response-time CDF at station ist for class cls, both 1-based.
FluidSolution solver_fluid_moments(const qn::NetworkStruct< T > &sn, const FluidOptions &opt)
Port of solver_fluid_moments.m: the second-order fluid analysis backing minnormal and refined.
FluidSolution solver_fluid_run_analyzer(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, qn::NetworkStruct< T > *sn_out=nullptr, qn::NetworkStruct< T > *refreshed_out=nullptr, solvers::CacheMetrics< T > *cache_out=nullptr)
Port of @@SolverFLD/runAnalyzer.m: resolve the method, route to the function the reference routes to,...
std::vector< std::vector< FluidPassage > > solver_fluid_cdf_respt(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, std::size_t points=201)
Port of @@SolverFLD/getCdfRespT: the response-time law of every (station, class) pair,...
std::string fluid_export_odes(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, const std::string &notation="scalar", const std::string &model_name="model")
Render the fluid ODE system of sn as a LaTeX document.
std::vector< FluidTranPoint > solver_fluid_run_transient(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, std::size_t points=101)
-a tran / @@SolverFLD/getTranAvg with the method HONOURED, which is the one place the reference does ...
std::vector< FluidTranPoint > solver_fluid_dae_transient(const qn::NetworkStruct< T > &sn, const FluidOptions &opt, double t_end, std::size_t points=101, const std::vector< double > &out_grid=std::vector< double >(), const FluidDaeOptions &dopt_in=FluidDaeOptions())
@@SolverFLD/getTranAvg for the DAE route: the metrics ALONG the trajectory.
Definition fluid_dae.h:2611
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:181
const char * sched_to_text(SchedStrategy s)
Definition lang_types.h:230
ReplacementStrategy
Cache replacement policies, with the values of MATLAB ReplacementStrategy.
Definition lang_types.h:380
@ FIFO
first in, first out
Definition lang_types.h:382
FjMmt< T > fj_fork_join_transform(const qn::NetworkStruct< T > &L, const std::string &method)
options.config.fork_join -> the transform it names.
Definition fj_ht.h:389
MvaSolution< T > fj_fixed_point(const qn::NetworkStruct< T > &L, FjMmt< T > &tr, std::vector< T > &lam, const MvaOptions &opt, InnerSolve inner)
Drive the fork-join fixed point of a transformed model to convergence.
Definition fj_driver.h:435
FeatureSet fluid_feature_set(const std::string &method)
SolverFLD.getFeatureSet, transcribed, MINUS what the requested method cannot evaluate – the port of @...
void check_binding_capacity(const std::string &solver, const NetworkStruct< T > &sn)
bool has_binding_capacity(const NetworkStruct< T > &sn)
getUsedLangFeatures: the features the MODEL uses.
void feature_gate(const std::string &solver, const FeatureSet &declared, const NetworkStruct< T > &sn, const std::string &requested_method="", const std::string &resolved_method="")
runAnalyzerChecks: refuse a model the solver does not declare, by name.
CacheMetrics< T > cache_metrics_of_matrix(const qn::NetworkStruct< T > &sn, const Matrix< T > &hitprob, const Matrix< T > &missprob)
The same, for the integrated caching-queueing branch, whose hit and miss probabilities are (ncaches x...
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
A queueing network and its refreshed NetworkStruct.
The DECLARED side of the gate: one feature set per solver.
SolverFluid: the closing method, a port of solver_fluid.m, solver_fluid_iteration....
What the steady analyzer returns: the fluid metrics plus the converged split.
Matrix< T > missprob
(ncaches x nclasses)
qn::NetworkStruct< T > refreshed
The struct whose cache self-switch carries the CONVERGED split rather than the offered one,...
Matrix< T > hitprob
(ncaches x nclasses), cache order as the node scan
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 > xvec
the converged fluid state
static constexpr double FineTol
Definition lang_types.h:760
static constexpr double Zero
Definition lang_types.h:762
A node of the network.
Every Cache node of the model, in node order; empty on a model with none.