274 using Samples = std::vector<std::vector<std::vector<SampledMetric>>>;
277 std::function<Matrix<double>(
ParamEstimator&,
const std::vector<std::size_t>&)>;
284 std::vector<std::vector<SampledMetric>>(
sn.classes.size()));
287 return self.estimator_ubr(nodes);
290 return self.estimator_qmle(nodes);
293 return self.estimator_gibbs(nodes);
296 return self.estimator_mlps(nodes);
299 return self.estimator_fmlps(nodes);
302 return self.estimator_ubo(nodes);
305 return self.estimator_erps(nodes);
308 return self.estimator_ekf(nodes);
311 return self.estimator_mcmc(nodes);
314 return self.estimator_mle(nodes);
317 return self.estimator_variational(nodes);
326 require_node(sample.
node,
"add_samples");
330 require_class(sample.
jobclass,
"add_samples");
359 std::vector<SampledMetric*>
get_qlen(std::size_t node, std::size_t jobclass,
361 require_node(node,
"get_qlen");
362 require_class(jobclass,
"get_qlen");
363 std::vector<SampledMetric*> out;
364 std::vector<SampledMetric>& cell =
samples[node - 1][jobclass - 1];
367 if (event !=
nullptr && (!sample.cond.has_value() || *sample.cond != *event))
continue;
368 out.push_back(&sample);
369 if (event !=
nullptr)
break;
375 bool has_arvr =
false, has_respt =
false, has_util =
false;
376 bool has_qlen =
false, has_tput =
false, has_trace =
false;
377 bool has_aggr_util =
false, has_aggr_qlen =
false;
378 for (
const auto& node :
samples)
379 for (
const auto& cls : node)
386 if (sample.is_trace()) has_trace =
true;
395 if (has_trace && has_respt && has_aggr_qlen)
397 else if (has_trace && has_respt && has_arvr)
399 else if (has_arvr && has_respt && (has_util || has_aggr_util))
401 else if (has_arvr && (has_util || has_aggr_util))
407 "ParamEstimator.auto_method: insufficient data to select an estimation method");
412 std::set<double> union_set;
414 union_set.insert(sample.
t.begin(), sample.
t.end());
416 if (union_set.empty())
return;
417 const std::vector<double> tunion(union_set.begin(), union_set.end());
419 sample.
data = detail::spline_not_a_knot(sample.
t, sample.
data, tunion);
425 estimators_[method] = estimator;
429 return estimators_.find(method) != estimators_.end();
434 if (it == estimators_.end())
436 "' has no C++ adapter");
438 const std::size_t classes = model_.raw_struct().classes.
size();
439 if (values.rows() != nodes.size() || values.cols() != classes)
441 "ParamEstimator.estimate_at: estimator result must be nodes by classes");
443 for (std::size_t n = 0; n < nodes.size(); ++n) {
444 const std::size_t node = nodes[n];
445 require_node(node,
"estimate_at");
447 const std::size_t station =
sn.nodes[node - 1].station;
449 throw InputError(
"ParamEstimator.estimate_at: node '" +
450 sn.nodes[node - 1].name +
"' is not a station");
452 for (std::size_t r = 0; r < classes; ++r) {
453 const double target = values(n, r);
454 if (!std::isfinite(target) || !(target > 0.0))
continue;
457 throw InputError(
"ParamEstimator.estimate_at: node '" +
458 sn.nodes[node - 1].name +
"' has no class " +
459 std::to_string(r + 1) +
" service process");
461 !std::isfinite(current.
mean)) {
464 switch (current.
type) {
477 "ParamEstimator.estimate_at: " +
479 " has no MATLAB setMean contract");
481 const double factor = current.
mean / target;
492 static const std::map<std::string, std::string> descriptions = {
493 {
"ubr",
"ArvR (per-class) + Util (per-class or aggregate)"},
494 {
"ubo",
"ArvR (per-class) + RespT (per-class) + Util (aggregate)"},
496 "RespT (per-class) + QLen (aggregate, conditional on class arrivals). PS "
498 {
"ekf",
"RespT (per-class) + Util (aggregate). Sequential/recursive estimation."},
499 {
"mcmc",
"QLen (per-class, or aggregate shared equally by the classes). Gibbs "
500 "sampling with MCMC. Open/mixed via closed equivalence."},
501 {
"mle",
"ArvR (per-class) + RespT (per-class) + Util (aggregate)"},
502 {
"vi",
"QLen (per-class, timeseries) at every station. Variational "
503 "inference over transition counts; noisy readings, Gamma posteriors."},
504 {
"mlps",
"ArvR (per-class, trace) + RespT (per-class, trace). PS stations only. "
505 "Open/mixed via closed equivalence."},
506 {
"fmlps",
"ArvR (per-class, trace) + RespT (per-class, trace). PS stations only. "
507 "Open/mixed via closed equivalence."},
508 {
"qmle",
"QLen (per-class). Open/mixed via closed equivalence (Z_r = N_r / "
510 {
"gibbs",
"ArvR (per-class, trace) + RespT (per-class, trace) + Tput "
511 "(per-class). Gibbs sampling."}};
512 const auto it = descriptions.find(method);
513 return it == descriptions.end() ?
"Unknown method: " + method : it->second;
518 std::map<std::string, Estimator> estimators_;
520 void require_node(std::size_t node,
const char* method)
const {
521 if (node == 0 || node >
samples.size())
522 throw InputError(std::string(
"ParamEstimator.") + method +
": node index is out of range");
525 void require_class(std::size_t jobclass,
const char* method)
const {
528 if (jobclass == 0 || jobclass > classes)
529 throw InputError(std::string(
"ParamEstimator.") + method +
530 ": class index is out of range");
533 SampledMetric* first_metric(std::size_t node, std::size_t jobclass,
535 require_node(node,
"metric lookup");
536 require_class(jobclass,
"metric lookup");
537 for (SampledMetric& sample :
samples[node - 1][jobclass - 1])
538 if (sample.type == type)
return &sample;
543 const ConditionEvent* event) {
544 require_node(node,
"aggregate metric lookup");
546 if (sample.type != type)
continue;
547 if (event !=
nullptr && (!sample.cond.has_value() || *sample.cond != *event))
continue;
554 void for_each_sample(F fn) {
556 for (
auto& cls : node)
557 for (SampledMetric& sample : cls) fn(sample);
559 for (SampledMetric& sample : node) fn(sample);
562 void require_single_station(
const std::vector<std::size_t>& nodes,
const char* method,
563 std::size_t& node, std::size_t& station)
const {
564 if (nodes.size() != 1)
565 throw InputError(std::string(
"ParamEstimator.") + method +
566 ": the estimator accepts exactly one station");
568 require_node(node, method);
569 const qn::NetworkStruct<double>& sn = model_.raw_struct();
570 station = sn.nodes[node - 1].station;
572 throw InputError(std::string(
"ParamEstimator.") + method +
573 ": the target node is not a station");
576 void effective_population_and_think(std::vector<double>& population,
577 std::vector<double>& think) {
578 const qn::NetworkStruct<double>& sn = model_.get_struct();
579 const std::size_t classes = sn.classes.size();
580 population.assign(classes, 0.0);
581 think.assign(classes, 0.0);
582 for (std::size_t r = 0; r < classes; ++r)
583 population[r] = std::isfinite(sn.classes[r].population)
584 ? sn.classes[r].population
587 for (
const qn::NodeDef& node : sn.nodes) {
588 if (node.station == 0)
continue;
590 for (std::size_t r = 0; r < classes; ++r)
591 if (std::isfinite(sn.classes[r].population) &&
592 !sn.service[node.station - 1][r].disabled)
593 think[r] += sn.service[node.station - 1][r].mean;
595 for (std::size_t r = 0; r < classes; ++r)
596 if (!std::isfinite(sn.classes[r].population)) {
597 const lang::Distrib<double>& arrival = sn.service[node.station - 1][r];
598 if (arrival.disabled || !(arrival.mean > 0.0) ||
599 !std::isfinite(arrival.mean))
600 throw InputError(
"ParamEstimator: an open class has no finite source "
601 "interarrival mean");
602 think[r] = population[r] * arrival.mean;
606 for (std::size_t r = 0; r < classes; ++r)
607 if (!(think[r] > 0.0) || !std::isfinite(think[r]))
608 throw InputError(
"ParamEstimator: class " + std::to_string(r + 1) +
609 " has no positive finite think time or open equivalent");
612 qn::Network<double> build_closed_equivalent(std::size_t node,
613 std::size_t& equivalent_queue) {
614 std::vector<double> population, think;
615 effective_population_and_think(population, think);
616 const qn::NetworkStruct<double>& sn = model_.get_struct();
617 const std::size_t station = sn.nodes[node - 1].station;
618 qn::Network<double> equivalent(
"closed_equiv");
619 const std::size_t delay = equivalent.add_delay(
"Think");
621 equivalent.set_number_of_servers(equivalent_queue, sn.stations[station - 1].nservers);
622 std::vector<std::size_t> classes(population.size(), 0);
623 for (std::size_t r = 0; r < population.size(); ++r) {
624 classes[r] = equivalent.add_closed_class(
"Class" + std::to_string(r + 1),
625 population[r], delay);
626 equivalent.set_service(delay, classes[r],
628 equivalent.set_service(equivalent_queue, classes[r], sn.service[station - 1][r]);
630 qn::RoutingMatrix<double> routing;
631 for (std::size_t r = 0; r < classes.size(); ++r) {
632 routing.set(classes[r], classes[r], delay, equivalent_queue, 1.0);
633 routing.set(classes[r], classes[r], equivalent_queue, delay, 1.0);
635 equivalent.link(routing);
639 std::vector<api::MlpsSample> mlps_samples(std::size_t node,
const char* method) {
640 const std::size_t classes = model_.raw_struct().classes.size();
641 std::vector<double> arrivals, response;
642 std::vector<std::size_t> labels;
643 for (std::size_t r = 0; r < classes; ++r) {
644 SampledMetric* arvr =
get_arvr(node, r + 1);
645 SampledMetric* respt =
get_respt(node, r + 1);
646 if (arvr ==
nullptr || respt ==
nullptr)
647 throw InputError(std::string(
"ParamEstimator.") + method +
648 ": arrival and response-time traces are required for class " +
649 std::to_string(r + 1));
650 if (!arvr->is_trace() || !respt->is_trace())
651 throw InputError(std::string(
"ParamEstimator.") + method +
652 ": arrival and response-time metrics must use trace format");
653 if (arvr->data.size() != respt->data.size())
654 throw InputError(std::string(
"ParamEstimator.") + method +
655 ": arrival and response-time traces have different lengths");
656 arrivals.insert(arrivals.end(), arvr->data.begin(), arvr->data.end());
657 response.insert(response.end(), respt->data.begin(), respt->data.end());
658 labels.insert(labels.end(), arvr->data.size(), r);
660 std::vector<long> ids(arrivals.size(), 0);
661 for (std::size_t i = 0; i < ids.size(); ++i) ids[i] =
static_cast<long>(i + 1);
662 const Matrix<double> qlen =
664 std::vector<std::size_t> order(arrivals.size(), 0);
665 for (std::size_t i = 0; i < order.size(); ++i) order[i] = i;
666 std::stable_sort(order.begin(), order.end(),
667 [&](std::size_t a, std::size_t b) { return arrivals[a] < arrivals[b]; });
669 std::vector<api::MlpsSample> out;
670 for (
const std::size_t i : order) {
671 if (!(response[i] > 0.0))
continue;
672 api::MlpsSample sample;
673 sample.rt = response[i];
674 sample.cls = labels[i] + 1;
675 sample.ql.assign(classes, 0.0);
676 for (std::size_t r = 0; r < classes; ++r) sample.ql[r] = qlen(i, r);
677 out.push_back(sample);
680 throw InputError(std::string(
"ParamEstimator.") + method +
681 ": no positive response-time observations remain");
694 Matrix<double> estimator_variational(
const std::vector<std::size_t>& nodes) {
695 if (nodes.empty())
throw InputError(
"ParamEstimator.vi: no stations were requested");
696 const qn::NetworkStruct<double>& sn = model_.get_struct();
697 const std::size_t M = sn.nstations, R = sn.nclasses, MR = M * R;
702 std::vector<int> sched(M, 1);
703 std::vector<bool> is_source(M,
false);
704 for (std::size_t i = 0; i < M; ++i) {
716 throw UnsupportedError(ErrorKind{
"SchedStrategy"},
"ParamEstimator.vi: unsupported scheduling at station " +
717 std::to_string(i + 1));
721 infer::VariationalSpec<double> spec;
722 std::vector<double> probs;
723 for (std::size_t c = 0; c < R; ++c) {
724 for (std::size_t i = 0; i < M; ++i) {
725 for (std::size_t j = 0; j < M; ++j) {
726 if (i == j || is_source[j])
continue;
727 const double p = rtst(i * R + c, j * R + c);
728 if (!(p > 0))
continue;
729 spec.arcs.push_back({{i + 1, j + 1, c + 1}});
734 if (spec.arcs.empty())
735 throw InputError(
"ParamEstimator.vi: the model has no job transitions to infer from");
738 std::vector<std::size_t> estimated(MR, 0), node_station(nodes.size(), 0);
740 for (std::size_t n = 0; n < nodes.size(); ++n) {
741 require_node(nodes[n],
"vi");
743 for (std::size_t i = 1; i <= M; ++i)
744 if (sn.node_of_station(i) == nodes[n]) st = i;
745 if (st == 0)
throw InputError(
"ParamEstimator.vi: node " + std::to_string(nodes[n]) +
746 " is not a station");
747 node_station[n] = st - 1;
748 for (std::size_t r = 0; r < R; ++r) {
749 const double rate = sn.rates(st - 1, r);
750 if (rate > 0 && std::isfinite(rate)) estimated[r * M + st - 1] = ++
P;
754 throw InputError(
"ParamEstimator.vi: no station-class pair with a positive rate; the Gamma priors are "
755 "centred on the current rates, so give each unknown service an initial guess instead of NaN");
757 const std::size_t narcs = spec.arcs.size();
758 spec.routeprob = probs;
759 spec.arcparam.assign(narcs, 0);
760 spec.arcrate.assign(narcs, 0.0);
761 for (std::size_t e = 0; e < narcs; ++e) {
762 const std::size_t i = spec.arcs[e][0] - 1, c = spec.arcs[e][2] - 1;
763 const std::size_t p = estimated[c * M + i];
765 spec.arcparam[e] = p;
767 spec.arcrate[e] = sn.rates(i, c);
768 if (!(spec.arcrate[e] > 0) || !std::isfinite(spec.arcrate[e]))
769 throw InputError(
"ParamEstimator.vi: station " + std::to_string(i + 1) +
770 " class " + std::to_string(c + 1) +
771 " has no usable rate to hold fixed");
775 spec.nservers.assign(M, 1.0);
776 for (std::size_t i = 0; i < M; ++i) {
777 const double k = sn.stations[i].nservers;
778 spec.nservers[i] = (std::isfinite(k) && k > 0) ? k : 1.0;
782 std::set<double> tset;
783 std::map<std::size_t, std::pair<std::vector<double>, std::vector<double>>> series;
784 for (std::size_t i = 1; i <= M; ++i) {
785 const std::size_t nd = sn.node_of_station(i);
786 if (nd == 0)
continue;
787 for (std::size_t r = 0; r < R; ++r) {
788 const std::vector<SampledMetric*> data =
get_qlen(nd, r + 1);
789 if (data.empty() || data[0]->t.empty())
continue;
790 series[r * M + i - 1] = std::make_pair(data[0]->t, data[0]->data);
791 for (
const double tv : data[0]->t) tset.insert(tv);
795 throw InputError(
"ParamEstimator.vi: queue-length timeseries data is missing");
796 spec.obsTimes.assign(tset.begin(), tset.end());
797 const std::size_t K = spec.obsTimes.size();
799 spec.obsData = Matrix<double>(K, MR, unobs);
800 for (std::map<std::size_t, std::pair<std::vector<double>, std::vector<double>>>::const_iterator
801 it = series.begin(); it != series.end(); ++it) {
802 for (std::size_t k = 0; k < it->second.first.size(); ++k) {
803 const std::vector<double>::const_iterator pos =
804 std::lower_bound(spec.obsTimes.begin(), spec.obsTimes.end(),
805 it->second.first[k]);
806 if (pos != spec.obsTimes.end() && *pos == it->second.first[k]) {
807 spec.obsData(
static_cast<std::size_t
>(pos - spec.obsTimes.begin()),
808 it->first) = std::round(it->second.second[k]);
814 spec.obsRange.assign(MR, 1.0);
815 spec.capacity.assign(MR, std::numeric_limits<double>::infinity());
816 spec.x0 = Matrix<double>(M, R, 0.0);
817 for (std::size_t r = 0; r < R; ++r) {
818 const double njobs = sn.classes[r].population;
820 const bool closed = std::isfinite(njobs);
825 for (std::size_t k = 0; k < K; ++k)
826 for (std::size_t i = 0; i < M; ++i)
827 if (spec.obsData(k, r * M + i) != unobs)
828 peak = std::max(peak, spec.obsData(k, r * M + i));
829 pop = std::max(1.0, 2.0 * peak);
831 for (std::size_t i = 0; i < M; ++i) {
832 spec.obsRange[r * M + i] = pop;
833 if (closed) spec.capacity[r * M + i] = pop;
835 if (closed && njobs > 0) {
836 const std::size_t ref = sn.classes[r].refstat;
837 spec.x0((ref >= 1 && ref <= M) ? ref - 1 : 0, r) = njobs;
842 spec.alpha0.assign(P, 0.0);
843 spec.beta0.assign(P, 0.0);
844 for (std::size_t i = 0; i < M; ++i) {
845 for (std::size_t r = 0; r < R; ++r) {
846 const std::size_t p = estimated[r * M + i];
855 const infer::VariationalResult<double> out =
861 Matrix<double> est(nodes.size(), R, 0.0);
862 for (std::size_t n = 0; n < nodes.size(); ++n) {
863 const std::size_t i = node_station[n];
864 for (std::size_t r = 0; r < R; ++r) {
865 const std::size_t p = estimated[r * M + i];
867 est(n, r) = out.mean_service_time[p - 1];
868 }
else if (sn.rates(i, r) > 0) {
869 est(n, r) = 1.0 / sn.rates(i, r);
876 Matrix<double> estimator_qmle(
const std::vector<std::size_t>& nodes) {
877 if (nodes.empty())
throw InputError(
"ParamEstimator.qmle: no stations were requested");
878 std::vector<double> population, think;
879 effective_population_and_think(population, think);
880 const std::size_t classes = population.size();
881 Matrix<double> qlen(nodes.size(), classes, 0.0);
882 for (std::size_t n = 0; n < nodes.size(); ++n) {
883 require_node(nodes[n],
"qmle");
884 for (std::size_t r = 0; r < classes; ++r) {
885 const std::vector<SampledMetric*> data =
get_qlen(nodes[n], r + 1);
886 if (data.empty() || data[0]->data.empty())
887 throw InputError(
"ParamEstimator.qmle: queue-length data is missing for node " +
888 std::to_string(nodes[n]) +
" class " +
889 std::to_string(r + 1));
891 for (
const double value : data[0]->data) sum += value;
892 qlen(n, r) = sum /
static_cast<double>(data[0]->data.size());
898 Matrix<double> estimator_gibbs(
const std::vector<std::size_t>& nodes) {
899 std::size_t node = 0, station = 0;
900 require_single_station(nodes,
"gibbs", node, station);
901 const qn::NetworkStruct<double>& sn = model_.get_struct();
902 std::vector<GibbsTrace<double>> traces(sn.classes.size());
903 for (std::size_t r = 0; r < sn.classes.size(); ++r) {
904 SampledMetric* arvr =
get_arvr(node, r + 1);
905 SampledMetric* respt =
get_respt(node, r + 1);
906 SampledMetric* tput =
get_tput(node, r + 1);
907 if (arvr ==
nullptr || respt ==
nullptr || tput ==
nullptr)
908 throw InputError(
"ParamEstimator.gibbs: arrival, response-time, and throughput "
909 "data are required for class " + std::to_string(r + 1));
910 if (!arvr->is_trace() || !respt->is_trace())
911 throw InputError(
"ParamEstimator.gibbs: arrival and response-time metrics must "
913 traces[r].arrival_ms.resize(arvr->data.size());
914 for (std::size_t i = 0; i < arvr->data.size(); ++i)
915 traces[r].arrival_ms[i] = arvr->data[i] * 1000.0;
916 traces[r].respt_s = respt->data;
917 traces[r].think_obs = tput->data;
922 const std::vector<double> demand =
923 infer_gibbs(traces, sn.stations[station - 1].nservers, gibbs, rng);
924 Matrix<double> out(1, demand.size(), 0.0);
925 for (std::size_t r = 0; r < demand.size(); ++r) out(0, r) = demand[r];
929 Matrix<double> estimator_mlps(
const std::vector<std::size_t>& nodes) {
930 std::size_t node = 0, station = 0;
931 require_single_station(nodes,
"mlps", node, station);
932 const qn::NetworkStruct<double>& sn = model_.get_struct();
934 throw InputError(
"ParamEstimator.mlps: the target station must use PS scheduling");
935 std::vector<double> population, think;
936 effective_population_and_think(population, think);
937 std::vector<double> rates(think.size(), 0.0);
938 for (std::size_t r = 0; r < think.size(); ++r) rates[r] = 1.0 / think[r];
939 const std::vector<double> demand =
941 mlps_samples(node,
"mlps"));
942 Matrix<double> out(1, demand.size(), 0.0);
943 for (std::size_t r = 0; r < demand.size(); ++r) out(0, r) = demand[r];
947 Matrix<double> estimator_fmlps(
const std::vector<std::size_t>& nodes) {
948 std::size_t node = 0, station = 0;
949 require_single_station(nodes,
"fmlps", node, station);
950 const qn::NetworkStruct<double>& sn = model_.get_struct();
952 throw InputError(
"ParamEstimator.fmlps: the target station must use PS scheduling");
953 std::size_t equivalent_queue = 0;
954 qn::Network<double> equivalent = build_closed_equivalent(node, equivalent_queue);
955 const qn::NetworkStruct<double>& eqsn = equivalent.get_struct();
956 const std::size_t eqstation = eqsn.nodes[equivalent_queue - 1].station;
957 const std::vector<double> demand =
959 Matrix<double> out(1, demand.size(), 0.0);
960 for (std::size_t r = 0; r < demand.size(); ++r) out(0, r) = demand[r];
964 Matrix<double> estimator_ubo(
const std::vector<std::size_t>& nodes) {
965 if (nodes.empty())
throw InputError(
"ParamEstimator.ubo: no stations were requested");
966 const qn::NetworkStruct<double>& sn = model_.get_struct();
967 const std::size_t stations = nodes.size(), classes = sn.classes.size();
968 std::size_t samples_count = 0;
969 std::vector<const SampledMetric*> util(stations,
nullptr);
970 std::vector<std::vector<const SampledMetric*>> arrivals(
971 stations, std::vector<const SampledMetric*>(classes,
nullptr));
972 std::vector<std::vector<const SampledMetric*>> response(
973 stations, std::vector<const SampledMetric*>(classes,
nullptr));
975 for (std::size_t i = 0; i < stations; ++i) {
976 require_node(nodes[i],
"ubo");
977 const std::size_t station = sn.nodes[nodes[i] - 1].station;
978 if (station == 0 || !std::isfinite(sn.stations[station - 1].nservers))
979 throw InputError(
"ParamEstimator.ubo: every target must be a finite-server station");
981 if (util[i] ==
nullptr)
982 throw InputError(
"ParamEstimator.ubo: aggregate utilization is missing for node " +
983 std::to_string(nodes[i]));
984 if (i == 0) samples_count = util[i]->data.size();
985 if (util[i]->data.size() != samples_count)
986 throw InputError(
"ParamEstimator.ubo: sampled metrics have different sample "
987 "counts; call interpolate first");
988 for (std::size_t r = 0; r < classes; ++r) {
989 arrivals[i][r] =
get_arvr(nodes[i], r + 1);
990 response[i][r] =
get_respt(nodes[i], r + 1);
991 if (arrivals[i][r] ==
nullptr || response[i][r] ==
nullptr)
992 throw InputError(
"ParamEstimator.ubo: arrival-rate and response-time data are "
993 "required for node " + std::to_string(nodes[i]) +
" class " +
994 std::to_string(r + 1));
995 if (arrivals[i][r]->data.size() != samples_count ||
996 response[i][r]->data.size() != samples_count)
997 throw InputError(
"ParamEstimator.ubo: sampled metrics have different sample "
998 "counts; call interpolate first");
1002 std::vector<std::size_t> valid;
1003 for (std::size_t n = 0; n < samples_count; ++n) {
1005 double total_arrival = 0.0;
1006 for (std::size_t i = 0; i < stations; ++i) {
1007 if (!std::isfinite(util[i]->data[n])) keep =
false;
1008 for (std::size_t r = 0; r < classes; ++r)
1009 total_arrival += arrivals[i][r]->data[n];
1011 if (keep && total_arrival > 0.0) valid.push_back(n);
1013 if (valid.empty())
throw InputError(
"ParamEstimator.ubo: no usable experiments remain");
1015 const std::size_t variables = stations * classes;
1016 Matrix<double> design(valid.size() * (classes + stations), variables, 0.0);
1017 std::vector<double> target(design.rows(), 0.0);
1018 std::size_t row = 0;
1019 for (
const std::size_t n : valid) {
1020 std::vector<double> rho(stations, 0.0), beta(stations, 0.0);
1021 std::vector<double> lambda_class(classes, 0.0), end_to_end(classes, 0.0);
1022 for (std::size_t i = 0; i < stations; ++i) {
1023 const std::size_t station = sn.nodes[nodes[i] - 1].station;
1024 rho[i] = util[i]->data[n] * sn.stations[station - 1].nservers;
1026 throw NumericError(ErrorKind{
"Instability"},
"ParamEstimator.ubo: utilization reaches one");
1027 beta[i] = 1.0 / (1.0 - rho[i]);
1028 for (std::size_t r = 0; r < classes; ++r) {
1029 lambda_class[r] += arrivals[i][r]->data[n];
1030 end_to_end[r] += response[i][r]->data[n];
1033 double total_lambda = 0.0;
1034 for (
const double value : lambda_class) total_lambda += value;
1035 for (std::size_t r = 0; r < classes; ++r) {
1036 const double weight = std::sqrt(lambda_class[r] / total_lambda);
1037 for (std::size_t i = 0; i < stations; ++i)
1038 design(row, r * stations + i) = weight * beta[i];
1039 target[row++] = weight * end_to_end[r];
1041 for (std::size_t i = 0; i < stations; ++i) {
1042 for (std::size_t r = 0; r < classes; ++r)
1043 design(row, r * stations + i) = arrivals[i][r]->data[n];
1044 target[row++] = rho[i];
1047 const std::vector<double> fit = detail::nnls(design, target);
1048 Matrix<double> out(stations, classes, 0.0);
1049 for (std::size_t r = 0; r < classes; ++r)
1050 for (std::size_t i = 0; i < stations; ++i)
1051 out(i, r) = fit[r * stations + i];
1055 Matrix<double> estimator_erps(
const std::vector<std::size_t>& nodes) {
1056 std::size_t node = 0, station = 0;
1057 require_single_station(nodes,
"erps", node, station);
1058 const qn::NetworkStruct<double>& sn = model_.get_struct();
1060 throw InputError(
"ParamEstimator.erps: the target station must use PS scheduling");
1061 const std::size_t classes = sn.classes.size();
1062 std::vector<const SampledMetric*> response(classes,
nullptr), qlen(classes,
nullptr);
1063 double busy_sum = 0.0;
1064 std::size_t busy_count = 0;
1065 for (std::size_t r = 0; r < classes; ++r) {
1069 if (response[r] ==
nullptr || qlen[r] ==
nullptr)
1070 throw InputError(
"ParamEstimator.erps: response-time and arrival-conditional "
1071 "aggregate queue-length data are required for class " +
1072 std::to_string(r + 1));
1073 if (response[r]->data.size() != qlen[r]->data.size())
1074 throw InputError(
"ParamEstimator.erps: sampled metrics have different sample "
1075 "counts; call interpolate first");
1076 for (
const double q : qlen[r]->data) {
1078 throw InputError(
"ParamEstimator.erps: an arrival queue length must include "
1079 "the arriving job");
1084 if (busy_count == 0)
throw InputError(
"ParamEstimator.erps: no observations");
1086 std::min(busy_sum /
static_cast<double>(busy_count),
1087 sn.stations[station - 1].nservers);
1088 if (!(busy > 0.0))
throw NumericError(ErrorKind{
"Degenerate"},
"ParamEstimator.erps: zero average busy cores");
1089 Matrix<double> out(1, classes, 0.0);
1090 for (std::size_t r = 0; r < classes; ++r) {
1091 double aa = 0.0, ab = 0.0;
1092 for (std::size_t i = 0; i < qlen[r]->data.size(); ++i) {
1093 const double regressor = qlen[r]->data[i] / busy;
1094 aa += regressor * regressor;
1095 ab += regressor * response[r]->data[i];
1097 if (!(aa > 0.0))
throw NumericError(ErrorKind{
"Degenerate"},
"ParamEstimator.erps: zero queue-length regressor");
1098 out(0, r) = std::max(0.0, ab / aa);
1103 std::vector<double> solver_measurement(
const qn::NetworkStruct<double>& base,
1104 std::size_t station,
1105 const std::vector<double>& demand) {
1106 qn::NetworkStruct<double> sn = base;
1107 for (std::size_t r = 0; r < demand.size(); ++r) {
1108 if (!(demand[r] > 0.0) || !std::isfinite(demand[r]))
1109 throw NumericError(ErrorKind{
"Degenerate"},
"ParamEstimator: a solver-backed estimate reached a "
1110 "non-positive service demand");
1114 mva::MvaOptions solver_options;
1115 solver_options.method =
"default";
1116 const mva::AvgResult<double> solved =
1118 std::vector<double> measurement(demand.size() + 1, 0.0);
1119 for (std::size_t r = 0; r < demand.size(); ++r) {
1120 measurement[r] = solved.RN(station - 1, r);
1121 measurement.back() += solved.UN(station - 1, r);
1126 Matrix<double> estimator_ekf(
const std::vector<std::size_t>& nodes) {
1127 std::size_t node = 0, station = 0;
1128 require_single_station(nodes,
"ekf", node, station);
1129 const qn::NetworkStruct<double>& sn = model_.get_struct();
1130 const double servers = sn.stations[station - 1].nservers;
1131 if (!std::isfinite(servers))
1132 throw InputError(
"ParamEstimator.ekf: the target station must have finite servers");
1134 if (aggregate ==
nullptr)
1135 throw InputError(
"ParamEstimator.ekf: aggregate utilization data is missing");
1136 const std::size_t classes = sn.classes.size(), count =
aggregate->data.size();
1137 std::vector<const SampledMetric*> arrivals(classes,
nullptr), response(classes,
nullptr);
1138 for (std::size_t r = 0; r < classes; ++r) {
1139 arrivals[r] =
get_arvr(node, r + 1);
1141 if (arrivals[r] ==
nullptr || response[r] ==
nullptr)
1142 throw InputError(
"ParamEstimator.ekf: arrival-rate and response-time data are "
1143 "required for class " + std::to_string(r + 1));
1144 if (arrivals[r]->data.size() != count || response[r]->data.size() != count)
1145 throw InputError(
"ParamEstimator.ekf: sampled metrics have different sample "
1146 "counts; call interpolate first");
1149 std::vector<std::size_t> valid;
1150 for (std::size_t n = 0; n < count; ++n) {
1151 double throughput = 0.0;
1152 for (std::size_t r = 0; r < classes; ++r) throughput += arrivals[r]->data[n];
1153 if (std::isfinite(
aggregate->data[n]) && throughput != 0.0) valid.push_back(n);
1155 if (valid.empty())
throw InputError(
"ParamEstimator.ekf: no usable experiments remain");
1157 std::vector<double> x(classes, 0.0);
1160 throw InputError(
"ParamEstimator.ekf: x0 must contain one demand per class");
1163 throw InputError(
"ParamEstimator.ekf: x0 must contain one positive demand per class");
1167 for (std::size_t r = 0; r < classes; ++r) {
1168 double maximum = 0.0;
1169 for (
const double value : response[r]->data) maximum = std::max(maximum, value);
1171 if (!(x[r] > 0.0)) x[r] = std::max(1e-9, maximum * 0.5);
1175 Matrix<double> covariance(classes, classes, 0.0);
1176 for (std::size_t r = 0; r < classes; ++r) covariance(r, r) = x[r] * x[r];
1177 const double delta = 1e-6;
1178 const std::size_t iterations = std::min(valid.size(),
options.
iter_max);
1179 for (std::size_t step = 0; step < iterations; ++step) {
1180 const std::size_t n = valid[step];
1181 Matrix<double> predicted_cov = covariance;
1182 for (std::size_t r = 0; r < classes; ++r) predicted_cov(r, r) += 0.001;
1183 const std::vector<double> predicted = solver_measurement(sn, station, x);
1185 Matrix<double> jacobian(classes + 1, classes, 0.0);
1186 for (std::size_t c = 0; c < classes; ++c) {
1187 std::vector<double> perturbed(x);
1188 perturbed[c] +=
delta;
1189 const std::vector<double> moved = solver_measurement(sn, station, perturbed);
1190 for (std::size_t k = 0; k < classes + 1; ++k)
1191 jacobian(k, c) = (moved[k] - predicted[k]) / delta;
1194 Matrix<double> innovation_cov(classes + 1, classes + 1, 0.0);
1195 for (std::size_t i = 0; i < classes + 1; ++i)
1196 for (std::size_t j = 0; j < classes + 1; ++j) {
1197 double value = i == j ? 0.01 : 0.0;
1198 for (std::size_t a = 0; a < classes; ++a)
1199 for (std::size_t b = 0; b < classes; ++b)
1200 value += jacobian(i, a) * predicted_cov(a, b) * jacobian(j, b);
1201 innovation_cov(i, j) = value;
1204 Matrix<double> gain(classes, classes + 1, 0.0);
1205 for (std::size_t r = 0; r < classes; ++r) {
1206 std::vector<double> rhs(classes + 1, 0.0);
1207 for (std::size_t k = 0; k < classes + 1; ++k)
1208 for (std::size_t a = 0; a < classes; ++a)
1209 rhs[k] += predicted_cov(r, a) * jacobian(k, a);
1210 const std::vector<double> solved =
solve(innovation_cov, rhs);
1211 for (std::size_t k = 0; k < classes + 1; ++k) gain(r, k) = solved[k];
1214 std::vector<double> residual(classes + 1, 0.0);
1215 for (std::size_t r = 0; r < classes; ++r)
1216 residual[r] = response[r]->data[n] - predicted[r];
1217 residual.back() =
aggregate->data[n] * servers - predicted.back();
1218 for (std::size_t r = 0; r < classes; ++r) {
1219 double update = x[r];
1220 for (std::size_t k = 0; k < classes + 1; ++k)
1221 update += gain(r, k) * residual[k];
1223 x[r] = update > 0.0 ? update : 0.4 * x[r];
1226 Matrix<double> next(classes, classes, 0.0);
1227 for (std::size_t i = 0; i < classes; ++i)
1228 for (std::size_t j = 0; j < classes; ++j) {
1230 for (std::size_t a = 0; a < classes; ++a) {
1231 double ikh = i == a ? 1.0 : 0.0;
1232 for (std::size_t k = 0; k < classes + 1; ++k)
1233 ikh -= gain(i, k) * jacobian(k, a);
1234 value += ikh * predicted_cov(a, j);
1240 Matrix<double> out(1, classes, 0.0);
1241 for (std::size_t r = 0; r < classes; ++r) out(0, r) = x[r];
1245 Matrix<double> estimator_mcmc(
const std::vector<std::size_t>& nodes) {
1246 if (nodes.empty())
throw InputError(
"ParamEstimator.mcmc: no stations were requested");
1247 const qn::NetworkStruct<double>& sn = model_.get_struct();
1248 const std::size_t classes = sn.classes.size();
1249 std::vector<double> population, think;
1250 effective_population_and_think(population, think);
1254 Matrix<double> average(nodes.size(), classes, 0.0);
1255 std::size_t experiments = 0;
1256 auto accumulate = [&](
const SampledMetric* metric, std::size_t i, std::size_t r,
1258 if (experiments == 0)
1259 experiments = metric->data.size();
1260 else if (metric->data.size() != experiments)
1261 throw InputError(
"ParamEstimator.mcmc: sampled metrics have different sample "
1262 "counts; call interpolate first");
1264 for (
const double value : metric->data) {
1265 if (!(value >= 0.0) || !std::isfinite(value))
1266 throw InputError(
"ParamEstimator.mcmc: queue lengths must be finite and "
1270 average(i, r) = sum / share /
static_cast<double>(experiments);
1272 for (std::size_t i = 0; i < nodes.size(); ++i) {
1273 require_node(nodes[i],
"mcmc");
1274 std::vector<const SampledMetric*> per_class(classes,
nullptr);
1275 bool have_per_class =
true;
1276 for (std::size_t r = 0; r < classes && have_per_class; ++r) {
1277 const std::vector<SampledMetric*> found =
get_qlen(nodes[i], r + 1);
1278 if (found.size() != 1 || found[0]->data.empty())
1279 have_per_class =
false;
1281 per_class[r] = found[0];
1283 if (have_per_class) {
1284 for (std::size_t r = 0; r < classes; ++r) accumulate(per_class[r], i, r, 1.0);
1287 if (qlen ==
nullptr || qlen->data.empty())
1288 throw InputError(
"ParamEstimator.mcmc: queue-length data is missing for "
1289 "node " + std::to_string(nodes[i]));
1290 for (std::size_t r = 0; r < classes; ++r)
1291 accumulate(qlen, i, r,
static_cast<double>(classes));
1298 for (std::size_t i = 0; i < average.rows(); ++i)
1299 for (std::size_t r = 0; r < average.cols(); ++r)
1300 upper = std::max(upper, average(i, r));
1301 for (std::size_t r = 0; r < classes; ++r) {
1302 double column = 0.0;
1303 for (std::size_t i = 0; i < nodes.size(); ++i) column += average(i, r);
1304 if (think[r] > 0.0 && population[r] > column)
1305 upper = std::max(upper, think[r] / (population[r] - column));
1308 throw InputError(
"ParamEstimator.mcmc: at least one mean queue length must be positive");
1310 std::vector<int> pop(classes, 0);
1311 double lattice = 1.0;
1312 Matrix<double> thinkz(1, classes, 0.0);
1313 for (std::size_t r = 0; r < classes; ++r) {
1314 pop[r] =
static_cast<int>(std::lround(population[r]));
1315 lattice *=
static_cast<double>(pop[r] + 1);
1316 thinkz(0, r) = think[r];
1319 const bool exact_g = lattice <= 1e4;
1323 const std::size_t samples_count = 100, grid_count = 400;
1324 const double step =
upper /
static_cast<double>(grid_count);
1325 const double log_prior = std::log(1.0 /
static_cast<double>(grid_count));
1326 const double nexp =
static_cast<double>(experiments);
1327 std::vector<Matrix<double>> theta(samples_count + 1,
1328 Matrix<double>(nodes.size(), classes, 0.0));
1330 std::vector<double> log_post(grid_count + 1, 0.0);
1331 for (std::size_t s = 0; s < samples_count; ++s) {
1332 Matrix<double> sample = theta[s];
1333 for (std::size_t i = 0; i < nodes.size(); ++i) {
1334 for (std::size_t r = 0; r < classes; ++r) {
1335 double best = -std::numeric_limits<double>::infinity();
1336 for (std::size_t k = 0; k <= grid_count; ++k) {
1337 const double candidate = step *
static_cast<double>(k);
1338 Matrix<double> trial = sample;
1339 trial(i, r) = candidate;
1340 const double lG = exact_g
1345 const double loglik = average(i, r) == 0.0
1347 : nexp * average(i, r) * std::log(candidate);
1348 log_post[k] = loglik - nexp * lG + log_prior;
1349 best = std::max(best, log_post[k]);
1352 std::vector<double> weights(grid_count + 1, 0.0);
1353 for (std::size_t k = 0; k <= grid_count; ++k) {
1354 weights[k] = std::isfinite(log_post[k]) ? std::exp(log_post[k] - best) : 0.0;
1355 total += weights[k];
1358 std::size_t picked = grid_count;
1359 for (std::size_t k = 0; k <= grid_count; ++k) {
1366 sample(i, r) = step *
static_cast<double>(picked);
1369 theta[s + 1] = sample;
1374 Matrix<double> out(nodes.size(), classes, 0.0);
1375 const std::size_t cutoff = samples_count / 2 - 1;
1376 for (std::size_t i = 0; i < nodes.size(); ++i) {
1377 for (std::size_t r = 0; r < classes; ++r) {
1379 for (std::size_t c = 0; c < sn.chains.size(); ++c) {
1380 if (r >= sn.chains[c].size() || !sn.chains[c][r] ||
1381 c >= sn.nodevisits.size())
1383 if (nodes[i] - 1 < sn.nodevisits[c].rows() &&
1384 r < sn.nodevisits[c].cols()) {
1385 const double candidate = sn.nodevisits[c](nodes[i] - 1, r);
1386 if (candidate > 0.0) visit = candidate;
1390 for (std::size_t s = cutoff; s <= samples_count; ++s)
1391 out(i, r) += theta[s](i, r) / visit;
1392 out(i, r) /=
static_cast<double>(samples_count - cutoff + 1);
1398 Matrix<double> estimator_mle(
const std::vector<std::size_t>& nodes) {
1399 std::size_t node = 0, station = 0;
1400 require_single_station(nodes,
"mle", node, station);
1401 const qn::NetworkStruct<double>& sn = model_.get_struct();
1402 const double servers = sn.stations[station - 1].nservers;
1403 if (!std::isfinite(servers))
1404 throw InputError(
"ParamEstimator.mle: the target station must have finite servers");
1406 if (aggregate ==
nullptr)
1407 throw InputError(
"ParamEstimator.mle: aggregate utilization data is missing");
1408 const std::size_t classes = sn.classes.size(), count =
aggregate->data.size();
1409 std::vector<const SampledMetric*> arrivals(classes,
nullptr), response(classes,
nullptr);
1410 std::vector<double>
upper(classes, 0.0);
1411 for (std::size_t r = 0; r < classes; ++r) {
1412 arrivals[r] =
get_arvr(node, r + 1);
1414 if (arrivals[r] ==
nullptr || response[r] ==
nullptr)
1415 throw InputError(
"ParamEstimator.mle: arrival-rate and response-time data are "
1416 "required for class " + std::to_string(r + 1));
1417 if (arrivals[r]->data.size() != count || response[r]->data.size() != count)
1418 throw InputError(
"ParamEstimator.mle: sampled metrics have different sample "
1419 "counts; call interpolate first");
1420 for (
const double value : response[r]->data)
1421 if (std::isfinite(value))
upper[r] = std::max(upper[r], value);
1422 if (!(upper[r] >= 1e-8))
1423 throw InputError(
"ParamEstimator.mle: response-time upper bounds must be positive");
1426 std::vector<std::size_t> valid;
1427 for (std::size_t n = 0; n < count; ++n) {
1428 double throughput = 0.0;
1429 for (std::size_t r = 0; r < classes; ++r) throughput += arrivals[r]->data[n];
1430 if (std::isfinite(
aggregate->data[n]) && throughput > 0.0) valid.push_back(n);
1432 if (valid.empty())
throw InputError(
"ParamEstimator.mle: no usable experiments remain");
1434 std::vector<double> x0(classes, 0.0);
1437 throw InputError(
"ParamEstimator.mle: x0 must contain one demand per class");
1441 for (std::size_t r = 0; r < classes; ++r)
1445 std::vector<Bound<double>> bounds(classes);
1446 for (std::size_t r = 0; r < classes; ++r) bounds[r] =
bound_box(1e-8, upper[r]);
1447 const auto objective = [&](
const std::vector<double>& x) {
1448 const std::vector<double> predicted = solver_measurement(sn, station, x);
1450 for (
const std::size_t n : valid) {
1452 for (std::size_t r = 0; r < classes; ++r) total += arrivals[r]->data[n];
1453 for (std::size_t r = 0; r < classes; ++r) {
1454 const double weight = arrivals[r]->data[n] / total;
1455 const double residual = predicted[r] - response[r]->data[n];
1456 value += weight * residual * residual;
1458 const double residual = predicted.back() -
aggregate->data[n] * servers;
1459 value += residual * residual;
1464 nm.max_iter =
static_cast<unsigned>(std::min<std::size_t>(
1466 nm.max_eval = std::max<unsigned>(1000u, nm.max_iter *
1467 static_cast<unsigned>(10 * (classes + 1)));
1468 const NelderMeadResult<double> fit =
nelder_mead_box(objective, x0, bounds, nm);
1469 Matrix<double> out(1, classes, 0.0);
1470 for (std::size_t r = 0; r < classes; ++r) out(0, r) = fit.x[r];
1474 Matrix<double> estimator_ubr(
const std::vector<std::size_t>& nodes) {
1475 if (nodes.size() != 1)
1476 throw InputError(
"ParamEstimator.ubr: the estimator accepts exactly one station");
1477 const std::size_t node = nodes[0];
1478 require_node(node,
"ubr");
1479 const qn::NetworkStruct<double>& sn = model_.raw_struct();
1480 const std::size_t station = sn.nodes[node - 1].station;
1482 throw InputError(
"ParamEstimator.ubr: the target node is not a station");
1483 const double servers = sn.stations[station - 1].nservers;
1484 if (!std::isfinite(servers))
1485 throw InputError(
"ParamEstimator.ubr: the target station must have finite servers");
1486 const std::size_t classes = sn.classes.size();
1488 std::vector<const SampledMetric*> arrivals(classes,
nullptr), utils(classes,
nullptr);
1489 std::size_t count = 0;
1490 for (std::size_t r = 0; r < classes; ++r) {
1491 arrivals[r] =
get_arvr(node, r + 1);
1492 if (arrivals[r] ==
nullptr)
1493 throw InputError(
"ParamEstimator.ubr: arrival-rate data is missing for class " +
1494 std::to_string(r + 1));
1496 count = arrivals[r]->data.size();
1497 else if (arrivals[r]->data.size() != count)
1499 "ParamEstimator.ubr: sampled metrics have different sample counts; call "
1500 "interpolate first");
1502 if (utils[r] !=
nullptr && utils[r]->data.size() != count)
1504 "ParamEstimator.ubr: sampled metrics have different sample counts; call "
1505 "interpolate first");
1508 Matrix<double> estimates(1, classes, 0.0);
1509 std::vector<std::size_t> unknown;
1510 std::vector<double> known_busy(count, 0.0);
1511 for (std::size_t r = 0; r < classes; ++r) {
1512 if (utils[r] ==
nullptr) {
1513 unknown.push_back(r);
1516 double aa = 0.0, au = 0.0;
1517 for (std::size_t i = 0; i < count; ++i) {
1518 const double busy = utils[r]->data[i] * servers;
1519 aa += arrivals[r]->data[i] * arrivals[r]->data[i];
1520 au += arrivals[r]->data[i] * busy;
1521 known_busy[i] += busy;
1524 throw NumericError(ErrorKind{
"Degenerate"},
"ParamEstimator.ubr: a class has zero arrival-rate regressor");
1525 estimates(0, r) = std::max(0.0, au / aa);
1528 if (!unknown.empty()) {
1530 if (aggregate ==
nullptr)
1532 "ParamEstimator.ubr: aggregate utilization is required for classes without "
1533 "per-class utilization");
1536 "ParamEstimator.ubr: sampled metrics have different sample counts; call "
1537 "interpolate first");
1538 Matrix<double> A(count, unknown.size(), 0.0);
1539 std::vector<double> residual(count, 0.0);
1540 for (std::size_t i = 0; i < count; ++i) {
1541 residual[i] =
aggregate->data[i] * servers - known_busy[i];
1542 for (std::size_t j = 0; j < unknown.size(); ++j)
1543 A(i, j) = arrivals[unknown[j]]->data[i];
1545 const std::vector<double> fit = detail::nnls(A, residual);
1546 for (std::size_t j = 0; j < unknown.size(); ++j) estimates(0, unknown[j]) = fit[j];