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