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