LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fluid_closing.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_CLOSING_H
6#define LINE_SOLVERS_FLUID_FLUID_CLOSING_H
7
8/**
9 * @file
10 * @ingroup line_solvers
11 * Port of `solver_fluid_initsol.m`, and of the entry point of
12 * `solver_fluid_closing.m` that consumes it.
13 *
14 * WHY THE INITIAL CONDITION IS NOT AN IMPLEMENTATION DETAIL. The fluid answer
15 * is a fixed point of the drift, and a drift that is not convex has more than
16 * one: a closed model whose queue can either drain or saturate settles wherever
17 * it was pushed from. The integration therefore SELECTS a fixed point, and what
18 * selects it is y0. The reference does not guess: it decodes the model's own
19 * initial state, `sn.state`, into the fluid coordinates, so the ODE starts
20 * where the model says the system starts. Reproducing that exactly is the whole
21 * point of this file; a start that merely conserves the population is a
22 * different question with a possibly different answer.
23 *
24 * WHERE `sn.state` WENT. This port's NetworkStruct carries no state field, so
25 * there is nothing to read. What the reference reads, however, is the state
26 * `Network.initDefault` wrote: every closed class at its reference station,
27 * one job per class in service at each Source, everything else empty, and every
28 * started job in PHASE ONE -- `State.fromMarginalAndStarted` writes
29 * `init(1) = si(r)` and never enumerates the phase assignment.
30 *
31 * THE ONE RULE THAT IS EASY TO MISS. Outside a Source, phase one of a block
32 * does NOT get `kir(r,1)`: it gets `nir(r) - sum_{k>=2} kir(r,k)`, the jobs in
33 * service in phase one PLUS everyone waiting in the buffer. The fluid state has
34 * no waiting room -- a queue is one mass per phase -- so the buffer has to land
35 * somewhere, and the reference restarts it in phase one. On the state
36 * `initDefault` writes every `kir(r,k>=2)` is zero, so that term returns the
37 * station's whole population to phase one and the decode collapses to the
38 * closed form `detail::fluid_default_initsol` computes directly. The rule is
39 * recorded because it is what makes the two the same vector, not because two
40 * of them are kept.
41 *
42 * WHAT THIS FILE DOES NOT REDO. The drift, the restarting integration and the
43 * Q/U/R/T extraction of `solver_fluid_closing.m` are already ported in
44 * `fluid_odes.h` and `solver_fluid.h` (`fluid_closing_metrics`,
45 * `detail::fluid_dispatch`). `solver_fluid_closing` below is the reference
46 * function's SHAPE -- seed the initial condition, gate the method, integrate --
47 * over that machinery, so there is one copy of the metric rules and not two.
48 * The reference's second output, the expanded `state` cell, is not ported: no
49 * caller in `solver_fluid_analyzer.m` reads it.
50 */
51
52#include <cmath>
53#include <cstddef>
54#include <string>
55#include <type_traits>
56#include <vector>
57
61#include "line/util/error.h"
62
63namespace line {
64namespace fluid {
65
66namespace detail {
67
68/**
69 * The disciplines `solver_fluid_initsol.m` knows how to decode.
70 *
71 * The reference's switch has two arms and an error arm, and the error arm is
72 * not a gap to be filled by falling through to the first arm: a discipline
73 * outside this list either holds its buffer in an encoding whose waiting jobs
74 * cannot be attributed to a phase (POLLING carries a controller, SRPT a
75 * remaining-work order) or is preemptive-resume, where a waiting job's phase is
76 * recorded per job and restarting it in phase one throws that away. Both would
77 * start the drift somewhere the model never is.
78 *
79 * GPS IS DECODED HERE AND STILL REFUSED BY MOST METHODS, which is not a
80 * contradiction: its state encoding folds into one mass per phase exactly like
81 * PS's, so `solver_fluid_initsol.m` lists it, while its capacity SHARE needs the
82 * backlog probability that only `minnormal` supplies. The refusal therefore
83 * belongs to the featset gate (`fluid_feature_set`), per method, and not here.
84 *
85 * Checked BEFORE the state row is decoded, which the reference does after; for
86 * an accepted discipline the two are the same, and for a refused one this
87 * reports the discipline rather than whatever the encoder happens to say first.
88 */
89inline void fluid_initsol_check_sched(lang::SchedStrategy s, std::size_t ist) {
90 switch (s) {
101 return;
102 default:
103 break;
104 }
105 throw UnsupportedError(
106 std::string("solver_fluid_initsol: station ") + std::to_string(ist) + " is scheduled '" +
108 "', whose state encoding this port cannot fold into one fluid mass per service phase; "
109 "only ext, fcfs, siro, ps, inf, dps, gps, hol, lcfs and lcfspr are decoded");
110}
111
112} // namespace detail
113
114/**
115 * Port of `solver_fluid_initsol.m`: the ODE's initial condition, in the layout
116 * the drift indexes.
117 *
118 * THIS IS THE DISCIPLINE GATE OVER `detail::fluid_default_initsol`, AND NOT A
119 * SECOND CONSTRUCTION OF THE SAME VECTOR. There used to be two: this one
120 * synthesized the `initDefault` marginal, pushed it through `from_marginal`,
121 * took the FIRST row and decoded it back through `to_marginal`, while the
122 * analyzer's own default wrote the population straight into the reference
123 * station's phase-one entry. They agreed on a one-phase model and disagreed on
124 * every other, so a multi-phase TRANSIENT was integrated from one of two
125 * different initial conditions depending on which entry point was called. The
126 * closed form is the correct one, for two reasons:
127 *
128 * THE ROW WAS THE WRONG ROW. `from_marginal` enumerates every phase
129 * assignment of the jobs in service in `multichoose` order, whose first row
130 * is `[0,...,0,n]` -- all of the mass in the LAST phase. The reference never
131 * enumerates: `State.fromMarginalAndStarted` writes `init(1) = si(r)` and so
132 * always starts its jobs in phase ONE, which is also what its
133 * `space(end:-1:1,:)` reordering exists to guarantee. Seeding an Erlang-2
134 * station's whole population in phase two starts the drift half a service
135 * ahead of where the model says the system is.
136 *
137 * AND IT WAS PAID FOR IN FACTORIALS. The FCFS branch of `from_marginal`
138 * enumerates the buffer PERMUTATIONS, so decoding the initial state of a
139 * closed station holding N jobs cost O(N!) before the first integration step.
140 *
141 * What the decode contributes that the closed form does not is the by-name
142 * refusal above, which is checked here per station and is the reference's own
143 * error arm; it is kept, and is the reason this wrapper exists at all.
144 */
145template <class T>
146std::vector<double> fluid_initsol(const qn::NetworkStruct<T>& sn, const FluidLayout& L) {
147 const std::size_t M = sn.nstations, K = sn.nclasses;
148 for (std::size_t i = 0; i < M; ++i) {
149 // A station with no enabled class occupies no fluid coordinates, which
150 // is how the reference's `isnan(ist)` skip comes out here.
151 bool any = false;
152 for (std::size_t r = 0; r < K; ++r) any = any || L.enabled[i][r];
153 if (any) detail::fluid_initsol_check_sched(sn.stations[i].sched, i + 1);
154 }
155 return detail::fluid_default_initsol(sn, L);
156}
157
158/** The same, for a caller that has not built the layout itself. */
159template <class T>
160std::vector<double> fluid_initsol(const qn::NetworkStruct<T>& sn) {
162}
163
164/**
165 * Port of `solver_fluid_closing.m`: the closing family's entry point.
166 *
167 * IT RETURNS THE UNCORRECTED TABLE, as the reference function does. The
168 * utilization and response time that reach a user go through
169 * `fluid_analyzer_correct`, which `solver_fluid_analyzer.m` applies AFTER the
170 * method switch and to every branch alike; applying it here as well would
171 * either double it or fork it. Callers who want the analyzer's answer call
172 * `solver_fluid`, which is that function.
173 *
174 * The method is gated rather than forwarded because the analyzer's switch sends
175 * only these names here; `matrix` and `pnorm` are a different drift and
176 * `mfq`, `diffusion` and `rmf` are different solvers entirely, and answering
177 * for them under this name would report one method's number as another's.
178 */
179template <class T>
181 if (!std::is_same<T, double>::value)
182 throw UnsupportedError(
183 "solver_fluid_closing: the fluid solver integrates its drift with LSODA, whose "
184 "coefficients assume double precision; rerun with --arith double");
185
186 std::string m = opt.method;
187 if (m.size() > 6 && m.compare(0, 6, "fluid.") == 0) m = m.substr(6);
188 // `default` at THIS entry point means the closing drift: the analyzer's own
189 // default resolves to the matrix method, and a caller who wants that calls
190 // `solver_fluid`.
191 if (m == "default") m = "closing";
192 if (!(m == "closing" || m == "statedep" || m == "softmin" || m == "tbi"))
193 throw UnsupportedError("solver_fluid_closing: the '" + opt.method +
194 "' method is not part of the closing family; 'matrix' and 'pnorm' "
195 "are solved by solver_fluid_matrix and 'mfq', 'diffusion' and 'rmf' "
196 "by their own solvers, all reachable through solver_fluid");
197
198 FluidOptions o = opt;
199 o.method = m;
200 // The reference fills `options.init_sol` in the analyzer, before the switch,
201 // and re-fills it after every phase refitting; an empty one here means the
202 // caller has not overridden it, not that the drift may start anywhere.
203 if (o.init_sol.empty()) o.init_sol = fluid_initsol(sn, fluid_layout(sn));
204 return detail::fluid_dispatch(sn, o);
205}
206
207} // namespace fluid
208} // namespace line
209
210#endif // LINE_SOLVERS_FLUID_FLUID_CLOSING_H
UnsupportedError(const std::string &what)
Definition error.h:51
A network plus its refreshed NetworkStruct.
The exception types the port throws.
The fluid drift: a port of solver_fluid_odes.m and the ode_jumps_new / ode_rate_base / ode_rates_clos...
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_closing(const qn::NetworkStruct< T > &sn, const FluidOptions &opt)
Port of solver_fluid_closing.m: the closing family's entry point.
std::vector< double > fluid_initsol(const qn::NetworkStruct< T > &sn, const FluidLayout &L)
Port of solver_fluid_initsol.m: the ODE's initial condition, in the layout the drift indexes.
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
A queueing network and its refreshed NetworkStruct.
SolverFluid: the closing method, a port of solver_fluid.m, solver_fluid_iteration....
Where each (station, class) block sits in the state vector.
Definition fluid_odes.h:86
std::vector< std::vector< bool > > enabled
whether (i,r) is served at all
Definition fluid_odes.h:90
Controls, defaulting to SolverOptions('Fluid') in the reference.
std::vector< double > init_sol
initial state; empty selects the default below
What the analyzer returns, in the same shape as the MVA solver's result.