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