LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
param_estimator.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_INFERENCE_PARAM_ESTIMATOR_H
6#define LINE_INFERENCE_PARAM_ESTIMATOR_H
7
8/**
9 * @file
10 * @ingroup line_inference
11 * Sample collection and estimator orchestration for queueing-network parameters.
12 *
13 * This is the workflow contract of MATLAB `ParamEstimator`: sampled metrics are
14 * stored by 1-based node and class, aggregate metrics have their own node row,
15 * interpolation aligns every series on the union of timestamps, `auto_method`
16 * applies the reference precedence, and `estimate_at` writes inferred means back
17 * into the model. Estimators are typed callables; all eleven MATLAB methods are
18 * installed by default.
19 */
20
21#include <algorithm>
22#include <cmath>
23#include <cstddef>
24#include <cstdint>
25#include <functional>
26#include <limits>
27#include <map>
28#include <set>
29#include <string>
30#include <utility>
31#include <vector>
32
46#include "line/util/error.h"
47#include "line/util/lu.h"
48#include "line/util/matrix.h"
50
51namespace line {
52namespace infer {
53
55 int verbose = 1;
56 std::string method = "ubr";
57 std::string variant = "default";
58 std::size_t iter_max = 1000;
59 double tol = 1e-3;
60 std::size_t open_population = 100;
61 std::uint64_t random_seed = 5489u;
62 std::vector<double> x0;
64
65 /** Options of the variational estimator ("vi"). */
67 /** Probability that a queue-length reading is faulty ("vi"). */
68 double epsilon = 0.05;
69 /** Shape of the Gamma prior placed on each estimated rate ("vi"). */
70 double prior_shape = 1.0;
71 /** Posterior Gamma parameters left behind by the variational estimator. */
72 std::vector<double> posterior_alpha, posterior_beta;
73 /** Evidence lower bound per iteration, left behind by the same. */
74 std::vector<double> bound;
75};
76
77namespace detail {
78
79inline std::vector<double> spline_not_a_knot(const std::vector<double>& x,
80 const std::vector<double>& y,
81 const std::vector<double>& xq) {
82 const std::size_t n = x.size();
83 if (n != y.size())
84 throw InputError("ParamEstimator.interpolate: sample times and data disagree in length");
85 if (n < 2)
86 throw InputError("ParamEstimator.interpolate: spline interpolation needs at least two samples");
87 for (std::size_t i = 1; i < n; ++i)
88 if (!(x[i] > x[i - 1]))
89 throw InputError(
90 "ParamEstimator.interpolate: sample timestamps must be strictly increasing");
91
92 std::vector<double> out(xq.size(), 0.0);
93 if (n == 2) {
94 const double slope = (y[1] - y[0]) / (x[1] - x[0]);
95 for (std::size_t q = 0; q < xq.size(); ++q) out[q] = y[0] + slope * (xq[q] - x[0]);
96 return out;
97 }
98 if (n == 3) {
99 const double d01 = (y[1] - y[0]) / (x[1] - x[0]);
100 const double d12 = (y[2] - y[1]) / (x[2] - x[1]);
101 const double d012 = (d12 - d01) / (x[2] - x[0]);
102 for (std::size_t q = 0; q < xq.size(); ++q) {
103 const double z = xq[q];
104 out[q] = y[0] + d01 * (z - x[0]) + d012 * (z - x[0]) * (z - x[1]);
105 }
106 return out;
107 }
108
109 std::vector<double> h(n - 1, 0.0), delta(n - 1, 0.0);
110 for (std::size_t i = 0; i + 1 < n; ++i) {
111 h[i] = x[i + 1] - x[i];
112 delta[i] = (y[i + 1] - y[i]) / h[i];
113 }
114
115 Matrix<double> A(n, n, 0.0);
116 std::vector<double> rhs(n, 0.0);
117 A(0, 0) = h[1];
118 A(0, 1) = -(h[0] + h[1]);
119 A(0, 2) = h[0];
120 for (std::size_t i = 1; i + 1 < n; ++i) {
121 A(i, i - 1) = h[i - 1];
122 A(i, i) = 2.0 * (h[i - 1] + h[i]);
123 A(i, i + 1) = h[i];
124 rhs[i] = 6.0 * (delta[i] - delta[i - 1]);
125 }
126 A(n - 1, n - 3) = h[n - 2];
127 A(n - 1, n - 2) = -(h[n - 3] + h[n - 2]);
128 A(n - 1, n - 1) = h[n - 3];
129 const std::vector<double> second = solve(A, rhs);
130
131 for (std::size_t q = 0; q < xq.size(); ++q) {
132 const double z = xq[q];
133 std::size_t i = 0;
134 if (z >= x[n - 1]) {
135 i = n - 2;
136 } else if (z > x[0]) {
137 i = static_cast<std::size_t>(std::upper_bound(x.begin(), x.end(), z) - x.begin() - 1);
138 }
139 const double s = z - x[i];
140 const double b = delta[i] - h[i] * (2.0 * second[i] + second[i + 1]) / 6.0;
141 const double c = second[i] / 2.0;
142 const double d = (second[i + 1] - second[i]) / (6.0 * h[i]);
143 out[q] = y[i] + s * (b + s * (c + s * d));
144 }
145 return out;
146}
147
148inline std::vector<double> least_squares_passive(const Matrix<double>& A,
149 const std::vector<double>& b,
150 std::vector<bool>& passive) {
151 std::vector<std::size_t> cols;
152 for (std::size_t j = 0; j < passive.size(); ++j)
153 if (passive[j]) cols.push_back(j);
154 std::vector<double> z(passive.size(), 0.0);
155 if (cols.empty()) return z;
156
157 // Modified Gram-Schmidt avoids squaring the condition number through the
158 // normal equations. Rank-deficient columns leave the passive set; this is
159 // the Lawson-Hanson active-set convention for a singular subproblem.
160 Matrix<double> r(cols.size(), cols.size(), 0.0);
161 std::vector<std::vector<double>> q;
162 std::vector<std::size_t> independent;
163 for (const std::size_t col : cols) {
164 std::vector<double> v(A.rows(), 0.0);
165 double column_norm2 = 0.0;
166 for (std::size_t i = 0; i < A.rows(); ++i) {
167 v[i] = A(i, col);
168 column_norm2 += v[i] * v[i];
169 }
170
171 std::vector<double> projection(q.size(), 0.0);
172 for (int pass = 0; pass < 2; ++pass) {
173 for (std::size_t k = 0; k < q.size(); ++k) {
174 double coefficient = 0.0;
175 for (std::size_t i = 0; i < A.rows(); ++i) coefficient += q[k][i] * v[i];
176 projection[k] += coefficient;
177 for (std::size_t i = 0; i < A.rows(); ++i) v[i] -= coefficient * q[k][i];
178 }
179 }
180
181 double residual_norm2 = 0.0;
182 for (const double value : v) residual_norm2 += value * value;
183 const double residual_norm = std::sqrt(residual_norm2);
184 const double rank_tol = 1e-12 * std::max(1.0, std::sqrt(column_norm2));
185 if (residual_norm <= rank_tol) {
186 passive[col] = false;
187 continue;
188 }
189
190 const std::size_t a = independent.size();
191 for (std::size_t k = 0; k < q.size(); ++k) r(k, a) = projection[k];
192 r(a, a) = residual_norm;
193 for (double& value : v) value /= residual_norm;
194 q.push_back(std::move(v));
195 independent.push_back(col);
196 }
197
198 std::vector<double> zp(independent.size(), 0.0);
199 for (std::size_t k = 0; k < q.size(); ++k)
200 for (std::size_t i = 0; i < A.rows(); ++i) zp[k] += q[k][i] * b[i];
201 for (std::size_t offset = 0; offset < independent.size(); ++offset) {
202 const std::size_t row = independent.size() - 1 - offset;
203 for (std::size_t col = row + 1; col < independent.size(); ++col)
204 zp[row] -= r(row, col) * zp[col];
205 zp[row] /= r(row, row);
206 }
207 for (std::size_t a = 0; a < independent.size(); ++a) z[independent[a]] = zp[a];
208 return z;
209}
210
211inline std::vector<double> nnls(const Matrix<double>& A, const std::vector<double>& b) {
212 if (A.rows() != b.size()) throw InputError("ParamEstimator.ubr: NNLS dimensions disagree");
213 const std::size_t n = A.cols();
214 std::vector<double> x(n, 0.0), w(n, 0.0);
215 std::vector<bool> passive(n, false);
216 const double eps = 1e-12;
217 const std::size_t limit = 30 * (n + 1) * (n + 1);
218
219 const auto gradient = [&]() {
220 std::vector<double> residual(b);
221 for (std::size_t i = 0; i < A.rows(); ++i)
222 for (std::size_t j = 0; j < n; ++j) residual[i] -= A(i, j) * x[j];
223 for (std::size_t j = 0; j < n; ++j) {
224 w[j] = 0.0;
225 for (std::size_t i = 0; i < A.rows(); ++i) w[j] += A(i, j) * residual[i];
226 }
227 };
228
229 gradient();
230 for (std::size_t outer = 0; outer < limit; ++outer) {
231 std::size_t enter = n;
232 double best = eps;
233 for (std::size_t j = 0; j < n; ++j)
234 if (!passive[j] && w[j] > best) {
235 best = w[j];
236 enter = j;
237 }
238 if (enter == n) return x;
239 passive[enter] = true;
240
241 for (std::size_t inner = 0; inner < limit; ++inner) {
242 const std::vector<bool> passive_before = passive;
243 const std::vector<double> z = least_squares_passive(A, b, passive);
244 for (std::size_t j = 0; j < n; ++j)
245 if (passive_before[j] && !passive[j]) x[j] = 0.0;
246 bool positive = true;
247 for (std::size_t j = 0; j < n; ++j)
248 if (passive[j] && z[j] <= eps) positive = false;
249 if (positive) {
250 x = z;
251 break;
252 }
253
254 double alpha = std::numeric_limits<double>::infinity();
255 for (std::size_t j = 0; j < n; ++j)
256 if (passive[j] && z[j] <= eps)
257 alpha = std::min(alpha, x[j] / (x[j] - z[j]));
258 for (std::size_t j = 0; j < n; ++j) x[j] += alpha * (z[j] - x[j]);
259 for (std::size_t j = 0; j < n; ++j)
260 if (passive[j] && x[j] <= eps) {
261 passive[j] = false;
262 x[j] = 0.0;
263 }
264 }
265 gradient();
266 }
267 throw NumericError(ErrorKind{"NonConvergence"}, "ParamEstimator.ubr: NNLS did not converge");
268}
269
270} // namespace detail
271
273 public:
274 using Samples = std::vector<std::vector<std::vector<SampledMetric>>>;
275 using AggregateSamples = std::vector<std::vector<SampledMetric>>;
276 using Estimator =
277 std::function<Matrix<double>(ParamEstimator&, const std::vector<std::size_t>&)>;
278
281 : options(options), model_(model) {
282 const qn::NetworkStruct<double>& sn = model_.raw_struct();
283 samples.resize(sn.nodes.size(),
284 std::vector<std::vector<SampledMetric>>(sn.classes.size()));
285 samples_aggr.resize(sn.nodes.size());
286 register_estimator("ubr", [](ParamEstimator& self, const std::vector<std::size_t>& nodes) {
287 return self.estimator_ubr(nodes);
288 });
289 register_estimator("qmle", [](ParamEstimator& self, const std::vector<std::size_t>& nodes) {
290 return self.estimator_qmle(nodes);
291 });
292 register_estimator("gibbs", [](ParamEstimator& self, const std::vector<std::size_t>& nodes) {
293 return self.estimator_gibbs(nodes);
294 });
295 register_estimator("mlps", [](ParamEstimator& self, const std::vector<std::size_t>& nodes) {
296 return self.estimator_mlps(nodes);
297 });
298 register_estimator("fmlps", [](ParamEstimator& self, const std::vector<std::size_t>& nodes) {
299 return self.estimator_fmlps(nodes);
300 });
301 register_estimator("ubo", [](ParamEstimator& self, const std::vector<std::size_t>& nodes) {
302 return self.estimator_ubo(nodes);
303 });
304 register_estimator("erps", [](ParamEstimator& self, const std::vector<std::size_t>& nodes) {
305 return self.estimator_erps(nodes);
306 });
307 register_estimator("ekf", [](ParamEstimator& self, const std::vector<std::size_t>& nodes) {
308 return self.estimator_ekf(nodes);
309 });
310 register_estimator("mcmc", [](ParamEstimator& self, const std::vector<std::size_t>& nodes) {
311 return self.estimator_mcmc(nodes);
312 });
313 register_estimator("mle", [](ParamEstimator& self, const std::vector<std::size_t>& nodes) {
314 return self.estimator_mle(nodes);
315 });
316 register_estimator("vi", [](ParamEstimator& self, const std::vector<std::size_t>& nodes) {
317 return self.estimator_variational(nodes);
318 });
319 }
320
324
325 void add_samples(const SampledMetric& sample) {
326 require_node(sample.node, "add_samples");
327 if (sample.is_aggregate()) {
328 samples_aggr[sample.node - 1].push_back(sample);
329 } else {
330 require_class(sample.jobclass, "add_samples");
331 samples[sample.node - 1][sample.jobclass - 1].push_back(sample);
332 }
333 }
334
335 Samples& get_data() { return samples; }
336 const Samples& get_data() const { return samples; }
338 const AggregateSamples& get_data_aggr() const { return samples_aggr; }
339
340 SampledMetric* get_arvr(std::size_t node, std::size_t jobclass) {
341 return first_metric(node, jobclass, lang::MetricType::ArvR);
342 }
343 SampledMetric* get_util(std::size_t node, std::size_t jobclass) {
344 return first_metric(node, jobclass, lang::MetricType::Util);
345 }
346 SampledMetric* get_respt(std::size_t node, std::size_t jobclass) {
347 return first_metric(node, jobclass, lang::MetricType::RespT);
348 }
349 SampledMetric* get_tput(std::size_t node, std::size_t jobclass) {
350 return first_metric(node, jobclass, lang::MetricType::Tput);
351 }
352 SampledMetric* get_aggr_util(std::size_t node) {
353 return first_aggr_metric(node, lang::MetricType::Util, nullptr);
354 }
355 SampledMetric* get_aggr_qlen(std::size_t node, const ConditionEvent* event = nullptr) {
356 return first_aggr_metric(node, lang::MetricType::QLen, event);
357 }
358
359 std::vector<SampledMetric*> get_qlen(std::size_t node, std::size_t jobclass,
360 const ConditionEvent* event = nullptr) {
361 require_node(node, "get_qlen");
362 require_class(jobclass, "get_qlen");
363 std::vector<SampledMetric*> out;
364 std::vector<SampledMetric>& cell = samples[node - 1][jobclass - 1];
365 for (SampledMetric& sample : cell) {
366 if (sample.type != lang::MetricType::QLen) continue;
367 if (event != nullptr && (!sample.cond.has_value() || *sample.cond != *event)) continue;
368 out.push_back(&sample);
369 if (event != nullptr) break;
370 }
371 return out;
372 }
373
374 std::string auto_method() {
375 bool has_arvr = false, has_respt = false, has_util = false;
376 bool has_qlen = false, has_tput = false, has_trace = false;
377 bool has_aggr_util = false, has_aggr_qlen = false;
378 for (const auto& node : samples)
379 for (const auto& cls : node)
380 for (const SampledMetric& sample : cls) {
381 if (sample.type == lang::MetricType::ArvR) has_arvr = true;
382 if (sample.type == lang::MetricType::RespT) has_respt = true;
383 if (sample.type == lang::MetricType::Util) has_util = true;
384 if (sample.type == lang::MetricType::QLen) has_qlen = true;
385 if (sample.type == lang::MetricType::Tput) has_tput = true;
386 if (sample.is_trace()) has_trace = true;
387 }
388 for (const auto& node : samples_aggr)
389 for (const SampledMetric& sample : node) {
390 if (sample.type == lang::MetricType::Util) has_aggr_util = true;
391 if (sample.type == lang::MetricType::QLen) has_aggr_qlen = true;
392 }
393 (void)has_tput;
394
395 if (has_trace && has_respt && has_aggr_qlen)
396 options.method = "erps";
397 else if (has_trace && has_respt && has_arvr)
398 options.method = "mlps";
399 else if (has_arvr && has_respt && (has_util || has_aggr_util))
400 options.method = "ubo";
401 else if (has_arvr && (has_util || has_aggr_util))
402 options.method = "ubr";
403 else if (has_qlen)
404 options.method = "qmle";
405 else
406 throw InputError(
407 "ParamEstimator.auto_method: insufficient data to select an estimation method");
408 return options.method;
409 }
410
411 void interpolate() {
412 std::set<double> union_set;
413 for_each_sample([&](SampledMetric& sample) {
414 union_set.insert(sample.t.begin(), sample.t.end());
415 });
416 if (union_set.empty()) return;
417 const std::vector<double> tunion(union_set.begin(), union_set.end());
418 for_each_sample([&](SampledMetric& sample) {
419 sample.data = detail::spline_not_a_knot(sample.t, sample.data, tunion);
420 sample.t = tunion;
421 });
422 }
423
424 void register_estimator(const std::string& method, const Estimator& estimator) {
425 estimators_[method] = estimator;
426 }
427
428 bool has_estimator(const std::string& method) const {
429 return estimators_.find(method) != estimators_.end();
430 }
431
432 Matrix<double> estimate_at(const std::vector<std::size_t>& nodes) {
433 const auto it = estimators_.find(options.method);
434 if (it == estimators_.end())
435 throw UnsupportedError(ErrorKind{"Method"}, "ParamEstimator: estimator '" + options.method +
436 "' has no C++ adapter");
437 Matrix<double> values = it->second(*this, nodes);
438 const std::size_t classes = model_.raw_struct().classes.size();
439 if (values.rows() != nodes.size() || values.cols() != classes)
440 throw InputError(
441 "ParamEstimator.estimate_at: estimator result must be nodes by classes");
442
443 for (std::size_t n = 0; n < nodes.size(); ++n) {
444 const std::size_t node = nodes[n];
445 require_node(node, "estimate_at");
446 qn::NetworkStruct<double>& sn = model_.raw_struct();
447 const std::size_t station = sn.nodes[node - 1].station;
448 if (station == 0)
449 throw InputError("ParamEstimator.estimate_at: node '" +
450 sn.nodes[node - 1].name + "' is not a station");
451 if (sn.stations[station - 1].nodetype == lang::NodeType::Source) continue;
452 for (std::size_t r = 0; r < classes; ++r) {
453 const double target = values(n, r);
454 if (!std::isfinite(target) || !(target > 0.0)) continue;
455 const lang::Distrib<double> current = sn.service[station - 1][r];
456 if (current.disabled)
457 throw InputError("ParamEstimator.estimate_at: node '" +
458 sn.nodes[node - 1].name + "' has no class " +
459 std::to_string(r + 1) + " service process");
460 if (current.type == lang::ProcessType::IMMEDIATE ||
461 !std::isfinite(current.mean)) {
462 model_.set_service(node, r + 1, lang::Distrib<double>::exp_mean(target));
463 } else {
464 switch (current.type) {
474 break;
475 default:
476 throw UnsupportedError(ErrorKind{"Method"},
477 "ParamEstimator.estimate_at: " +
478 std::string(lang::process_to_text(current.type)) +
479 " has no MATLAB setMean contract");
480 }
481 const double factor = current.mean / target;
482 model_.set_service(node, r + 1, lang::dist_scale_rate(current, factor));
483 }
484 }
485 }
486 return values;
487 }
488
490
491 static std::string get_required_metrics(const std::string& method) {
492 static const std::map<std::string, std::string> descriptions = {
493 {"ubr", "ArvR (per-class) + Util (per-class or aggregate)"},
494 {"ubo", "ArvR (per-class) + RespT (per-class) + Util (aggregate)"},
495 {"erps",
496 "RespT (per-class) + QLen (aggregate, conditional on class arrivals). PS "
497 "stations only."},
498 {"ekf", "RespT (per-class) + Util (aggregate). Sequential/recursive estimation."},
499 {"mcmc", "QLen (per-class, or aggregate shared equally by the classes). Gibbs "
500 "sampling with MCMC. Open/mixed via closed equivalence."},
501 {"mle", "ArvR (per-class) + RespT (per-class) + Util (aggregate)"},
502 {"vi", "QLen (per-class, timeseries) at every station. Variational "
503 "inference over transition counts; noisy readings, Gamma posteriors."},
504 {"mlps", "ArvR (per-class, trace) + RespT (per-class, trace). PS stations only. "
505 "Open/mixed via closed equivalence."},
506 {"fmlps", "ArvR (per-class, trace) + RespT (per-class, trace). PS stations only. "
507 "Open/mixed via closed equivalence."},
508 {"qmle", "QLen (per-class). Open/mixed via closed equivalence (Z_r = N_r / "
509 "lambda_r)."},
510 {"gibbs", "ArvR (per-class, trace) + RespT (per-class, trace) + Tput "
511 "(per-class). Gibbs sampling."}};
512 const auto it = descriptions.find(method);
513 return it == descriptions.end() ? "Unknown method: " + method : it->second;
514 }
515
516 private:
517 qn::Network<double>& model_;
518 std::map<std::string, Estimator> estimators_;
519
520 void require_node(std::size_t node, const char* method) const {
521 if (node == 0 || node > samples.size())
522 throw InputError(std::string("ParamEstimator.") + method + ": node index is out of range");
523 }
524
525 void require_class(std::size_t jobclass, const char* method) const {
526 const std::size_t classes = samples.empty() ? model_.raw_struct().classes.size()
527 : samples[0].size();
528 if (jobclass == 0 || jobclass > classes)
529 throw InputError(std::string("ParamEstimator.") + method +
530 ": class index is out of range");
531 }
532
533 SampledMetric* first_metric(std::size_t node, std::size_t jobclass,
534 lang::MetricType type) {
535 require_node(node, "metric lookup");
536 require_class(jobclass, "metric lookup");
537 for (SampledMetric& sample : samples[node - 1][jobclass - 1])
538 if (sample.type == type) return &sample;
539 return nullptr;
540 }
541
542 SampledMetric* first_aggr_metric(std::size_t node, lang::MetricType type,
543 const ConditionEvent* event) {
544 require_node(node, "aggregate metric lookup");
545 for (SampledMetric& sample : samples_aggr[node - 1]) {
546 if (sample.type != type) continue;
547 if (event != nullptr && (!sample.cond.has_value() || *sample.cond != *event)) continue;
548 return &sample;
549 }
550 return nullptr;
551 }
552
553 template <class F>
554 void for_each_sample(F fn) {
555 for (auto& node : samples)
556 for (auto& cls : node)
557 for (SampledMetric& sample : cls) fn(sample);
558 for (auto& node : samples_aggr)
559 for (SampledMetric& sample : node) fn(sample);
560 }
561
562 void require_single_station(const std::vector<std::size_t>& nodes, const char* method,
563 std::size_t& node, std::size_t& station) const {
564 if (nodes.size() != 1)
565 throw InputError(std::string("ParamEstimator.") + method +
566 ": the estimator accepts exactly one station");
567 node = nodes[0];
568 require_node(node, method);
569 const qn::NetworkStruct<double>& sn = model_.raw_struct();
570 station = sn.nodes[node - 1].station;
571 if (station == 0)
572 throw InputError(std::string("ParamEstimator.") + method +
573 ": the target node is not a station");
574 }
575
576 void effective_population_and_think(std::vector<double>& population,
577 std::vector<double>& think) {
578 const qn::NetworkStruct<double>& sn = model_.get_struct();
579 const std::size_t classes = sn.classes.size();
580 population.assign(classes, 0.0);
581 think.assign(classes, 0.0);
582 for (std::size_t r = 0; r < classes; ++r)
583 population[r] = std::isfinite(sn.classes[r].population)
584 ? sn.classes[r].population
585 : static_cast<double>(options.open_population);
586
587 for (const qn::NodeDef& node : sn.nodes) {
588 if (node.station == 0) continue;
589 if (node.nodetype == lang::NodeType::Delay) {
590 for (std::size_t r = 0; r < classes; ++r)
591 if (std::isfinite(sn.classes[r].population) &&
592 !sn.service[node.station - 1][r].disabled)
593 think[r] += sn.service[node.station - 1][r].mean;
594 } else if (node.nodetype == lang::NodeType::Source) {
595 for (std::size_t r = 0; r < classes; ++r)
596 if (!std::isfinite(sn.classes[r].population)) {
597 const lang::Distrib<double>& arrival = sn.service[node.station - 1][r];
598 if (arrival.disabled || !(arrival.mean > 0.0) ||
599 !std::isfinite(arrival.mean))
600 throw InputError("ParamEstimator: an open class has no finite source "
601 "interarrival mean");
602 think[r] = population[r] * arrival.mean;
603 }
604 }
605 }
606 for (std::size_t r = 0; r < classes; ++r)
607 if (!(think[r] > 0.0) || !std::isfinite(think[r]))
608 throw InputError("ParamEstimator: class " + std::to_string(r + 1) +
609 " has no positive finite think time or open equivalent");
610 }
611
612 qn::Network<double> build_closed_equivalent(std::size_t node,
613 std::size_t& equivalent_queue) {
614 std::vector<double> population, think;
615 effective_population_and_think(population, think);
616 const qn::NetworkStruct<double>& sn = model_.get_struct();
617 const std::size_t station = sn.nodes[node - 1].station;
618 qn::Network<double> equivalent("closed_equiv");
619 const std::size_t delay = equivalent.add_delay("Think");
620 equivalent_queue = equivalent.add_queue("Queue1", lang::SchedStrategy::PS);
621 equivalent.set_number_of_servers(equivalent_queue, sn.stations[station - 1].nservers);
622 std::vector<std::size_t> classes(population.size(), 0);
623 for (std::size_t r = 0; r < population.size(); ++r) {
624 classes[r] = equivalent.add_closed_class("Class" + std::to_string(r + 1),
625 population[r], delay);
626 equivalent.set_service(delay, classes[r],
627 lang::Distrib<double>::exp_rate(1.0 / think[r]));
628 equivalent.set_service(equivalent_queue, classes[r], sn.service[station - 1][r]);
629 }
630 qn::RoutingMatrix<double> routing;
631 for (std::size_t r = 0; r < classes.size(); ++r) {
632 routing.set(classes[r], classes[r], delay, equivalent_queue, 1.0);
633 routing.set(classes[r], classes[r], equivalent_queue, delay, 1.0);
634 }
635 equivalent.link(routing);
636 return equivalent;
637 }
638
639 std::vector<api::MlpsSample> mlps_samples(std::size_t node, const char* method) {
640 const std::size_t classes = model_.raw_struct().classes.size();
641 std::vector<double> arrivals, response;
642 std::vector<std::size_t> labels;
643 for (std::size_t r = 0; r < classes; ++r) {
644 SampledMetric* arvr = get_arvr(node, r + 1);
645 SampledMetric* respt = get_respt(node, r + 1);
646 if (arvr == nullptr || respt == nullptr)
647 throw InputError(std::string("ParamEstimator.") + method +
648 ": arrival and response-time traces are required for class " +
649 std::to_string(r + 1));
650 if (!arvr->is_trace() || !respt->is_trace())
651 throw InputError(std::string("ParamEstimator.") + method +
652 ": arrival and response-time metrics must use trace format");
653 if (arvr->data.size() != respt->data.size())
654 throw InputError(std::string("ParamEstimator.") + method +
655 ": arrival and response-time traces have different lengths");
656 arrivals.insert(arrivals.end(), arvr->data.begin(), arvr->data.end());
657 response.insert(response.end(), respt->data.begin(), respt->data.end());
658 labels.insert(labels.end(), arvr->data.size(), r);
659 }
660 std::vector<long> ids(arrivals.size(), 0);
661 for (std::size_t i = 0; i < ids.size(); ++i) ids[i] = static_cast<long>(i + 1);
662 const Matrix<double> qlen =
663 infer_compute_ql_at_arrival(arrivals, ids, response, ids, labels, classes);
664 std::vector<std::size_t> order(arrivals.size(), 0);
665 for (std::size_t i = 0; i < order.size(); ++i) order[i] = i;
666 std::stable_sort(order.begin(), order.end(),
667 [&](std::size_t a, std::size_t b) { return arrivals[a] < arrivals[b]; });
668
669 std::vector<api::MlpsSample> out;
670 for (const std::size_t i : order) {
671 if (!(response[i] > 0.0)) continue;
672 api::MlpsSample sample;
673 sample.rt = response[i];
674 sample.cls = labels[i] + 1;
675 sample.ql.assign(classes, 0.0);
676 for (std::size_t r = 0; r < classes; ++r) sample.ql[r] = qlen(i, r);
677 out.push_back(sample);
678 }
679 if (out.empty())
680 throw InputError(std::string("ParamEstimator.") + method +
681 ": no positive response-time observations remain");
682 return out;
683 }
684
685 /**
686 * Variational inference for Markovian queueing networks (Perez-Casale, AAP
687 * 53(3), 2021). The network is translated into the transition set
688 * eta=(i,j,c) of the paper, with lambda_eta = mu_{i,c} p^c_{i,j}; routing
689 * probabilities are taken as known from the model and only the station
690 * rates of the requested nodes are estimated. The data are QLen
691 * timeseries, one per (node, class), read as exact with probability
692 * 1-epsilon and uniform over the remaining feasible values otherwise.
693 */
694 Matrix<double> estimator_variational(const std::vector<std::size_t>& nodes) {
695 if (nodes.empty()) throw InputError("ParamEstimator.vi: no stations were requested");
696 const qn::NetworkStruct<double>& sn = model_.get_struct();
697 const std::size_t M = sn.nstations, R = sn.nclasses, MR = M * R;
698
699 // station-to-station routing; the pseudo-closed sink-to-source
700 // feedback is not a job transition
701 const Matrix<double> rtst = api::sn_rt_stations(sn).rtst;
702 std::vector<int> sched(M, 1);
703 std::vector<bool> is_source(M, false);
704 for (std::size_t i = 0; i < M; ++i) {
705 const lang::SchedStrategy ss = sn.stations[i].sched;
706 if (ss == lang::SchedStrategy::INF) {
707 sched[i] = 0;
708 } else if (ss == lang::SchedStrategy::EXT) {
709 sched[i] = 2;
710 is_source[i] = true;
711 } else if (ss == lang::SchedStrategy::PS || ss == lang::SchedStrategy::FCFS ||
714 sched[i] = 1;
715 } else {
716 throw UnsupportedError(ErrorKind{"SchedStrategy"}, "ParamEstimator.vi: unsupported scheduling at station " +
717 std::to_string(i + 1));
718 }
719 }
720
721 infer::VariationalSpec<double> spec;
722 std::vector<double> probs;
723 for (std::size_t c = 0; c < R; ++c) {
724 for (std::size_t i = 0; i < M; ++i) {
725 for (std::size_t j = 0; j < M; ++j) {
726 if (i == j || is_source[j]) continue;
727 const double p = rtst(i * R + c, j * R + c);
728 if (!(p > 0)) continue;
729 spec.arcs.push_back({{i + 1, j + 1, c + 1}});
730 probs.push_back(p);
731 }
732 }
733 }
734 if (spec.arcs.empty())
735 throw InputError("ParamEstimator.vi: the model has no job transitions to infer from");
736
737 // which station-class rates are being estimated
738 std::vector<std::size_t> estimated(MR, 0), node_station(nodes.size(), 0);
739 std::size_t P = 0;
740 for (std::size_t n = 0; n < nodes.size(); ++n) {
741 require_node(nodes[n], "vi");
742 std::size_t st = 0;
743 for (std::size_t i = 1; i <= M; ++i)
744 if (sn.node_of_station(i) == nodes[n]) st = i;
745 if (st == 0) throw InputError("ParamEstimator.vi: node " + std::to_string(nodes[n]) +
746 " is not a station");
747 node_station[n] = st - 1;
748 for (std::size_t r = 0; r < R; ++r) {
749 const double rate = sn.rates(st - 1, r);
750 if (rate > 0 && std::isfinite(rate)) estimated[r * M + st - 1] = ++P;
751 }
752 }
753 if (P == 0)
754 throw InputError("ParamEstimator.vi: no station-class pair with a positive rate; the Gamma priors are "
755 "centred on the current rates, so give each unknown service an initial guess instead of NaN");
756
757 const std::size_t narcs = spec.arcs.size();
758 spec.routeprob = probs;
759 spec.arcparam.assign(narcs, 0);
760 spec.arcrate.assign(narcs, 0.0);
761 for (std::size_t e = 0; e < narcs; ++e) {
762 const std::size_t i = spec.arcs[e][0] - 1, c = spec.arcs[e][2] - 1;
763 const std::size_t p = estimated[c * M + i];
764 if (p > 0) {
765 spec.arcparam[e] = p;
766 } else {
767 spec.arcrate[e] = sn.rates(i, c);
768 if (!(spec.arcrate[e] > 0) || !std::isfinite(spec.arcrate[e]))
769 throw InputError("ParamEstimator.vi: station " + std::to_string(i + 1) +
770 " class " + std::to_string(c + 1) +
771 " has no usable rate to hold fixed");
772 }
773 }
774 spec.sched = sched;
775 spec.nservers.assign(M, 1.0);
776 for (std::size_t i = 0; i < M; ++i) {
777 const double k = sn.stations[i].nservers;
778 spec.nservers[i] = (std::isfinite(k) && k > 0) ? k : 1.0;
779 }
780
781 // observations: QLen timeseries, one column per (station, class) pair
782 std::set<double> tset;
783 std::map<std::size_t, std::pair<std::vector<double>, std::vector<double>>> series;
784 for (std::size_t i = 1; i <= M; ++i) {
785 const std::size_t nd = sn.node_of_station(i);
786 if (nd == 0) continue;
787 for (std::size_t r = 0; r < R; ++r) {
788 const std::vector<SampledMetric*> data = get_qlen(nd, r + 1);
789 if (data.empty() || data[0]->t.empty()) continue;
790 series[r * M + i - 1] = std::make_pair(data[0]->t, data[0]->data);
791 for (const double tv : data[0]->t) tset.insert(tv);
792 }
793 }
794 if (series.empty())
795 throw InputError("ParamEstimator.vi: queue-length timeseries data is missing");
796 spec.obsTimes.assign(tset.begin(), tset.end());
797 const std::size_t K = spec.obsTimes.size();
798 const double unobs = infer::VariationalSpec<double>::unobserved();
799 spec.obsData = Matrix<double>(K, MR, unobs);
800 for (std::map<std::size_t, std::pair<std::vector<double>, std::vector<double>>>::const_iterator
801 it = series.begin(); it != series.end(); ++it) {
802 for (std::size_t k = 0; k < it->second.first.size(); ++k) {
803 const std::vector<double>::const_iterator pos =
804 std::lower_bound(spec.obsTimes.begin(), spec.obsTimes.end(),
805 it->second.first[k]);
806 if (pos != spec.obsTimes.end() && *pos == it->second.first[k]) {
807 spec.obsData(static_cast<std::size_t>(pos - spec.obsTimes.begin()),
808 it->first) = std::round(it->second.second[k]);
809 }
810 }
811 }
812
813 // population per class bounds both the contamination support and the load
814 spec.obsRange.assign(MR, 1.0);
815 spec.capacity.assign(MR, std::numeric_limits<double>::infinity());
816 spec.x0 = Matrix<double>(M, R, 0.0);
817 for (std::size_t r = 0; r < R; ++r) {
818 const double njobs = sn.classes[r].population;
819 double pop;
820 const bool closed = std::isfinite(njobs);
821 if (closed) {
822 pop = njobs;
823 } else {
824 double peak = 1.0;
825 for (std::size_t k = 0; k < K; ++k)
826 for (std::size_t i = 0; i < M; ++i)
827 if (spec.obsData(k, r * M + i) != unobs)
828 peak = std::max(peak, spec.obsData(k, r * M + i));
829 pop = std::max(1.0, 2.0 * peak);
830 }
831 for (std::size_t i = 0; i < M; ++i) {
832 spec.obsRange[r * M + i] = pop;
833 if (closed) spec.capacity[r * M + i] = pop;
834 }
835 if (closed && njobs > 0) {
836 const std::size_t ref = sn.classes[r].refstat;
837 spec.x0((ref >= 1 && ref <= M) ? ref - 1 : 0, r) = njobs;
838 }
839 }
840
841 // Gamma priors centred on the model's current rates
842 spec.alpha0.assign(P, 0.0);
843 spec.beta0.assign(P, 0.0);
844 for (std::size_t i = 0; i < M; ++i) {
845 for (std::size_t r = 0; r < R; ++r) {
846 const std::size_t p = estimated[r * M + i];
847 if (p > 0) {
848 spec.alpha0[p - 1] = options.prior_shape;
849 spec.beta0[p - 1] = options.prior_shape / sn.rates(i, r);
850 }
851 }
852 }
853 spec.epsilon = options.epsilon;
854
855 const infer::VariationalResult<double> out =
857 options.posterior_alpha = out.alpha;
858 options.posterior_beta = out.beta;
859 options.bound = out.bound;
860
861 Matrix<double> est(nodes.size(), R, 0.0);
862 for (std::size_t n = 0; n < nodes.size(); ++n) {
863 const std::size_t i = node_station[n];
864 for (std::size_t r = 0; r < R; ++r) {
865 const std::size_t p = estimated[r * M + i];
866 if (p > 0) {
867 est(n, r) = out.mean_service_time[p - 1];
868 } else if (sn.rates(i, r) > 0) {
869 est(n, r) = 1.0 / sn.rates(i, r);
870 }
871 }
872 }
873 return est;
874 }
875
876 Matrix<double> estimator_qmle(const std::vector<std::size_t>& nodes) {
877 if (nodes.empty()) throw InputError("ParamEstimator.qmle: no stations were requested");
878 std::vector<double> population, think;
879 effective_population_and_think(population, think);
880 const std::size_t classes = population.size();
881 Matrix<double> qlen(nodes.size(), classes, 0.0);
882 for (std::size_t n = 0; n < nodes.size(); ++n) {
883 require_node(nodes[n], "qmle");
884 for (std::size_t r = 0; r < classes; ++r) {
885 const std::vector<SampledMetric*> data = get_qlen(nodes[n], r + 1);
886 if (data.empty() || data[0]->data.empty())
887 throw InputError("ParamEstimator.qmle: queue-length data is missing for node " +
888 std::to_string(nodes[n]) + " class " +
889 std::to_string(r + 1));
890 double sum = 0.0;
891 for (const double value : data[0]->data) sum += value;
892 qlen(n, r) = sum / static_cast<double>(data[0]->data.size());
893 }
894 }
895 return infer_qmle(qlen, population, think);
896 }
897
898 Matrix<double> estimator_gibbs(const std::vector<std::size_t>& nodes) {
899 std::size_t node = 0, station = 0;
900 require_single_station(nodes, "gibbs", node, station);
901 const qn::NetworkStruct<double>& sn = model_.get_struct();
902 std::vector<GibbsTrace<double>> traces(sn.classes.size());
903 for (std::size_t r = 0; r < sn.classes.size(); ++r) {
904 SampledMetric* arvr = get_arvr(node, r + 1);
905 SampledMetric* respt = get_respt(node, r + 1);
906 SampledMetric* tput = get_tput(node, r + 1);
907 if (arvr == nullptr || respt == nullptr || tput == nullptr)
908 throw InputError("ParamEstimator.gibbs: arrival, response-time, and throughput "
909 "data are required for class " + std::to_string(r + 1));
910 if (!arvr->is_trace() || !respt->is_trace())
911 throw InputError("ParamEstimator.gibbs: arrival and response-time metrics must "
912 "use trace format");
913 traces[r].arrival_ms.resize(arvr->data.size());
914 for (std::size_t i = 0; i < arvr->data.size(); ++i)
915 traces[r].arrival_ms[i] = arvr->data[i] * 1000.0;
916 traces[r].respt_s = respt->data;
917 traces[r].think_obs = tput->data;
918 }
919 GibbsOptions gibbs = options.gibbs;
920 gibbs.tol = options.tol;
922 const std::vector<double> demand =
923 infer_gibbs(traces, sn.stations[station - 1].nservers, gibbs, rng);
924 Matrix<double> out(1, demand.size(), 0.0);
925 for (std::size_t r = 0; r < demand.size(); ++r) out(0, r) = demand[r];
926 return out;
927 }
928
929 Matrix<double> estimator_mlps(const std::vector<std::size_t>& nodes) {
930 std::size_t node = 0, station = 0;
931 require_single_station(nodes, "mlps", node, station);
932 const qn::NetworkStruct<double>& sn = model_.get_struct();
933 if (sn.stations[station - 1].sched != lang::SchedStrategy::PS)
934 throw InputError("ParamEstimator.mlps: the target station must use PS scheduling");
935 std::vector<double> population, think;
936 effective_population_and_think(population, think);
937 std::vector<double> rates(think.size(), 0.0);
938 for (std::size_t r = 0; r < think.size(); ++r) rates[r] = 1.0 / think[r];
939 const std::vector<double> demand =
940 api::infer_mlps(rates, sn.stations[station - 1].nservers,
941 mlps_samples(node, "mlps"));
942 Matrix<double> out(1, demand.size(), 0.0);
943 for (std::size_t r = 0; r < demand.size(); ++r) out(0, r) = demand[r];
944 return out;
945 }
946
947 Matrix<double> estimator_fmlps(const std::vector<std::size_t>& nodes) {
948 std::size_t node = 0, station = 0;
949 require_single_station(nodes, "fmlps", node, station);
950 const qn::NetworkStruct<double>& sn = model_.get_struct();
951 if (sn.stations[station - 1].sched != lang::SchedStrategy::PS)
952 throw InputError("ParamEstimator.fmlps: the target station must use PS scheduling");
953 std::size_t equivalent_queue = 0;
954 qn::Network<double> equivalent = build_closed_equivalent(node, equivalent_queue);
955 const qn::NetworkStruct<double>& eqsn = equivalent.get_struct();
956 const std::size_t eqstation = eqsn.nodes[equivalent_queue - 1].station;
957 const std::vector<double> demand =
958 api::infer_fmlps(eqsn, eqstation, mlps_samples(node, "fmlps"));
959 Matrix<double> out(1, demand.size(), 0.0);
960 for (std::size_t r = 0; r < demand.size(); ++r) out(0, r) = demand[r];
961 return out;
962 }
963
964 Matrix<double> estimator_ubo(const std::vector<std::size_t>& nodes) {
965 if (nodes.empty()) throw InputError("ParamEstimator.ubo: no stations were requested");
966 const qn::NetworkStruct<double>& sn = model_.get_struct();
967 const std::size_t stations = nodes.size(), classes = sn.classes.size();
968 std::size_t samples_count = 0;
969 std::vector<const SampledMetric*> util(stations, nullptr);
970 std::vector<std::vector<const SampledMetric*>> arrivals(
971 stations, std::vector<const SampledMetric*>(classes, nullptr));
972 std::vector<std::vector<const SampledMetric*>> response(
973 stations, std::vector<const SampledMetric*>(classes, nullptr));
974
975 for (std::size_t i = 0; i < stations; ++i) {
976 require_node(nodes[i], "ubo");
977 const std::size_t station = sn.nodes[nodes[i] - 1].station;
978 if (station == 0 || !std::isfinite(sn.stations[station - 1].nservers))
979 throw InputError("ParamEstimator.ubo: every target must be a finite-server station");
980 util[i] = get_aggr_util(nodes[i]);
981 if (util[i] == nullptr)
982 throw InputError("ParamEstimator.ubo: aggregate utilization is missing for node " +
983 std::to_string(nodes[i]));
984 if (i == 0) samples_count = util[i]->data.size();
985 if (util[i]->data.size() != samples_count)
986 throw InputError("ParamEstimator.ubo: sampled metrics have different sample "
987 "counts; call interpolate first");
988 for (std::size_t r = 0; r < classes; ++r) {
989 arrivals[i][r] = get_arvr(nodes[i], r + 1);
990 response[i][r] = get_respt(nodes[i], r + 1);
991 if (arrivals[i][r] == nullptr || response[i][r] == nullptr)
992 throw InputError("ParamEstimator.ubo: arrival-rate and response-time data are "
993 "required for node " + std::to_string(nodes[i]) + " class " +
994 std::to_string(r + 1));
995 if (arrivals[i][r]->data.size() != samples_count ||
996 response[i][r]->data.size() != samples_count)
997 throw InputError("ParamEstimator.ubo: sampled metrics have different sample "
998 "counts; call interpolate first");
999 }
1000 }
1001
1002 std::vector<std::size_t> valid;
1003 for (std::size_t n = 0; n < samples_count; ++n) {
1004 bool keep = true;
1005 double total_arrival = 0.0;
1006 for (std::size_t i = 0; i < stations; ++i) {
1007 if (!std::isfinite(util[i]->data[n])) keep = false;
1008 for (std::size_t r = 0; r < classes; ++r)
1009 total_arrival += arrivals[i][r]->data[n];
1010 }
1011 if (keep && total_arrival > 0.0) valid.push_back(n);
1012 }
1013 if (valid.empty()) throw InputError("ParamEstimator.ubo: no usable experiments remain");
1014
1015 const std::size_t variables = stations * classes;
1016 Matrix<double> design(valid.size() * (classes + stations), variables, 0.0);
1017 std::vector<double> target(design.rows(), 0.0);
1018 std::size_t row = 0;
1019 for (const std::size_t n : valid) {
1020 std::vector<double> rho(stations, 0.0), beta(stations, 0.0);
1021 std::vector<double> lambda_class(classes, 0.0), end_to_end(classes, 0.0);
1022 for (std::size_t i = 0; i < stations; ++i) {
1023 const std::size_t station = sn.nodes[nodes[i] - 1].station;
1024 rho[i] = util[i]->data[n] * sn.stations[station - 1].nservers;
1025 if (rho[i] == 1.0)
1026 throw NumericError(ErrorKind{"Instability"}, "ParamEstimator.ubo: utilization reaches one");
1027 beta[i] = 1.0 / (1.0 - rho[i]);
1028 for (std::size_t r = 0; r < classes; ++r) {
1029 lambda_class[r] += arrivals[i][r]->data[n];
1030 end_to_end[r] += response[i][r]->data[n];
1031 }
1032 }
1033 double total_lambda = 0.0;
1034 for (const double value : lambda_class) total_lambda += value;
1035 for (std::size_t r = 0; r < classes; ++r) {
1036 const double weight = std::sqrt(lambda_class[r] / total_lambda);
1037 for (std::size_t i = 0; i < stations; ++i)
1038 design(row, r * stations + i) = weight * beta[i];
1039 target[row++] = weight * end_to_end[r];
1040 }
1041 for (std::size_t i = 0; i < stations; ++i) {
1042 for (std::size_t r = 0; r < classes; ++r)
1043 design(row, r * stations + i) = arrivals[i][r]->data[n];
1044 target[row++] = rho[i];
1045 }
1046 }
1047 const std::vector<double> fit = detail::nnls(design, target);
1048 Matrix<double> out(stations, classes, 0.0);
1049 for (std::size_t r = 0; r < classes; ++r)
1050 for (std::size_t i = 0; i < stations; ++i)
1051 out(i, r) = fit[r * stations + i];
1052 return out;
1053 }
1054
1055 Matrix<double> estimator_erps(const std::vector<std::size_t>& nodes) {
1056 std::size_t node = 0, station = 0;
1057 require_single_station(nodes, "erps", node, station);
1058 const qn::NetworkStruct<double>& sn = model_.get_struct();
1059 if (sn.stations[station - 1].sched != lang::SchedStrategy::PS)
1060 throw InputError("ParamEstimator.erps: the target station must use PS scheduling");
1061 const std::size_t classes = sn.classes.size();
1062 std::vector<const SampledMetric*> response(classes, nullptr), qlen(classes, nullptr);
1063 double busy_sum = 0.0;
1064 std::size_t busy_count = 0;
1065 for (std::size_t r = 0; r < classes; ++r) {
1066 response[r] = get_respt(node, r + 1);
1067 const ConditionEvent arrival(node, r + 1, lang::EventType::ARV);
1068 qlen[r] = get_aggr_qlen(node, &arrival);
1069 if (response[r] == nullptr || qlen[r] == nullptr)
1070 throw InputError("ParamEstimator.erps: response-time and arrival-conditional "
1071 "aggregate queue-length data are required for class " +
1072 std::to_string(r + 1));
1073 if (response[r]->data.size() != qlen[r]->data.size())
1074 throw InputError("ParamEstimator.erps: sampled metrics have different sample "
1075 "counts; call interpolate first");
1076 for (const double q : qlen[r]->data) {
1077 if (q < 1.0)
1078 throw InputError("ParamEstimator.erps: an arrival queue length must include "
1079 "the arriving job");
1080 busy_sum += q;
1081 ++busy_count;
1082 }
1083 }
1084 if (busy_count == 0) throw InputError("ParamEstimator.erps: no observations");
1085 const double busy =
1086 std::min(busy_sum / static_cast<double>(busy_count),
1087 sn.stations[station - 1].nservers);
1088 if (!(busy > 0.0)) throw NumericError(ErrorKind{"Degenerate"}, "ParamEstimator.erps: zero average busy cores");
1089 Matrix<double> out(1, classes, 0.0);
1090 for (std::size_t r = 0; r < classes; ++r) {
1091 double aa = 0.0, ab = 0.0;
1092 for (std::size_t i = 0; i < qlen[r]->data.size(); ++i) {
1093 const double regressor = qlen[r]->data[i] / busy;
1094 aa += regressor * regressor;
1095 ab += regressor * response[r]->data[i];
1096 }
1097 if (!(aa > 0.0)) throw NumericError(ErrorKind{"Degenerate"}, "ParamEstimator.erps: zero queue-length regressor");
1098 out(0, r) = std::max(0.0, ab / aa);
1099 }
1100 return out;
1101 }
1102
1103 std::vector<double> solver_measurement(const qn::NetworkStruct<double>& base,
1104 std::size_t station,
1105 const std::vector<double>& demand) {
1106 qn::NetworkStruct<double> sn = base;
1107 for (std::size_t r = 0; r < demand.size(); ++r) {
1108 if (!(demand[r] > 0.0) || !std::isfinite(demand[r]))
1109 throw NumericError(ErrorKind{"Degenerate"}, "ParamEstimator: a solver-backed estimate reached a "
1110 "non-positive service demand");
1111 sn.set_service(station, r + 1, lang::Distrib<double>::exp_mean(demand[r]));
1112 }
1113 sn.refresh_rates();
1114 mva::MvaOptions solver_options;
1115 solver_options.method = "default";
1116 const mva::AvgResult<double> solved =
1117 mva::solver_mva_run_analyzer(sn, solver_options, Matrix<double>());
1118 std::vector<double> measurement(demand.size() + 1, 0.0);
1119 for (std::size_t r = 0; r < demand.size(); ++r) {
1120 measurement[r] = solved.RN(station - 1, r);
1121 measurement.back() += solved.UN(station - 1, r);
1122 }
1123 return measurement;
1124 }
1125
1126 Matrix<double> estimator_ekf(const std::vector<std::size_t>& nodes) {
1127 std::size_t node = 0, station = 0;
1128 require_single_station(nodes, "ekf", node, station);
1129 const qn::NetworkStruct<double>& sn = model_.get_struct();
1130 const double servers = sn.stations[station - 1].nservers;
1131 if (!std::isfinite(servers))
1132 throw InputError("ParamEstimator.ekf: the target station must have finite servers");
1133 SampledMetric* aggregate = get_aggr_util(node);
1134 if (aggregate == nullptr)
1135 throw InputError("ParamEstimator.ekf: aggregate utilization data is missing");
1136 const std::size_t classes = sn.classes.size(), count = aggregate->data.size();
1137 std::vector<const SampledMetric*> arrivals(classes, nullptr), response(classes, nullptr);
1138 for (std::size_t r = 0; r < classes; ++r) {
1139 arrivals[r] = get_arvr(node, r + 1);
1140 response[r] = get_respt(node, r + 1);
1141 if (arrivals[r] == nullptr || response[r] == nullptr)
1142 throw InputError("ParamEstimator.ekf: arrival-rate and response-time data are "
1143 "required for class " + std::to_string(r + 1));
1144 if (arrivals[r]->data.size() != count || response[r]->data.size() != count)
1145 throw InputError("ParamEstimator.ekf: sampled metrics have different sample "
1146 "counts; call interpolate first");
1147 }
1148
1149 std::vector<std::size_t> valid;
1150 for (std::size_t n = 0; n < count; ++n) {
1151 double throughput = 0.0;
1152 for (std::size_t r = 0; r < classes; ++r) throughput += arrivals[r]->data[n];
1153 if (std::isfinite(aggregate->data[n]) && throughput != 0.0) valid.push_back(n);
1154 }
1155 if (valid.empty()) throw InputError("ParamEstimator.ekf: no usable experiments remain");
1156
1157 std::vector<double> x(classes, 0.0);
1158 if (!options.x0.empty()) {
1159 if (options.x0.size() != classes)
1160 throw InputError("ParamEstimator.ekf: x0 must contain one demand per class");
1161 for (const double value : options.x0)
1162 if (!(value > 0.0))
1163 throw InputError("ParamEstimator.ekf: x0 must contain one positive demand per class");
1164 x = options.x0;
1165 } else {
1167 for (std::size_t r = 0; r < classes; ++r) {
1168 double maximum = 0.0;
1169 for (const double value : response[r]->data) maximum = std::max(maximum, value);
1170 x[r] = pfqn::mc_uniform<double>(rng) * maximum;
1171 if (!(x[r] > 0.0)) x[r] = std::max(1e-9, maximum * 0.5);
1172 }
1173 }
1174
1175 Matrix<double> covariance(classes, classes, 0.0);
1176 for (std::size_t r = 0; r < classes; ++r) covariance(r, r) = x[r] * x[r];
1177 const double delta = 1e-6;
1178 const std::size_t iterations = std::min(valid.size(), options.iter_max);
1179 for (std::size_t step = 0; step < iterations; ++step) {
1180 const std::size_t n = valid[step];
1181 Matrix<double> predicted_cov = covariance;
1182 for (std::size_t r = 0; r < classes; ++r) predicted_cov(r, r) += 0.001;
1183 const std::vector<double> predicted = solver_measurement(sn, station, x);
1184
1185 Matrix<double> jacobian(classes + 1, classes, 0.0);
1186 for (std::size_t c = 0; c < classes; ++c) {
1187 std::vector<double> perturbed(x);
1188 perturbed[c] += delta;
1189 const std::vector<double> moved = solver_measurement(sn, station, perturbed);
1190 for (std::size_t k = 0; k < classes + 1; ++k)
1191 jacobian(k, c) = (moved[k] - predicted[k]) / delta;
1192 }
1193
1194 Matrix<double> innovation_cov(classes + 1, classes + 1, 0.0);
1195 for (std::size_t i = 0; i < classes + 1; ++i)
1196 for (std::size_t j = 0; j < classes + 1; ++j) {
1197 double value = i == j ? 0.01 : 0.0;
1198 for (std::size_t a = 0; a < classes; ++a)
1199 for (std::size_t b = 0; b < classes; ++b)
1200 value += jacobian(i, a) * predicted_cov(a, b) * jacobian(j, b);
1201 innovation_cov(i, j) = value;
1202 }
1203
1204 Matrix<double> gain(classes, classes + 1, 0.0);
1205 for (std::size_t r = 0; r < classes; ++r) {
1206 std::vector<double> rhs(classes + 1, 0.0);
1207 for (std::size_t k = 0; k < classes + 1; ++k)
1208 for (std::size_t a = 0; a < classes; ++a)
1209 rhs[k] += predicted_cov(r, a) * jacobian(k, a);
1210 const std::vector<double> solved = solve(innovation_cov, rhs);
1211 for (std::size_t k = 0; k < classes + 1; ++k) gain(r, k) = solved[k];
1212 }
1213
1214 std::vector<double> residual(classes + 1, 0.0);
1215 for (std::size_t r = 0; r < classes; ++r)
1216 residual[r] = response[r]->data[n] - predicted[r];
1217 residual.back() = aggregate->data[n] * servers - predicted.back();
1218 for (std::size_t r = 0; r < classes; ++r) {
1219 double update = x[r];
1220 for (std::size_t k = 0; k < classes + 1; ++k)
1221 update += gain(r, k) * residual[k];
1222 // positivity guard: a step that would make a demand non-positive shrinks the previous estimate instead
1223 x[r] = update > 0.0 ? update : 0.4 * x[r];
1224 }
1225
1226 Matrix<double> next(classes, classes, 0.0);
1227 for (std::size_t i = 0; i < classes; ++i)
1228 for (std::size_t j = 0; j < classes; ++j) {
1229 double value = 0.0;
1230 for (std::size_t a = 0; a < classes; ++a) {
1231 double ikh = i == a ? 1.0 : 0.0;
1232 for (std::size_t k = 0; k < classes + 1; ++k)
1233 ikh -= gain(i, k) * jacobian(k, a);
1234 value += ikh * predicted_cov(a, j);
1235 }
1236 next(i, j) = value;
1237 }
1238 covariance = next;
1239 }
1240 Matrix<double> out(1, classes, 0.0);
1241 for (std::size_t r = 0; r < classes; ++r) out(0, r) = x[r];
1242 return out;
1243 }
1244
1245 Matrix<double> estimator_mcmc(const std::vector<std::size_t>& nodes) {
1246 if (nodes.empty()) throw InputError("ParamEstimator.mcmc: no stations were requested");
1247 const qn::NetworkStruct<double>& sn = model_.get_struct();
1248 const std::size_t classes = sn.classes.size();
1249 std::vector<double> population, think;
1250 effective_population_and_think(population, think);
1251
1252 // Per-class samples when every class has exactly one; otherwise the aggregate, which identifies only
1253 // sum_r Q(i,r) and is therefore shared equally by the classes (no sample tells them apart).
1254 Matrix<double> average(nodes.size(), classes, 0.0);
1255 std::size_t experiments = 0;
1256 auto accumulate = [&](const SampledMetric* metric, std::size_t i, std::size_t r,
1257 double share) {
1258 if (experiments == 0)
1259 experiments = metric->data.size();
1260 else if (metric->data.size() != experiments)
1261 throw InputError("ParamEstimator.mcmc: sampled metrics have different sample "
1262 "counts; call interpolate first");
1263 double sum = 0.0;
1264 for (const double value : metric->data) {
1265 if (!(value >= 0.0) || !std::isfinite(value))
1266 throw InputError("ParamEstimator.mcmc: queue lengths must be finite and "
1267 "non-negative");
1268 sum += value;
1269 }
1270 average(i, r) = sum / share / static_cast<double>(experiments);
1271 };
1272 for (std::size_t i = 0; i < nodes.size(); ++i) {
1273 require_node(nodes[i], "mcmc");
1274 std::vector<const SampledMetric*> per_class(classes, nullptr);
1275 bool have_per_class = true;
1276 for (std::size_t r = 0; r < classes && have_per_class; ++r) {
1277 const std::vector<SampledMetric*> found = get_qlen(nodes[i], r + 1);
1278 if (found.size() != 1 || found[0]->data.empty())
1279 have_per_class = false;
1280 else
1281 per_class[r] = found[0];
1282 }
1283 if (have_per_class) {
1284 for (std::size_t r = 0; r < classes; ++r) accumulate(per_class[r], i, r, 1.0);
1285 } else {
1286 SampledMetric* qlen = get_aggr_qlen(nodes[i]);
1287 if (qlen == nullptr || qlen->data.empty())
1288 throw InputError("ParamEstimator.mcmc: queue-length data is missing for "
1289 "node " + std::to_string(nodes[i]));
1290 for (std::size_t r = 0; r < classes; ++r)
1291 accumulate(qlen, i, r, static_cast<double>(classes));
1292 }
1293 }
1294
1295 // A demand cannot exceed 1/X(c) (unit utilization), and X(c) is at least (P(c)-sum_i Q(i,c))/Z(c)
1296 // when the rest of the population thinks at Z(c).
1297 double upper = 0.0;
1298 for (std::size_t i = 0; i < average.rows(); ++i)
1299 for (std::size_t r = 0; r < average.cols(); ++r)
1300 upper = std::max(upper, average(i, r));
1301 for (std::size_t r = 0; r < classes; ++r) {
1302 double column = 0.0;
1303 for (std::size_t i = 0; i < nodes.size(); ++i) column += average(i, r);
1304 if (think[r] > 0.0 && population[r] > column)
1305 upper = std::max(upper, think[r] / (population[r] - column));
1306 }
1307 if (!(upper > 0.0))
1308 throw InputError("ParamEstimator.mcmc: at least one mean queue length must be positive");
1309
1310 std::vector<int> pop(classes, 0);
1311 double lattice = 1.0;
1312 Matrix<double> thinkz(1, classes, 0.0);
1313 for (std::size_t r = 0; r < classes; ++r) {
1314 pop[r] = static_cast<int>(std::lround(population[r]));
1315 lattice *= static_cast<double>(pop[r] + 1);
1316 thinkz(0, r) = think[r];
1317 }
1318 // exact convolution while the population lattice is small, else the default pfqn_nc method
1319 const bool exact_g = lattice <= 1e4;
1320
1321 // Gibbs sampling over a 400-step grid on [0, upper] with a uniform prior. The normalizing constant is
1322 // a function of the candidate demand, so it is evaluated with theta(i,r) set to that candidate.
1323 const std::size_t samples_count = 100, grid_count = 400;
1324 const double step = upper / static_cast<double>(grid_count);
1325 const double log_prior = std::log(1.0 / static_cast<double>(grid_count));
1326 const double nexp = static_cast<double>(experiments);
1327 std::vector<Matrix<double>> theta(samples_count + 1,
1328 Matrix<double>(nodes.size(), classes, 0.0));
1330 std::vector<double> log_post(grid_count + 1, 0.0);
1331 for (std::size_t s = 0; s < samples_count; ++s) {
1332 Matrix<double> sample = theta[s];
1333 for (std::size_t i = 0; i < nodes.size(); ++i) {
1334 for (std::size_t r = 0; r < classes; ++r) {
1335 double best = -std::numeric_limits<double>::infinity();
1336 for (std::size_t k = 0; k <= grid_count; ++k) {
1337 const double candidate = step * static_cast<double>(k);
1338 Matrix<double> trial = sample;
1339 trial(i, r) = candidate;
1340 const double lG = exact_g
1341 ? pfqn::pfqn_ca(trial, pop, thinkz).lG
1342 : pfqn::pfqn_nc(trial, pop, thinkz,
1344 // 0*log(0) is taken as 0: a zero mean queue length puts no weight on the demand
1345 const double loglik = average(i, r) == 0.0
1346 ? 0.0
1347 : nexp * average(i, r) * std::log(candidate);
1348 log_post[k] = loglik - nexp * lG + log_prior;
1349 best = std::max(best, log_post[k]);
1350 }
1351 double total = 0.0;
1352 std::vector<double> weights(grid_count + 1, 0.0);
1353 for (std::size_t k = 0; k <= grid_count; ++k) {
1354 weights[k] = std::isfinite(log_post[k]) ? std::exp(log_post[k] - best) : 0.0;
1355 total += weights[k];
1356 }
1357 double draw = pfqn::mc_uniform01(rng) * total;
1358 std::size_t picked = grid_count;
1359 for (std::size_t k = 0; k <= grid_count; ++k) {
1360 draw -= weights[k];
1361 if (draw < 0.0) {
1362 picked = k;
1363 break;
1364 }
1365 }
1366 sample(i, r) = step * static_cast<double>(picked);
1367 }
1368 }
1369 theta[s + 1] = sample;
1370 }
1371
1372 // posterior mean over theta(cutoff:end) of the reference (1-based, theta(1) the zero start), divided
1373 // by the visit ratios to give mean service times
1374 Matrix<double> out(nodes.size(), classes, 0.0);
1375 const std::size_t cutoff = samples_count / 2 - 1;
1376 for (std::size_t i = 0; i < nodes.size(); ++i) {
1377 for (std::size_t r = 0; r < classes; ++r) {
1378 double visit = 1.0;
1379 for (std::size_t c = 0; c < sn.chains.size(); ++c) {
1380 if (r >= sn.chains[c].size() || !sn.chains[c][r] ||
1381 c >= sn.nodevisits.size())
1382 continue;
1383 if (nodes[i] - 1 < sn.nodevisits[c].rows() &&
1384 r < sn.nodevisits[c].cols()) {
1385 const double candidate = sn.nodevisits[c](nodes[i] - 1, r);
1386 if (candidate > 0.0) visit = candidate;
1387 }
1388 break;
1389 }
1390 for (std::size_t s = cutoff; s <= samples_count; ++s)
1391 out(i, r) += theta[s](i, r) / visit;
1392 out(i, r) /= static_cast<double>(samples_count - cutoff + 1);
1393 }
1394 }
1395 return out;
1396 }
1397
1398 Matrix<double> estimator_mle(const std::vector<std::size_t>& nodes) {
1399 std::size_t node = 0, station = 0;
1400 require_single_station(nodes, "mle", node, station);
1401 const qn::NetworkStruct<double>& sn = model_.get_struct();
1402 const double servers = sn.stations[station - 1].nservers;
1403 if (!std::isfinite(servers))
1404 throw InputError("ParamEstimator.mle: the target station must have finite servers");
1405 SampledMetric* aggregate = get_aggr_util(node);
1406 if (aggregate == nullptr)
1407 throw InputError("ParamEstimator.mle: aggregate utilization data is missing");
1408 const std::size_t classes = sn.classes.size(), count = aggregate->data.size();
1409 std::vector<const SampledMetric*> arrivals(classes, nullptr), response(classes, nullptr);
1410 std::vector<double> upper(classes, 0.0);
1411 for (std::size_t r = 0; r < classes; ++r) {
1412 arrivals[r] = get_arvr(node, r + 1);
1413 response[r] = get_respt(node, r + 1);
1414 if (arrivals[r] == nullptr || response[r] == nullptr)
1415 throw InputError("ParamEstimator.mle: arrival-rate and response-time data are "
1416 "required for class " + std::to_string(r + 1));
1417 if (arrivals[r]->data.size() != count || response[r]->data.size() != count)
1418 throw InputError("ParamEstimator.mle: sampled metrics have different sample "
1419 "counts; call interpolate first");
1420 for (const double value : response[r]->data)
1421 if (std::isfinite(value)) upper[r] = std::max(upper[r], value);
1422 if (!(upper[r] >= 1e-8))
1423 throw InputError("ParamEstimator.mle: response-time upper bounds must be positive");
1424 }
1425
1426 std::vector<std::size_t> valid;
1427 for (std::size_t n = 0; n < count; ++n) {
1428 double throughput = 0.0;
1429 for (std::size_t r = 0; r < classes; ++r) throughput += arrivals[r]->data[n];
1430 if (std::isfinite(aggregate->data[n]) && throughput > 0.0) valid.push_back(n);
1431 }
1432 if (valid.empty()) throw InputError("ParamEstimator.mle: no usable experiments remain");
1433
1434 std::vector<double> x0(classes, 0.0);
1435 if (!options.x0.empty()) {
1436 if (options.x0.size() != classes)
1437 throw InputError("ParamEstimator.mle: x0 must contain one demand per class");
1438 x0 = options.x0;
1439 } else {
1441 for (std::size_t r = 0; r < classes; ++r)
1442 x0[r] = std::max(1e-8, pfqn::mc_uniform01(rng) * upper[r]);
1443 }
1444
1445 std::vector<Bound<double>> bounds(classes);
1446 for (std::size_t r = 0; r < classes; ++r) bounds[r] = bound_box(1e-8, upper[r]);
1447 const auto objective = [&](const std::vector<double>& x) {
1448 const std::vector<double> predicted = solver_measurement(sn, station, x);
1449 double value = 0.0;
1450 for (const std::size_t n : valid) {
1451 double total = 0.0;
1452 for (std::size_t r = 0; r < classes; ++r) total += arrivals[r]->data[n];
1453 for (std::size_t r = 0; r < classes; ++r) {
1454 const double weight = arrivals[r]->data[n] / total;
1455 const double residual = predicted[r] - response[r]->data[n];
1456 value += weight * residual * residual;
1457 }
1458 const double residual = predicted.back() - aggregate->data[n] * servers;
1459 value += residual * residual;
1460 }
1461 return value;
1462 };
1463 NelderMeadOptions<double> nm = nelder_mead_defaults<double>();
1464 nm.max_iter = static_cast<unsigned>(std::min<std::size_t>(
1465 options.iter_max, std::numeric_limits<unsigned>::max()));
1466 nm.max_eval = std::max<unsigned>(1000u, nm.max_iter *
1467 static_cast<unsigned>(10 * (classes + 1)));
1468 const NelderMeadResult<double> fit = nelder_mead_box(objective, x0, bounds, nm);
1469 Matrix<double> out(1, classes, 0.0);
1470 for (std::size_t r = 0; r < classes; ++r) out(0, r) = fit.x[r];
1471 return out;
1472 }
1473
1474 Matrix<double> estimator_ubr(const std::vector<std::size_t>& nodes) {
1475 if (nodes.size() != 1)
1476 throw InputError("ParamEstimator.ubr: the estimator accepts exactly one station");
1477 const std::size_t node = nodes[0];
1478 require_node(node, "ubr");
1479 const qn::NetworkStruct<double>& sn = model_.raw_struct();
1480 const std::size_t station = sn.nodes[node - 1].station;
1481 if (station == 0)
1482 throw InputError("ParamEstimator.ubr: the target node is not a station");
1483 const double servers = sn.stations[station - 1].nservers;
1484 if (!std::isfinite(servers))
1485 throw InputError("ParamEstimator.ubr: the target station must have finite servers");
1486 const std::size_t classes = sn.classes.size();
1487
1488 std::vector<const SampledMetric*> arrivals(classes, nullptr), utils(classes, nullptr);
1489 std::size_t count = 0;
1490 for (std::size_t r = 0; r < classes; ++r) {
1491 arrivals[r] = get_arvr(node, r + 1);
1492 if (arrivals[r] == nullptr)
1493 throw InputError("ParamEstimator.ubr: arrival-rate data is missing for class " +
1494 std::to_string(r + 1));
1495 if (r == 0)
1496 count = arrivals[r]->data.size();
1497 else if (arrivals[r]->data.size() != count)
1498 throw InputError(
1499 "ParamEstimator.ubr: sampled metrics have different sample counts; call "
1500 "interpolate first");
1501 utils[r] = get_util(node, r + 1);
1502 if (utils[r] != nullptr && utils[r]->data.size() != count)
1503 throw InputError(
1504 "ParamEstimator.ubr: sampled metrics have different sample counts; call "
1505 "interpolate first");
1506 }
1507
1508 Matrix<double> estimates(1, classes, 0.0);
1509 std::vector<std::size_t> unknown;
1510 std::vector<double> known_busy(count, 0.0);
1511 for (std::size_t r = 0; r < classes; ++r) {
1512 if (utils[r] == nullptr) {
1513 unknown.push_back(r);
1514 continue;
1515 }
1516 double aa = 0.0, au = 0.0;
1517 for (std::size_t i = 0; i < count; ++i) {
1518 const double busy = utils[r]->data[i] * servers;
1519 aa += arrivals[r]->data[i] * arrivals[r]->data[i];
1520 au += arrivals[r]->data[i] * busy;
1521 known_busy[i] += busy;
1522 }
1523 if (!(aa > 0.0))
1524 throw NumericError(ErrorKind{"Degenerate"}, "ParamEstimator.ubr: a class has zero arrival-rate regressor");
1525 estimates(0, r) = std::max(0.0, au / aa);
1526 }
1527
1528 if (!unknown.empty()) {
1529 SampledMetric* aggregate = get_aggr_util(node);
1530 if (aggregate == nullptr)
1531 throw InputError(
1532 "ParamEstimator.ubr: aggregate utilization is required for classes without "
1533 "per-class utilization");
1534 if (aggregate->data.size() != count)
1535 throw InputError(
1536 "ParamEstimator.ubr: sampled metrics have different sample counts; call "
1537 "interpolate first");
1538 Matrix<double> A(count, unknown.size(), 0.0);
1539 std::vector<double> residual(count, 0.0);
1540 for (std::size_t i = 0; i < count; ++i) {
1541 residual[i] = aggregate->data[i] * servers - known_busy[i];
1542 for (std::size_t j = 0; j < unknown.size(); ++j)
1543 A(i, j) = arrivals[unknown[j]]->data[i];
1544 }
1545 const std::vector<double> fit = detail::nnls(A, residual);
1546 for (std::size_t j = 0; j < unknown.size(); ++j) estimates(0, unknown[j]) = fit[j];
1547 }
1548 return estimates;
1549 }
1550};
1551
1552} // namespace infer
1553} // namespace line
1554
1555#endif // LINE_INFERENCE_PARAM_ESTIMATOR_H
InputError(const std::string &what)
Definition error.h:105
std::size_t size() const
Definition matrix.h:91
std::size_t rows() const
Definition matrix.h:89
NumericError(const std::string &what)
Definition error.h:112
UnsupportedError(const std::string &what)
Definition error.h:119
Matrix< double > estimate_at(const std::vector< std::size_t > &nodes)
std::vector< std::vector< SampledMetric > > AggregateSamples
std::function< Matrix< double >(ParamEstimator &, const std::vector< std::size_t > &)> Estimator
SampledMetric * get_aggr_util(std::size_t node)
void register_estimator(const std::string &method, const Estimator &estimator)
std::vector< std::vector< std::vector< SampledMetric > > > Samples
static EstimatorOptions default_options()
SampledMetric * get_respt(std::size_t node, std::size_t jobclass)
SampledMetric * get_aggr_qlen(std::size_t node, const ConditionEvent *event=nullptr)
void add_samples(const SampledMetric &sample)
std::vector< SampledMetric * > get_qlen(std::size_t node, std::size_t jobclass, const ConditionEvent *event=nullptr)
const AggregateSamples & get_data_aggr() const
SampledMetric * get_util(std::size_t node, std::size_t jobclass)
ParamEstimator(qn::Network< double > &model, const EstimatorOptions &options=EstimatorOptions())
AggregateSamples & get_data_aggr()
bool has_estimator(const std::string &method) const
SampledMetric * get_tput(std::size_t node, std::size_t jobclass)
static std::string get_required_metrics(const std::string &method)
SampledMetric * get_arvr(std::size_t node, std::size_t jobclass)
const Samples & get_data() const
std::vector< double > data
std::vector< double > t
A network plus its refreshed NetworkStruct.
A queueing network under construction.
NetworkStruct< T > & raw_struct()
The struct WITHOUT refreshing it, for a caller that is still building.
Rate-scaled copy of a distribution, preserving its shape.
The exception types the port throws.
Per-class queue lengths seen by each arriving job, reconstructed from arrival and response time sampl...
Fluid response-time likelihood, and the FMLPS demand estimator built on it.
Gibbs sampling demand estimator for a closed delay-plus-queue model.
Maximum-likelihood service-demand estimation at a processor-sharing queue.
Queue-length-based maximum-likelihood estimator of the service demands of a closed queueing network.
Variational inference for Markovian queueing networks.
LU factorization with partial pivoting, templated on the number type.
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, double W)
FMLPS: the fluid analogue of MLPS, the reference's infer_fmlps(model, node,rt, class,...
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
SnRtStations< T > sn_rt_stations(const qn::NetworkStruct< T > &sn)
VariationalResult< T > infer_variational(VariationalSpec< T > spec, VariationalOptions< T > opt=VariationalOptions< T >())
Run the variational inference procedure.
std::vector< T > infer_gibbs(const std::vector< GibbsTrace< T > > &data, const T &nbCores, const GibbsOptions &opts, pfqn::McRng &rng)
Estimated per-class mean demands.
Matrix< T > infer_qmle(const Matrix< T > &Q, const std::vector< T > &N, const std::vector< T > &Z)
Queue-length-based maximum-likelihood estimator of the service demands of a closed queueing network.
Definition infer_qmle.h:50
Matrix< T > infer_compute_ql_at_arrival(const std::vector< T > &at, const std::vector< long > &at_jobid, const std::vector< T > &rt, const std::vector< long > &rt_jobid, const std::vector< std::size_t > &cls, std::size_t R)
Per-class queue lengths seen by each arriving job, reconstructed from arrival and response time sampl...
AggregateResult< T > aggregate(const MarkovChainModel< T > &m, const std::vector< std::vector< std::size_t > > &MS, const std::string &method="courtois", const T *param=nullptr)
MarkovProcess.aggregate: aggregation-disaggregation over a macrostate partition.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
Definition lang_types.h:185
@ ARV
a job arrives
Definition lang_types.h:118
MetricType
Solver output metrics, with the numeric values of MATLAB MetricType.
Definition lang_types.h:50
const char * process_to_text(ProcessType p)
The MATLAB ProcessType name, as sn.procid prints it.
Definition lang_types.h:601
Distrib< T > dist_scale_rate(const Distrib< T > &d, const T &factor)
The law of X / factor, in the same family as d.
double delta(double t)
std::string upper(std::string s)
Definition srvn_writer.h:86
AvgResult< T > solver_mva_run_analyzer(const qn::NetworkStruct< T > &L, const MvaOptions &opt_in, const Matrix< T > &init_sol)
Port of @@SolverMVA/runAnalyzer.m for the lang='matlab' path: gate, solve, convert,...
std::mt19937_64 McRng
The generator type every Monte Carlo entry point in this tree accepts.
double mc_uniform01(McRng &g)
Uniform deviate on [0,1) with 53 significant bits, as a double.
NcResult< T > pfqn_ca(const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z)
Convolution algorithm for the exact normalizing constant of a closed product-form network (Buzen 1973...
Definition pfqn_ca.h:120
T mc_uniform(McRng &g)
The same deviate materialized in the working arithmetic.
NcDispatchResult< T > pfqn_nc(const std::vector< T > &lambda, const Matrix< T > &L, const std::vector< int > &N, const Matrix< T > &Z, NcMethod method, const T &atol, const NcOptions &nopt)
Normalizing constant of a product-form queueing network: the dispatcher.
Definition pfqn_nc.h:275
Conservation laws of a layered queueing network, enumerated from its structure.
Definition aoi_dist2ph.h:52
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
std::vector< T > solve(const Matrix< T > &A, const std::vector< T > &b)
Convenience: solve Ax = b, leaving A and b untouched.
Definition lu.h:158
NelderMeadOptions< T > nelder_mead_defaults()
fminsearch's coefficients and initial simplex, with tighter tolerances.
Definition neldermead.h:79
Bound< T > bound_box(const T &lo, const T &hi)
lo <= x <= hi.
Definition neldermead.h:146
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,...
Convolution algorithm for the exact normalizing constant of a closed product-form network (Buzen 1973...
Normalizing constant of a product-form queueing network: the dispatcher.
Observed metric data supplied to parameter estimators.
Port of matlab/src/api/sn/sn_rt_stations.m.
The SolverMVA class surface: @@SolverMVA/runAnalyzer.m and the gates around it.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
Definition error.h:37
double epsilon
Probability that a queue-length reading is faulty ("vi").
std::vector< double > bound
Evidence lower bound per iteration, left behind by the same.
VariationalOptions< double > variational
Options of the variational estimator ("vi").
std::vector< double > posterior_beta
double prior_shape
Shape of the Gamma prior placed on each estimated rate ("vi").
std::vector< double > posterior_alpha
Posterior Gamma parameters left behind by the variational estimator.
MATLAB's hard-coded budgets, exposed with their MATLAB values as defaults.
double tol
grid step and convergence tolerance
Options of infer_variational; a negative box means "derive a default".
static T unobserved()
Sentinel marking an unobserved entry of obsData.
static Distrib exp_rate(const T &r)
Definition lang_types.h:955
static Distrib exp_mean(const T &m)
Definition lang_types.h:940