272 using Samples = std::vector<std::vector<std::vector<SampledMetric>>>;
275 std::function<Matrix<double>(
ParamEstimator&,
const std::vector<std::size_t>&)>;
282 std::vector<std::vector<SampledMetric>>(
sn.classes.size()));
285 return self.estimator_ubr(nodes);
288 return self.estimator_qmle(nodes);
291 return self.estimator_gibbs(nodes);
294 return self.estimator_mlps(nodes);
297 return self.estimator_fmlps(nodes);
300 return self.estimator_ubo(nodes);
303 return self.estimator_erps(nodes);
306 return self.estimator_ekf(nodes);
309 return self.estimator_mcmc(nodes);
312 return self.estimator_mle(nodes);
315 return self.estimator_variational(nodes);
324 require_node(sample.
node,
"add_samples");
328 require_class(sample.
jobclass,
"add_samples");
357 std::vector<SampledMetric*>
get_qlen(std::size_t node, std::size_t jobclass,
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];
365 if (event !=
nullptr && (!sample.cond.has_value() || *sample.cond != *event))
continue;
366 out.push_back(&sample);
367 if (event !=
nullptr)
break;
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)
384 if (sample.is_trace()) has_trace =
true;
393 if (has_trace && has_respt && has_aggr_qlen)
395 else if (has_trace && has_respt && has_arvr)
397 else if (has_arvr && has_respt && (has_util || has_aggr_util))
399 else if (has_arvr && (has_util || has_aggr_util))
405 "ParamEstimator.auto_method: insufficient data to select an estimation method");
410 std::set<double> union_set;
412 union_set.insert(sample.
t.begin(), sample.
t.end());
414 if (union_set.empty())
return;
415 const std::vector<double> tunion(union_set.begin(), union_set.end());
417 sample.
data = detail::spline_not_a_knot(sample.
t, sample.
data, tunion);
423 estimators_[method] = estimator;
427 return estimators_.find(method) != estimators_.end();
432 if (it == estimators_.end())
434 "' has no C++ adapter");
436 const std::size_t classes = model_.raw_struct().classes.
size();
437 if (values.
rows() != nodes.size() || values.
cols() != classes)
439 "ParamEstimator.estimate_at: estimator result must be nodes by classes");
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");
445 const std::size_t station =
sn.nodes[node - 1].station;
447 throw InputError(
"ParamEstimator.estimate_at: node '" +
448 sn.nodes[node - 1].name +
"' is not a station");
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;
455 throw InputError(
"ParamEstimator.estimate_at: node '" +
456 sn.nodes[node - 1].name +
"' has no class " +
457 std::to_string(r + 1) +
" service process");
459 !std::isfinite(current.
mean)) {
462 switch (current.
type) {
475 "ParamEstimator.estimate_at: " +
477 " has no MATLAB setMean contract");
479 const double factor = current.
mean / target;
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)"},
494 "RespT (per-class) + QLen (aggregate, conditional on class arrivals). PS "
496 {
"ekf",
"RespT (per-class) + Util (aggregate). Sequential/recursive estimation."},
497 {
"mcmc",
"QLen (aggregate). Gibbs sampling with MCMC. Open/mixed via closed "
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 / "
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;
516 std::map<std::string, Estimator> estimators_;
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");
523 void require_class(std::size_t jobclass,
const char* method)
const {
526 if (jobclass == 0 || jobclass > classes)
527 throw InputError(std::string(
"ParamEstimator.") + method +
528 ": class index is out of range");
531 SampledMetric* first_metric(std::size_t node, std::size_t jobclass,
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;
541 const ConditionEvent* event) {
542 require_node(node,
"aggregate metric lookup");
544 if (sample.type != type)
continue;
545 if (event !=
nullptr && (!sample.cond.has_value() || *sample.cond != *event))
continue;
552 void for_each_sample(F fn) {
554 for (
auto& cls : node)
555 for (SampledMetric& sample : cls) fn(sample);
557 for (SampledMetric& sample : node) fn(sample);
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");
566 require_node(node, method);
567 const qn::NetworkStruct<double>& sn = model_.raw_struct();
568 station = sn.nodes[node - 1].station;
570 throw InputError(std::string(
"ParamEstimator.") + method +
571 ": the target node is not a station");
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
585 for (
const qn::NodeDef& node : sn.nodes) {
586 if (node.station == 0)
continue;
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;
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;
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");
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");
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],
626 equivalent.set_service(equivalent_queue, classes[r], sn.service[station - 1][r]);
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);
633 equivalent.link(routing);
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);
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 =
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]; });
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);
678 throw InputError(std::string(
"ParamEstimator.") + method +
679 ": no positive response-time observations remain");
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;
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) {
714 throw InputError(
"ParamEstimator.vi: unsupported scheduling at station " +
715 std::to_string(i + 1));
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}});
732 if (spec.arcs.empty())
733 throw InputError(
"ParamEstimator.vi: the model has no job transitions to infer from");
736 std::vector<std::size_t> estimated(MR, 0), node_station(nodes.size(), 0);
738 for (std::size_t n = 0; n < nodes.size(); ++n) {
739 require_node(nodes[n],
"vi");
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;
752 throw InputError(
"ParamEstimator.vi: no station-class pair with a positive rate");
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];
762 spec.arcparam[e] = p;
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");
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;
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);
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();
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]);
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;
817 const bool closed = std::isfinite(njobs);
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);
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;
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;
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];
852 const infer::VariationalResult<double> out =
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];
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);
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));
888 for (
const double value : data[0]->data) sum += value;
889 qlen(n, r) = sum /
static_cast<double>(data[0]->data.size());
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 "
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;
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];
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();
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 =
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];
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();
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 =
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];
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));
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");
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");
999 std::vector<std::size_t> valid;
1000 for (std::size_t n = 0; n < samples_count; ++n) {
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];
1008 if (keep && total_arrival > 0.0) valid.push_back(n);
1010 if (valid.empty())
throw InputError(
"ParamEstimator.ubo: no usable experiments remain");
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;
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];
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];
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];
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];
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();
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) {
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) {
1075 throw InputError(
"ParamEstimator.erps: an arrival queue length must include "
1076 "the arriving job");
1081 if (busy_count == 0)
throw InputError(
"ParamEstimator.erps: no observations");
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];
1094 if (!(aa > 0.0))
throw NumericError(
"ParamEstimator.erps: zero queue-length regressor");
1095 out(0, r) = std::max(0.0, ab / aa);
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");
1111 mva::MvaOptions solver_options;
1112 solver_options.method =
"default";
1113 const mva::AvgResult<double> solved =
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);
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");
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);
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");
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);
1152 if (valid.empty())
throw InputError(
"ParamEstimator.ekf: no usable experiments remain");
1154 std::vector<double> x(classes, 0.0);
1157 throw InputError(
"ParamEstimator.ekf: x0 must contain one demand per class");
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);
1165 if (!(x[r] > 0.0)) x[r] = std::max(1e-9, maximum * 0.5);
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);
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;
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;
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];
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);
1219 for (
const double value : x) sum += value;
1221 for (
double& value : x) value = -value;
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) {
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);
1237 Matrix<double> out(1, classes, 0.0);
1238 for (std::size_t r = 0; r < classes; ++r) out(0, r) = x[r];
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);
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");
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]));
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");
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 "
1271 for (std::size_t r = 0; r < classes; ++r)
1272 average(i, r) = sum /
static_cast<double>(experiments);
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");
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);
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];
1304 std::size_t picked = grid_count;
1305 for (std::size_t k = 0; k <= grid_count; ++k) {
1312 sample(i, r) = maximum *
static_cast<double>(picked) /
1313 static_cast<double>(grid_count);
1316 theta[s + 1] = sample;
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) {
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())
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;
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);
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");
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);
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");
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);
1377 if (valid.empty())
throw InputError(
"ParamEstimator.mle: no usable experiments remain");
1379 std::vector<double> x0(classes, 0.0);
1382 throw InputError(
"ParamEstimator.mle: x0 must contain one demand per class");
1386 for (std::size_t r = 0; r < classes; ++r)
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);
1395 for (
const std::size_t n : valid) {
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;
1403 const double residual = predicted.back() -
aggregate->data[n] * servers;
1404 value += residual * residual;
1409 nm.max_iter =
static_cast<unsigned>(std::min<std::size_t>(
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];
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;
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();
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));
1441 count = arrivals[r]->data.size();
1442 else if (arrivals[r]->data.size() != count)
1444 "ParamEstimator.ubr: sampled metrics have different sample counts; call "
1445 "interpolate first");
1447 if (utils[r] !=
nullptr && utils[r]->data.size() != count)
1449 "ParamEstimator.ubr: sampled metrics have different sample counts; call "
1450 "interpolate first");
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);
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;
1469 throw NumericError(
"ParamEstimator.ubr: a class has zero arrival-rate regressor");
1470 estimates(0, r) = std::max(0.0, au / aa);
1473 if (!unknown.empty()) {
1475 if (aggregate ==
nullptr)
1477 "ParamEstimator.ubr: aggregate utilization is required for classes without "
1478 "per-class utilization");
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];
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];