LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
infer_fmlps.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_API_INFER_INFER_FMLPS_H
6#define LINE_API_INFER_INFER_FMLPS_H
7
8/**
9 * @file
10 * @ingroup api_infer
11 * Fluid response-time likelihood, and the FMLPS demand estimator built on it.
12 *
13 * Port of matlab/src/api/infer/infer_fluid_ps_rt_likelihood.m and
14 * infer_fmlps.m. Both are MATLAB-ONLY: no JAR, no native-Python twin.
15 *
16 * Reference: Casale et al., "Fluid Analysis of Queueing in Processor Sharing
17 * Systems".
18 *
19 * WHAT THE LIKELIHOOD IS. MLPS builds an exact CTMC per observation and reads a
20 * phase-type density; that is exact and costs a state space. FMLPS replaces it
21 * with the FLUID limit: mark one unit of fluid at the reference station in the
22 * tagged class, let the deterministic drift carry it, and the passage-time
23 * density at the observed response time is `-d/dt (marked mass) / mass0`. The
24 * marked mass is nonincreasing, so that derivative is the density and the
25 * likelihood needs no separate normalization.
26 *
27 * THE AUGMENTATION IS THE SAME ONE `fluid_passage_time` PERFORMS, and this file
28 * deliberately reuses it rather than re-deriving the drift. The reference
29 * expands `sn.mu`, `sn.phi`, `sn.proc` and the routing table from K to K+1
30 * classes by hand and hands the loose arrays to `solver_fluid_odes`; the C++
31 * fluid solver takes a `NetworkStruct`, and the marked-fluid construction it
32 * already carries is exactly the tagged class K+1 -- a departure of the marked
33 * block UNMARKS the fluid and delivers it wherever the class routes, which is
34 * precisely the reference's "absorption at refIdx: the tagged class switches
35 * back to the original classes". Re-transcribing the routing expansion would
36 * duplicate the one part of the reference where the index arithmetic
37 * (`l:Kc:end` against `l:K:end`) is easiest to get wrong.
38 *
39 * TWO DIFFERENCES FROM THE CTMC ESTIMATOR THAT A CALLER MUST KNOW:
40 *
41 * - the fluid density is a LIMIT, so on a small population it is an
42 * approximation where MLPS is exact. It is what buys a model whose state
43 * space MLPS cannot enumerate;
44 * - the reference returns a likelihood of exactly ZERO when the integration
45 * terminates before the observed time (the marked fluid is gone). That is
46 * kept: a zero likelihood is information, and replacing it with a floor
47 * would make an impossible observation look merely unlikely.
48 *
49 * ARITHMETIC: double, following the ODE.
50 */
51
52#include <algorithm>
53#include <cmath>
54#include <cstddef>
55#include <limits>
56#include <vector>
57
60#include "line/num/number.h"
62#include "line/util/error.h"
63#include "line/util/matrix.h"
65#include "line/util/lsoda.h"
66#include "line/util/ode.h"
67
68namespace line {
69namespace api {
70
71/** What the fluid likelihood call reports. */
73 double like = 0.0; ///< the passage-time density at the observed time
74 double marked0 = 0.0; ///< the marked mass placed at t = 0
75 double markedT = 0.0; ///< what is left of it at the observed time
76 std::size_t nstates = 0; ///< size of the augmented system
77};
78
79namespace fmlpsdetail {
80
81/**
82 * The augmented drift: the untagged system plus one marked block at (i, c).
83 *
84 * This is `fluid_passage_time`'s construction. The station's service share is
85 * computed on the FOLDED state -- marked mass added back into the block it came
86 * from -- because a processor-sharing server does not know which of its jobs is
87 * tagged, and computing the share without folding would give the marked job a
88 * larger share than it has.
89 */
90struct TaggedDrift {
91 const fluid::FluidOdeSystem* sys;
92 std::size_t base, tag0, P, n, nt;
93 struct TagEvent {
94 std::size_t minus, plus, event_idx;
95 double rate_base;
96 };
97 std::vector<TagEvent> tev;
98};
99
100inline TaggedDrift build_tagged(const fluid::FluidOdeSystem& sys, std::size_t i, std::size_t c) {
101 TaggedDrift td;
102 td.sys = &sys;
103 td.P = sys.layout.kic[i][c];
104 td.base = sys.layout.qidx[i][c];
105 td.n = sys.layout.nstates;
106 td.tag0 = td.n;
107 td.nt = td.n + td.P;
108 for (std::size_t e = 0; e < sys.events.size(); ++e) {
109 const fluid::FluidEvent& ev = sys.events[e];
110 if (ev.event_idx < td.base || ev.event_idx >= td.base + td.P) continue;
111 const std::size_t k = ev.event_idx - td.base;
112 TaggedDrift::TagEvent t2;
113 t2.event_idx = td.tag0 + k;
114 t2.rate_base = ev.rate_base;
115 t2.minus = td.tag0 + k;
116 if (e < sys.n_departures) {
117 // A departure UNMARKS the fluid: it leaves the measured block and
118 // arrives untagged wherever the class routes. That is the
119 // reference's absorption.
120 t2.plus = ev.plus;
121 } else {
122 t2.plus = td.tag0 + (ev.plus - td.base); // a phase change stays marked
123 }
124 td.tev.push_back(t2);
125 }
126 return td;
127}
128
129} // namespace fmlpsdetail
130
131/**
132 * The fluid passage-time density at one observed response time.
133 *
134 * @param sn the network
135 * @param ist 1-based reference station where the tagged job sits
136 * @param cls 1-based tagged class
137 * @param levels (nstates) initial fluid state of the UNAUGMENTED system
138 * @param rsampled the observed response time
139 * @param marked the marked mass, the reference's `newFluid`; one job
140 */
141template <class T>
143 std::size_t cls, const std::vector<double>& levels,
144 double rsampled, double marked = 1.0) {
145 const std::size_t M = sn.nstations, K = sn.nclasses;
146 if (ist == 0 || ist > M) throw InputError("infer_fluid_ps_rt_likelihood: station out of range");
147 if (cls == 0 || cls > K) throw InputError("infer_fluid_ps_rt_likelihood: class out of range");
148 if (!(rsampled > 0.0))
149 throw InputError("infer_fluid_ps_rt_likelihood: the response time must be positive");
150 if (!(marked > 0.0))
151 throw InputError("infer_fluid_ps_rt_likelihood: the marked mass must be positive");
152
153 const std::size_t i = ist - 1, c = cls - 1;
155 const fluid::FluidLayout& L = sys.layout;
156 if (levels.size() != L.nstates)
157 throw InputError(
158 "infer_fluid_ps_rt_likelihood: the initial fluid state has the wrong length for this "
159 "model");
160
161 const fmlpsdetail::TaggedDrift td = fmlpsdetail::build_tagged(sys, i, c);
163 out.nstates = td.nt;
164 out.marked0 = marked;
165 if (td.P == 0) {
166 // The class is not served at this station, so there is no passage to
167 // measure and no density to report.
168 out.like = 0.0;
169 return out;
170 }
171
172 // The observed job is MOVED out of its class into the marked block, so the
173 // total fluid is unchanged and the state the job found is what the drift
174 // starts from.
175 std::vector<double> y0(td.nt, 0.0);
176 for (std::size_t s = 0; s < td.n; ++s) y0[s] = levels[s];
177 double avail = 0.0;
178 for (std::size_t k = 0; k < td.P; ++k) avail += y0[td.base + k];
179 if (avail + 1e-12 < marked)
180 throw InputError(
181 "infer_fluid_ps_rt_likelihood: the initial state holds less fluid in the tagged block "
182 "than the observation marks, so the observed job is not in the state it arrived to");
183 // Remove the marked mass proportionally over the block's phases, which is
184 // what leaves the rest of the block undisturbed.
185 for (std::size_t k = 0; k < td.P; ++k) y0[td.base + k] -= marked * (y0[td.base + k] / avail);
186 y0[td.tag0] = marked; // all of it enters in phase one
187
188 const std::size_t base = td.base, tag0 = td.tag0, P = td.P, n = td.n, nt = td.nt;
189 const std::vector<fmlpsdetail::TaggedDrift::TagEvent> tev = td.tev;
190 auto drift = [&sys, &tev, base, tag0, P, n, nt](double, const double* x, double* dx) {
191 std::vector<double> xb(x, x + n);
192 std::vector<double> g(xb);
193 for (std::size_t k = 0; k < P; ++k) g[base + k] += x[tag0 + k];
194 std::vector<double> gg(g);
195 fluid::fluid_rates_closing(sys, g.data(), gg);
196 double blk = 0.0, gblk = 0.0;
197 for (std::size_t k = 0; k < P; ++k) {
198 blk += g[base + k];
199 gblk += gg[base + k];
200 }
201 const double share = (blk > 0.0) ? gblk / blk : 1.0;
202
203 for (std::size_t s = 0; s < nt; ++s) dx[s] = 0.0;
204 for (std::size_t e = 0; e < sys.events.size(); ++e) {
205 const fluid::FluidEvent& ev = sys.events[e];
206 double drive = gg[ev.event_idx];
207 if (ev.event_idx >= base && ev.event_idx < base + P)
208 drive = xb[base + (ev.event_idx - base)] * share;
209 const double r = ev.rate_base * drive;
210 if (r == 0.0) continue;
211 dx[ev.minus] -= r;
212 dx[ev.plus] += r;
213 }
214 for (std::size_t e = 0; e < tev.size(); ++e) {
215 const double r = tev[e].rate_base * x[tev[e].event_idx] * share;
216 if (r == 0.0) continue;
217 dx[tev[e].minus] -= r;
218 dx[tev[e].plus] += r;
219 }
220 };
221
222 std::vector<double> grid;
223 grid.push_back(0.0);
224 grid.push_back(rsampled);
225 LsodaOptions lopt;
226 lopt.rtol = 1e-5;
227 lopt.atol = 1e-8;
228 const LsodaSolution sol = fluid::fluid_integrate_grid(drift, y0, grid, lopt);
229 if (sol.y.empty()) throw InputError("infer_fluid_ps_rt_likelihood: the ODE returned no state");
230
231 const std::vector<double>& yT = sol.y.back();
232 double left = 0.0;
233 for (std::size_t k = 0; k < P; ++k) left += std::max(0.0, yT[tag0 + k]);
234 out.markedT = left;
235
236 // The density is the rate at which the marked mass is leaving, normalized
237 // by the mass placed. Reading the derivative rather than differencing the
238 // trajectory is what the reference does and is what stays accurate when the
239 // curve is nearly flat.
240 std::vector<double> dy(nt, 0.0);
241 drift(rsampled, yT.data(), dy.data());
242 double ddt = 0.0;
243 for (std::size_t k = 0; k < P; ++k) ddt += dy[tag0 + k];
244 out.like = -ddt / marked;
245 // The marked mass is nonincreasing, so a negative density is round-off at
246 // an exhausted block, not a model result.
247 if (out.like < 0.0) out.like = 0.0;
248 return out;
249}
250
251/**
252 * FMLPS: the fluid analogue of MLPS.
253 *
254 * Same objective as `infer_mlps` -- the negative log-likelihood of the observed
255 * response times -- with the exact phase-type density replaced by the fluid
256 * passage-time density above. The initial fluid state of each observation is
257 * the per-class queue length it found, spread over the phases of each block.
258 *
259 * @param sn a template network whose station `ist` carries the demands
260 * being estimated; only its service RATES are varied
261 * @param ist 1-based PS station
262 * @param samples the observations
263 */
264template <class T>
265std::vector<double> infer_fmlps(const qn::NetworkStruct<T>& sn, std::size_t ist,
266 const std::vector<MlpsSample>& samples) {
267 const std::size_t M = sn.nstations, K = sn.nclasses;
268 if (ist == 0 || ist > M) throw InputError("infer_fmlps: station out of range");
269 if (samples.empty()) throw InputError("infer_fmlps: no observations");
270 for (std::size_t s = 0; s < samples.size(); ++s) {
271 if (samples[s].cls < 1 || samples[s].cls > K)
272 throw InputError("infer_fmlps: a sample names a class outside 1..K");
273 if (samples[s].ql.size() != K)
274 throw InputError("infer_fmlps: a sample's queue length has the wrong width");
275 if (!(samples[s].rt > 0.0))
276 throw InputError("infer_fmlps: a response time must be positive");
277 }
278
279 // The reference's starting point and box, as in MLPS.
280 double meanQL = 0.0, rtmax = 0.0;
281 for (std::size_t s = 0; s < samples.size(); ++s) {
282 for (std::size_t r = 0; r < K; ++r) meanQL += samples[s].ql[r];
283 rtmax = std::max(rtmax, samples[s].rt);
284 }
285 meanQL /= static_cast<double>(samples.size());
286 const double nCores = sn.stations[ist - 1].nservers;
287 const double Vtilde = std::min(meanQL, std::isinf(nCores) ? meanQL : nCores);
288 std::vector<double> x0(K, 1e-3);
289 for (std::size_t r = 0; r < K; ++r) {
290 double sum = 0.0;
291 std::size_t cnt = 0;
292 for (std::size_t s = 0; s < samples.size(); ++s)
293 if (samples[s].cls == r + 1) {
294 sum += samples[s].rt;
295 ++cnt;
296 }
297 if (cnt > 0 && meanQL > 0.0) x0[r] = Vtilde * (sum / static_cast<double>(cnt)) / meanQL;
298 }
299
300 const double TOL = 1e-6;
301 auto objective = [&](const std::vector<double>& x) {
302 for (std::size_t r = 0; r < K; ++r)
303 if (!(x[r] > 0.0)) return std::numeric_limits<double>::infinity();
304
306 for (std::size_t r = 0; r < K; ++r)
307 mod.set_service(ist, r + 1,
309 mod.refresh_rates();
310
312 double f = 0.0;
313 for (std::size_t s = 0; s < samples.size(); ++s) {
314 // The observed queue length, spread over each block's phases.
315 std::vector<double> lev(sysx.layout.nstates, 0.0);
316 for (std::size_t r = 0; r < K; ++r) {
317 const std::size_t P = sysx.layout.kic[ist - 1][r];
318 if (P == 0) continue;
319 for (std::size_t k = 0; k < P; ++k)
320 lev[sysx.layout.qidx[ist - 1][r] + k] =
321 samples[s].ql[r] / static_cast<double>(P);
322 }
323 double like = 0.0;
324 try {
325 like = infer_fluid_ps_rt_likelihood(mod, ist, samples[s].cls, lev, samples[s].rt)
326 .like;
327 } catch (const Error&) {
328 // An observation the model cannot host contributes the floor
329 // rather than aborting the whole fit.
330 like = 0.0;
331 }
332 f -= std::log(TOL + std::max(0.0, like));
333 }
334 return f;
335 };
336
337 std::vector<Bound<double>> bounds(K);
338 for (std::size_t r = 0; r < K; ++r) {
339 bounds[r].has_lo = true;
340 bounds[r].has_hi = true;
341 bounds[r].lo = 0.0;
342 bounds[r].hi = rtmax;
343 }
344 return nelder_mead_box(objective, x0, bounds).x;
345}
346
347} // namespace api
348} // namespace line
349
350#endif // LINE_API_INFER_INFER_FMLPS_H
Base error for the multiprecision C++ port.
Definition error.h:31
InputError(const std::string &what)
Definition error.h:39
A network plus its refreshed NetworkStruct.
void refresh_rates()
Port of MNetwork.refreshRates: lower each service process onto a rate and an SCV.
void set_service(std::size_t station, std::size_t cls, const Distrib< T > &d)
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...
Port of ode_eliminate_immediate.m, eliminate_immediate_matrix.m and ode_solve_stiff....
Maximum-likelihood service-demand estimation at a processor-sharing queue.
LSODA: the LINE-facing wrapper over the vendored solver in third_party/lsoda.hpp.
Dense matrix and non-owning view.
std::vector< double > infer_fmlps(const qn::NetworkStruct< T > &sn, std::size_t ist, const std::vector< MlpsSample > &samples)
FMLPS: the fluid analogue of MLPS.
FluidRtLikelihood infer_fluid_ps_rt_likelihood(const qn::NetworkStruct< T > &sn, std::size_t ist, std::size_t cls, const std::vector< double > &levels, double rsampled, double marked=1.0)
The fluid passage-time density at one observed response time.
LsodaSolution fluid_integrate_grid(const std::function< void(double, const double *, double *)> &f, const std::vector< double > &y0, const std::vector< double > &grid, const LsodaOptions &lopt)
The same retry over a whole output grid, for the callers that ask LSODA for a trajectory rather than ...
FluidOdeSystem fluid_ode_system(const qn::NetworkStruct< T > &sn)
Build the drift of sn: the port of ode_jumps_new and ode_rate_base fused into one pass.
Definition fluid_odes.h:322
void fluid_rates_closing(const FluidOdeSystem &sys, const double *x, std::vector< double > &g)
The reference's ode_rates_closing name, kept for the first-order callers.
Definition fluid_odes.h:646
NelderMeadResult< T > nelder_mead_box(F f, const std::vector< T > &x0, const std::vector< Bound< T > > &bounds, const NelderMeadOptions< T > &opt)
Box-constrained simplex minimization by the transformation described in the header comment.
Definition neldermead.h:370
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
Adaptive stiff ODE integrator: a four-stage Rosenbrock method of order four with an embedded order-th...
Integration controls.
Definition lsoda.h:64
double atol
absolute tolerance, applied to every component
Definition lsoda.h:66
double rtol
relative tolerance, applied to every component
Definition lsoda.h:65
Result of an integration, mirroring OdeSolution in ode.h.
Definition lsoda.h:126
std::vector< std::vector< double > > y
y[i] is the state at t[i]
Definition lsoda.h:128
What the fluid likelihood call reports.
Definition infer_fmlps.h:72
double markedT
what is left of it at the observed time
Definition infer_fmlps.h:75
std::size_t nstates
size of the augmented system
Definition infer_fmlps.h:76
double like
the passage-time density at the observed time
Definition infer_fmlps.h:73
double marked0
the marked mass placed at t = 0
Definition infer_fmlps.h:74
One event of the drift.
Definition fluid_odes.h:101
std::size_t event_idx
state entry whose g(x) drives this rate
Definition fluid_odes.h:104
double rate_base
the model-fixed part of the rate
Definition fluid_odes.h:105
Where each (station, class) block sits in the state vector.
Definition fluid_odes.h:86
std::vector< std::vector< std::size_t > > qidx
0-based first index of (i,r)
Definition fluid_odes.h:88
std::size_t nstates
length of the state vector
Definition fluid_odes.h:87
std::vector< std::vector< std::size_t > > kic
phases held by (i,r); 0 when disabled
Definition fluid_odes.h:89
std::size_t n_departures
How many leading entries of events are DEPARTURES (a job completing at one block and starting at anot...
Definition fluid_odes.h:206
std::vector< FluidEvent > events
Definition fluid_odes.h:197
static Distrib exp_rate(const T &r)
Definition lang_types.h:814