LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
infer_mlps.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_MLPS_H
6#define LINE_API_INFER_INFER_MLPS_H
7
8/**
9 * @file
10 * @ingroup api_infer
11 * Maximum-likelihood service-demand estimation at a processor-sharing queue.
12 *
13 * Port of matlab/src/api/infer/infer_mlps.m and infer_minps.m. THESE ARE
14 * MATLAB-ONLY: there is no JAR or native-Python twin, so MATLAB is not merely
15 * the ground truth here, it is the only prior art.
16 *
17 * WHAT MLPS IS. Each observation is a response time `rt`, the class of the
18 * tagged job, and the per-class queue length seen ON ARRIVAL. For a given
19 * vector of mean demands, the sojourn of a tagged job that arrives into a known
20 * queue state is the absorption time of a small CTMC: build the same PS queue
21 * with one EXTRA class carrying the tagged job, mark every transition that is
22 * the tagged job departing, and the remaining sub-generator is the phase-type
23 * representation of that sojourn. The likelihood of one sample is that
24 * phase-type density at `rt`, and the estimate maximizes their product.
25 *
26 * WHY THE AUGMENTED MODELS ARE BUILT ONCE. The state space, the departure event
27 * indices and the absorbing subset depend only on the (tagged class, arrival
28 * queue length) PAIR, not on the demands being optimized. The reference caches
29 * them per distinct pair and rebuilds only the rates inside the objective; so
30 * does this port, because the enumeration is the expensive part and it would
31 * otherwise be repeated once per likelihood evaluation.
32 *
33 * TWO SUBSTITUTIONS FOR MATLAB:
34 *
35 * 1. `fmincon` with BOX bounds only and no other constraint is
36 * `nelder_mead_box`. The reference passes empty A, b, Aeq, beq and no
37 * nonlinear constraint, so the interior-point machinery is doing nothing an
38 * ordinary box-constrained minimizer does not; the objective is a smooth
39 * log-likelihood in a handful of variables.
40 * 2. `sn_set_service_coc` HAS NO C++ COUNTERPART AND NEEDS NONE. It exists only
41 * to write MATLAB's `sn.mu{i}{k}` cell-of-cells without reshaping the cell
42 * array, a storage quirk of that language; here the service law is
43 * `sn.service[i][r]`, an ordinary vector, and setting it is setting it.
44 *
45 * ARITHMETIC: double. The optimizer and the phase-type density are floating
46 * point, and the reference's tolerances are absolute in double.
47 */
48
49#include <algorithm>
50#include <cmath>
51#include <cstddef>
52#include <limits>
53#include <map>
54#include <string>
55#include <vector>
56
61#include "line/num/number.h"
63#include "line/util/error.h"
64#include "line/util/matrix.h"
66
67namespace line {
68namespace api {
69
70/** One observation: a response time, the tagged class, and the arrival state. */
71struct MlpsSample {
72 double rt = 0.0;
73 std::size_t cls = 0; ///< 1-based class of the tagged job
74 std::vector<double> ql; ///< per-class queue length seen on arrival
75};
76
77namespace mlpsdetail {
78
79/**
80 * Everything about one (tagged class, arrival queue length) pair that does NOT
81 * move with the demands: the augmented model, the absorbing subset and the
82 * transitions that are the tagged job departing.
83 */
84struct MlpsPrebuilt {
85 std::size_t tagClass = 0; ///< 1-based, in the ORIGINAL class space
86 std::vector<double> N; ///< augmented population, length R+1
87 std::vector<std::size_t> depSync; ///< indices into the sync list
88 std::vector<std::size_t> subset; ///< states with the tagged job at the queue
89 Matrix<double> SSqueue; ///< the queue block of those states
90 qn::Network<double> model; ///< the augmented model, rates still placeholders
91 std::size_t queueNode = 0, delayNode = 0;
92
93 explicit MlpsPrebuilt(const qn::Network<double>& m) : model(m) {}
94};
95
96/** The row of `SSqueue` equal to `N`, or npos. */
97inline std::size_t match_row(const Matrix<double>& S, const std::vector<double>& N) {
98 for (std::size_t i = 0; i < S.rows(); ++i) {
99 bool eq = true;
100 for (std::size_t j = 0; j < S.cols() && j < N.size(); ++j)
101 if (std::fabs(S(i, j) - N[j]) > 1e-9) eq = false;
102 if (eq) return i;
103 }
104 return static_cast<std::size_t>(-1);
105}
106
107} // namespace mlpsdetail
108
109/**
110 * MLPS demand estimation at a PS queue.
111 *
112 * @param muZ (R) think-time rates of the delay station
113 * @param nCores servers at the PS queue
114 * @param samples the observations
115 * @return (R) estimated mean service demands
116 */
117inline std::vector<double> infer_mlps(const std::vector<double>& muZ, double nCores,
118 const std::vector<MlpsSample>& samples) {
119 using namespace mlpsdetail;
120 const std::size_t R = muZ.size();
121 if (R == 0) throw InputError("infer_mlps: at least one class is required");
122 if (samples.empty()) throw InputError("infer_mlps: no observations");
123 for (std::size_t i = 0; i < samples.size(); ++i) {
124 if (samples[i].cls < 1 || samples[i].cls > R)
125 throw InputError("infer_mlps: a sample names a class outside 1..R");
126 if (samples[i].ql.size() != R)
127 throw InputError("infer_mlps: a sample's queue length has the wrong width");
128 if (!(samples[i].rt > 0.0))
129 throw InputError("infer_mlps: a response time must be positive");
130 }
131
132 // ---- the initial point, as the reference forms it -------------------
133 double meanQL = 0.0;
134 for (std::size_t i = 0; i < samples.size(); ++i)
135 for (std::size_t r = 0; r < R; ++r) meanQL += samples[i].ql[r];
136 meanQL /= static_cast<double>(samples.size());
137 const double Vtilde = std::min(meanQL, nCores);
138 std::vector<double> x0(R, 1e-3), lo(R, 0.0), hi(R, 0.0);
139 double rtmax = 0.0;
140 for (std::size_t i = 0; i < samples.size(); ++i) rtmax = std::max(rtmax, samples[i].rt);
141 for (std::size_t r = 0; r < R; ++r) {
142 double sum = 0.0;
143 std::size_t cnt = 0;
144 for (std::size_t i = 0; i < samples.size(); ++i)
145 if (samples[i].cls == r + 1) {
146 sum += samples[i].rt;
147 ++cnt;
148 }
149 if (cnt > 0 && meanQL > 0.0)
150 x0[r] = Vtilde * (sum / static_cast<double>(cnt)) / meanQL;
151 hi[r] = rtmax;
152 }
153
154 // ---- one augmented model per distinct (tagged class, arrival state) --
155 const std::size_t newR = R + 1;
156 std::vector<std::string> keys;
157 std::vector<MlpsPrebuilt> pre;
158 std::map<std::string, std::size_t> keyIndex;
159 auto key_of = [](std::size_t tc, const std::vector<double>& q) {
160 std::string k = std::to_string(tc);
161 for (std::size_t i = 0; i < q.size(); ++i) k += "," + std::to_string(q[i]);
162 return k;
163 };
164
165 for (std::size_t i = 0; i < samples.size(); ++i) {
166 const std::string k = key_of(samples[i].cls, samples[i].ql);
167 if (keyIndex.count(k)) continue;
168
169 const std::size_t tc = samples[i].cls;
170 // The tagged job is MOVED out of its own class into the extra one, so
171 // the total population is unchanged and the queue state observed on
172 // arrival is exactly what the augmented chain starts from.
173 std::vector<double> N(newR, 0.0);
174 for (std::size_t r = 0; r < R; ++r) N[r] = samples[i].ql[r];
175 N[tc - 1] -= 1.0;
176 N[newR - 1] = 1.0;
177 if (N[tc - 1] < 0.0)
178 throw InputError(
179 "infer_mlps: a sample reports its own class empty on arrival, so the tagged job "
180 "cannot be moved into the auxiliary class");
181
182 qn::Network<double> m("mlps_aug");
183 const std::size_t d = m.add_delay("Think");
184 const std::size_t q = m.add_queue("Queue1", lang::SchedStrategy::PS);
185 m.set_number_of_servers(q, nCores);
186 std::vector<std::size_t> cls(newR, 0);
187 for (std::size_t r = 0; r < newR; ++r) {
188 cls[r] = m.add_closed_class("Class" + std::to_string(r + 1), N[r], d);
189 // The auxiliary class thinks at the tagged class's own rate.
190 const double mz = (r + 1 == newR) ? muZ[tc - 1] : muZ[r];
192 m.set_service(q, cls[r], lang::Distrib<double>::exp_rate(1.0)); // placeholder
193 }
195 for (std::size_t r = 0; r < newR; ++r) {
196 P.set(cls[r], cls[r], d, q, 1.0);
197 P.set(cls[r], cls[r], q, d, 1.0);
198 }
199 m.link(P);
200
201 MlpsPrebuilt pb(m);
202 pb.tagClass = tc;
203 pb.N = N;
204 pb.queueNode = q;
205 pb.delayNode = d;
206
211
212 // The transitions that ARE the tagged job departing the queue. They are
213 // invariant under a rate change, which is why they are cached.
214 for (std::size_t e = 0; e < gen.sync.size(); ++e)
215 if (gen.sync[e].active.node == q && gen.sync[e].active.cls == newR &&
216 gen.sync[e].active.event == lang::EventType::DEP)
217 pb.depSync.push_back(e);
218 if (pb.depSync.empty())
219 throw InputError(
220 "infer_mlps: the augmented chain has no departure of the auxiliary class at the "
221 "queue, so the sojourn has no absorbing event");
222
223 // The states in which the tagged job is AT the queue: those are the
224 // ones the sojourn runs over.
225 const std::size_t qst = asn.stations[0].name == "Queue1" ? 1 : 2;
226 const std::size_t taggedCol = (qst - 1) * newR + (newR - 1);
227 for (std::size_t s = 0; s < aggr.rows(); ++s)
228 if (std::fabs(aggr(s, taggedCol) - 1.0) < 1e-9) pb.subset.push_back(s);
229 if (pb.subset.empty())
230 throw InputError("infer_mlps: no state has the tagged job at the queue");
231
232 pb.SSqueue = Matrix<double>(pb.subset.size(), newR, 0.0);
233 for (std::size_t s = 0; s < pb.subset.size(); ++s)
234 for (std::size_t r = 0; r < newR; ++r)
235 pb.SSqueue(s, r) = aggr(pb.subset[s], (qst - 1) * newR + r);
236
237 keyIndex[k] = pre.size();
238 keys.push_back(k);
239 pre.push_back(pb);
240 }
241
242 // ---- the negative log-likelihood ------------------------------------
243 const double TOL = 1e-6;
244 auto objective = [&](const std::vector<double>& x) {
245 // The demands are means; the model carries rates.
246 std::vector<double> rates(R, 0.0);
247 for (std::size_t r = 0; r < R; ++r)
248 rates[r] = (x[r] > 0.0) ? 1.0 / x[r] : std::numeric_limits<double>::infinity();
249 for (std::size_t r = 0; r < R; ++r)
250 if (!std::isfinite(rates[r])) return std::numeric_limits<double>::infinity();
251
252 std::vector<Matrix<double>> A(pre.size());
253 for (std::size_t p = 0; p < pre.size(); ++p) {
254 qn::Network<double> m = pre[p].model;
255 for (std::size_t r = 0; r < newR; ++r) {
256 const double rate = (r + 1 == newR) ? rates[pre[p].tagClass - 1] : rates[r];
257 m.set_service(pre[p].queueNode, r + 1, lang::Distrib<double>::exp_rate(rate));
258 }
262
263 // Q minus the tagged departures is the sub-generator of the sojourn.
264 Matrix<double> Q = gen.Q;
265 for (std::size_t di = 0; di < pre[p].depSync.size(); ++di) {
266 const Matrix<double>& F = gen.filt[pre[p].depSync[di]];
267 for (std::size_t i = 0; i < Q.rows(); ++i)
268 for (std::size_t j = 0; j < Q.cols(); ++j) Q(i, j) -= F(i, j);
269 }
270 const std::size_t ns = pre[p].subset.size();
271 Matrix<double> S(ns, ns, 0.0);
272 for (std::size_t i = 0; i < ns; ++i)
273 for (std::size_t j = 0; j < ns; ++j) S(i, j) = Q(pre[p].subset[i], pre[p].subset[j]);
274 A[p] = S;
275 }
276
277 double f = 0.0;
278 for (std::size_t i = 0; i < samples.size(); ++i) {
279 const std::size_t p = keyIndex.find(key_of(samples[i].cls, samples[i].ql))->second;
280 const Matrix<double>& S = A[p];
281 const std::size_t ns = S.rows();
282 // The chain starts in the state the sample OBSERVED.
283 const std::size_t idx = match_row(pre[p].SSqueue, pre[p].N);
284 std::vector<double> pie(ns, 0.0);
285 if (idx != static_cast<std::size_t>(-1)) pie[idx] = 1.0;
286 // The absorbing MAP: D0 = S, D1 = (-S 1) pie, i.e. every absorption
287 // restarts in the observed state, which makes the density of the
288 // first passage the phase-type density this needs.
290 mp.D0 = S;
291 mp.D1 = Matrix<double>(ns, ns, 0.0);
292 for (std::size_t a = 0; a < ns; ++a) {
293 double row = 0.0;
294 for (std::size_t b = 0; b < ns; ++b) row += S(a, b);
295 for (std::size_t b = 0; b < ns; ++b) mp.D1(a, b) = -row * pie[b];
296 }
297 std::vector<double> at(1, samples[i].rt);
298 const double like = mam::map_pdf(mp, at)[0];
299 f -= std::log(TOL + std::max(0.0, like));
300 }
301 return f;
302 };
303
304 std::vector<Bound<double>> bounds(R);
305 for (std::size_t r = 0; r < R; ++r) {
306 bounds[r].has_lo = true;
307 bounds[r].has_hi = true;
308 bounds[r].lo = lo[r];
309 bounds[r].hi = hi[r];
310 }
311 const NelderMeadResult<double> res = nelder_mead_box(objective, x0, bounds);
312 return res.x;
313}
314
315/**
316 * MINPS: run MLPS and RPS and keep whichever gives the smaller mean demand.
317 *
318 * The reference's rule verbatim. It is a selection, not a blend: taking the
319 * elementwise minimum instead would mix two estimators' class assignments and
320 * report a demand vector neither of them produced.
321 */
322inline std::vector<double> infer_minps(const std::vector<double>& muZ, double nCores,
323 const std::vector<MlpsSample>& samples) {
324 const std::vector<double> mlps = infer_mlps(muZ, nCores, samples);
325
326 std::vector<double> rt;
327 std::vector<std::size_t> cls;
328 Matrix<double> ql(samples.size(), muZ.size(), 0.0);
329 for (std::size_t i = 0; i < samples.size(); ++i) {
330 rt.push_back(samples[i].rt);
331 // `infer_rps` indexes its classes from ZERO while an MlpsSample carries
332 // the reference's 1-based label; passing the label through makes every
333 // class below the maximum look empty and the estimator refuses.
334 cls.push_back(samples[i].cls - 1);
335 for (std::size_t r = 0; r < muZ.size(); ++r) ql(i, r) = samples[i].ql[r];
336 }
337 const std::vector<double> rps = infer::infer_rps<double>(rt, cls, ql, static_cast<long>(nCores));
338
339 double ma = 0.0, mb = 0.0;
340 for (std::size_t r = 0; r < mlps.size(); ++r) ma += mlps[r];
341 for (std::size_t r = 0; r < rps.size(); ++r) mb += rps[r];
342 if (!mlps.empty()) ma /= static_cast<double>(mlps.size());
343 if (!rps.empty()) mb /= static_cast<double>(rps.size());
344 return (ma < mb) ? mlps : rps;
345}
346
347} // namespace api
348} // namespace line
349
350#endif // LINE_API_INFER_INFER_MLPS_H
InputError(const std::string &what)
Definition error.h:39
std::size_t cols() const
Definition matrix.h:90
std::size_t rows() const
Definition matrix.h:89
A network plus its refreshed NetworkStruct.
std::vector< Station< T > > stations
stations[k-1] is the k-th station
A queueing network under construction.
std::size_t add_delay(const std::string &nm)
An infinite-server station (a Delay, MATLAB's Delay / DelayStation).
void set_number_of_servers(std::size_t node, double n)
queue.setNumberOfServers(n).
std::size_t add_queue(const std::string &nm, SchedStrategy sched=SchedStrategy::FCFS)
A queueing station.
std::size_t add_closed_class(const std::string &nm, double njobs, std::size_t refstat_node, int prio=0)
A closed class of the given population, referencing a station node.
const NetworkStruct< T > & get_struct()
The refreshed struct, MATLAB's model.getStruct().
void link(const RoutingMatrix< T > &Pm)
model.link(P): install the routing.
void set_service(std::size_t node, std::size_t cls, const Distrib< T > &d)
station.setService(class, dist).
The routing matrix a model script fills in, MATLAB's P cell array.
void set(std::size_t r, std::size_t s, std::size_t i, std::size_t j, const T &p)
The exception types the port throws.
Regression for Processor Sharing (RPS) demand estimator.
Probability density of the inter-arrival time of a MAP.
Dense matrix and non-owning view.
std::vector< double > infer_minps(const std::vector< double > &muZ, double nCores, const std::vector< MlpsSample > &samples)
MINPS: run MLPS and RPS and keep whichever gives the smaller mean demand.
Definition infer_mlps.h:322
std::vector< double > infer_mlps(const std::vector< double > &muZ, double nCores, const std::vector< MlpsSample > &samples)
MLPS demand estimation at a PS queue.
Definition infer_mlps.h:117
CtmcGenerator< T > ctmc_get_generator(const NetworkStruct< T > &sn, const CtmcSolution< T > &d)
Port of @@SolverCTMC/getGenerator.m: the generator, its event filtration and the synchronization list...
Matrix< T > ctmc_get_state_space_aggr(const NetworkStruct< T > &sn, const CtmcOptions &opt)
Port of @@SolverCTMC/getStateSpaceAggr.m: the per-(station, class) job counts of every state,...
std::vector< T > infer_rps(const std::vector< T > &rt, const std::vector< std::size_t > &cls, const Matrix< T > &ql, long V)
Regression for Processor Sharing (RPS) demand estimator.
Definition infer_rps.h:57
@ DEP
a job departs
Definition lang_types.h:115
std::vector< T > map_pdf(const Map< T > &m, const std::vector< T > &tset)
Probability density of the inter-arrival time at the given points.
Definition map_pdf.h:43
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
Derivative-free simplex minimization (Nelder and Mead, 1965), with optional box bounds imposed by a c...
The Network constructor API: Queue, Delay, Source, Sink, Router, ClassSwitch, Cache,...
A queueing network and its refreshed NetworkStruct.
Number-type abstraction for the templated API port.
The remaining @@SolverCTMC accessors: getGenerator / getInfGen, getStateSpace / getStateSpaceAggr and...
Outcome of a simplex minimization.
Definition neldermead.h:96
std::vector< T > x
best point found
Definition neldermead.h:97
One observation: a response time, the tagged class, and the arrival state.
Definition infer_mlps.h:71
std::vector< double > ql
per-class queue length seen on arrival
Definition infer_mlps.h:74
std::size_t cls
1-based class of the tagged job
Definition infer_mlps.h:73
[infGen, eventFilt, ev] of @@SolverCTMC/getGenerator.m.
std::vector< Matrix< T > > filt
eventFilt: filt[a] holds only what synchronization sync[a] contributed, so sum_a filt[a] is the off-d...
std::vector< Sync< T > > sync
ev, the reference's sn.sync
Matrix< T > Q
the infinitesimal generator
The SolverCTMC knobs this port honours.
static Distrib exp_rate(const T &r)
Definition lang_types.h:814
A MAP as the pair of matrices (D0, D1).
Definition map_moment.h:53
Matrix< T > D1
Definition map_moment.h:55
Matrix< T > D0
Definition map_moment.h:54