5#ifndef LINE_SOLVERS_FLUID_FLUID_MOMENTS_H
6#define LINE_SOLVERS_FLUID_FLUID_MOMENTS_H
99 const double eps = std::numeric_limits<double>::epsilon();
100 const double s1 = f.
s.empty() ? 0.0 : f.
s[0];
101 const double tol =
static_cast<double>(std::max(A.
rows(), A.
cols())) * eps * s1;
102 std::size_t keep = 0;
103 for (std::size_t i = 0; i < f.
s.size(); ++i)
104 if (f.
s[i] > tol) ++keep;
106 for (std::size_t j = 0; j < keep; ++j)
107 for (std::size_t i = 0; i < A.
rows(); ++i) V(i, j) = f.
U(i, j);
113 for (std::size_t i = 0; i < M.
rows(); ++i)
114 for (std::size_t j = i + 1; j < M.
cols(); ++j) {
115 const double v = 0.5 * (M(i, j) + M(j, i));
133 for (std::size_t i = 0; i < b.
sigma2.size(); ++i) {
134 const double ai = i < a.
sigma2.size() ? a.
sigma2[i] : 0.0;
138 for (std::size_t i = 0; i < b.
cov.size(); ++i) {
142 if (ai.
rows() == 0 && bi.
rows() == 0)
continue;
143 if (ai.
rows() == 0) {
145 for (std::size_t r = 0; r < bi.
rows(); ++r)
146 for (std::size_t c = 0; c < bi.
cols(); ++c) m(r, c) = step * bi(r, c);
148 }
else if (bi.
rows() == 0) {
150 for (std::size_t r = 0; r < ai.
rows(); ++r)
151 for (std::size_t c = 0; c < ai.
cols(); ++c) m(r, c) = (1.0 - step) * ai(r, c);
155 for (std::size_t r = 0; r < bi.
rows(); ++r)
156 for (std::size_t c = 0; c < bi.
cols(); ++c)
157 m(r, c) = ai(r, c) + step * (bi(r, c) - ai(r, c));
180 if (tol < 0.0) tol = std::sqrt(std::numeric_limits<double>::epsilon());
181 const std::size_t n = A.
rows();
185 info.
max_real_eig = -std::numeric_limits<double>::infinity();
193 detail::fluid_symmetrize(Qr);
195 const std::vector<std::complex<double>> ev =
eig_values(Ar);
196 double max_re = -std::numeric_limits<double>::infinity();
197 for (std::size_t i = 0; i < ev.size(); ++i) max_re = std::max(max_re, ev[i].real());
200 info.
stable = max_re < -tol;
203 "fluid_lyapunov: the fluid fixed point is not exponentially stable on the reachable "
204 "subspace (largest Jacobian eigenvalue has real part " +
205 std::to_string(max_re) +
206 "), so the linear noise approximation has no stationary covariance. This happens at an "
207 "unstable model or at a drift kink; use method 'closing' for the mean only");
211 for (std::size_t i = 0; i < Qr.
rows(); ++i)
212 for (std::size_t j = 0; j < Qr.
cols(); ++j) negQr(i, j) = -Qr(i, j);
214 detail::fluid_symmetrize(W);
216 detail::fluid_symmetrize(Sigma);
267 std::vector<double>
S;
285 return t.
absorb(s, s) == 0.0;
290 const std::size_t M =
sn.nstations, K =
sn.nclasses;
321 for (std::size_t r = 0; r < K; ++r)
322 if (std::isfinite(
sn.classes[r].population)) npop +=
sn.classes[r].population;
324 t.
is_ext.assign(M,
false);
325 for (std::size_t i = 0; i < M; ++i) {
326 const double c =
sn.stations[i].nservers;
327 t.
S[i] = std::isfinite(c) ? c : std::max(npop, 1.0);
333 t.
class_block.assign(M, std::vector<std::vector<std::size_t>>(K));
334 std::vector<bool> keep(t.
nstate,
true);
335 for (std::size_t i = 0; i < M; ++i) {
336 for (std::size_t r = 0; r < K; ++r) {
337 for (std::size_t k = 0; k < L.
kic[i][r]; ++k) {
341 if (!t.
is_ext[i] || L.
kic[i][r] == 0)
continue;
344 "fluid_moment_terms: the moment-closure methods need a Poisson arrival stream, "
345 "but the source of class " +
346 std::to_string(r + 1) +
" is a " + std::to_string(L.
kic[i][r]) +
347 "-phase process. Those coordinates track the phase of a single arrival process "
348 "rather than a population, so they carry no linear noise approximation. Use an "
349 "exponential inter-arrival time, or method 'matrix'");
350 for (std::size_t k = 0; k < L.
kic[i][r]; ++k) keep[L.
qidx[i][r] + k] =
false;
353 for (std::size_t a = 0; a < t.
nstate; ++a)
354 if (keep[a]) t.
cov_idx.push_back(a);
371 if (!
sn.chains.empty()) {
372 for (std::size_t i = 0; i < M; ++i) {
375 !std::isfinite(
sn.stations[i].nservers))
377 const std::size_t isf =
sn.stateful_of_station(i + 1) - 1;
379 bool infinite =
false;
380 std::vector<bool> covered(K,
false);
381 for (std::size_t ch = 0; ch <
sn.chains.size(); ++ch) {
384 bool pop_inf =
false;
385 const bool has_vis = ch <
sn.visits.size() &&
sn.visits[ch].rows() > isf;
386 for (std::size_t r = 0; r < K; ++r) {
387 if (!
sn.chains[ch][r])
continue;
389 const double n =
sn.classes[r].population;
390 if (std::isfinite(n)) pop += n;
else pop_inf =
true;
397 if (pop_inf) infinite =
true;
401 bool uncovered =
false;
402 for (std::size_t r = 0; r < K; ++r)
403 if (t.
sys.
layout.
kic[i][r] > 0 && !covered[r]) uncovered =
true;
404 if (uncovered)
continue;
405 t.
min_exact[i] = !infinite && bound <=
sn.stations[i].nservers +
415 std::vector<std::size_t> coord_station(t.
nstate, 0), coord_class(t.
nstate, 0);
416 for (std::size_t i = 0; i < M; ++i)
417 for (std::size_t r = 0; r < K; ++r)
418 for (std::size_t k = 0; k < L.
kic[i][r]; ++k) {
419 coord_station[L.
qidx[i][r] + k] = i;
420 coord_class[L.
qidx[i][r] + k] = r;
424 const std::size_t nev0 = sys0.
events.size();
428 for (std::size_t e = 0; e < nev0; ++e) {
435 for (std::size_t e = 0; e < t.
sys.
events.size() && e < nev0; ++e) t.
emap(e, e) = 1.0;
442 const std::vector<double>& x,
446 std::vector<double> g = x;
453 const std::vector<double>& x,
456 std::vector<double> r(t.
sys.
events.size(), 0.0);
457 for (std::size_t e = 0; e < r.size(); ++e)
464 const std::vector<double>& x,
467 std::vector<double> f(t.
nstate, 0.0);
468 for (std::size_t e = 0; e < r.size(); ++e) {
469 if (r[e] == 0.0)
continue;
491 const std::size_t M = L.
qidx.size();
492 const std::size_t K = M ? L.
qidx[0].size() : 0;
493 const std::size_t n = t.
nstate;
494 const bool gaussian = cl.
gaussian();
497 const auto zero_rows = [&](
const std::vector<std::size_t>& rows) {
498 for (std::size_t a = 0; a < rows.size(); ++a)
499 for (std::size_t j = 0; j < n; ++j) G(rows[a], j) = 0.0;
502 for (std::size_t i = 0; i < M; ++i) {
503 const std::vector<double>& lld = sys.
lld[i];
507 const std::size_t nb = blk.
size();
508 if (nb == 0)
continue;
510 for (std::size_t a = 0; a < nb; ++a) ni += x[blk[a]];
512 switch (sys.
sched[i]) {
514 if (lld.empty() || !(ni > 0.0))
break;
516 const double f = h.
h / ni, fp = (ni * h.
dh - h.
h) / (ni * ni);
518 for (std::size_t a = 0; a < nb; ++a)
519 for (std::size_t b = 0; b < nb; ++b)
520 G(blk[a], blk[b]) = (a == b ? f : 0.0) + x[blk[a]] * fp;
524 for (std::size_t r = 0; r < K; ++r) {
525 if (!L.
enabled[i][r])
continue;
526 const std::size_t b = L.
qidx[i][r], nn = L.
kic[i][r];
527 for (std::size_t j = 0; j < n; ++j) G(b, j) = 0.0;
528 for (std::size_t p = 1; p < nn; ++p) G(b, b + p) = -1.0;
534 if (!(ni > 0.0))
break;
535 double h = 0.0, dh = 0.0;
536 if (gaussian || !lld.empty()) {
546 std::vector<double> xb(nb, 0.0), wv(nb, 1.0);
547 for (std::size_t a = 0; a < nb; ++a) xb[a] = x[blk[a]];
550 for (std::size_t a = 0; a < nb; ++a)
551 for (std::size_t b = 0; b < nb; ++b)
552 G(blk[a], blk[b]) = sh.
ds(a, b) * h + sh.
s[a] * dh +
553 dh * sh.
dcn(a, b) + sh.
cn[a] * d2h;
573 const double f = h / ni, fp = (ni * dh - h) / (ni * ni);
575 for (std::size_t a = 0; a < nb; ++a)
576 for (std::size_t b = 0; b < nb; ++b)
577 G(blk[a], blk[b]) = (a == b ? f : 0.0) + x[blk[a]] * fp;
582 for (std::size_t r = 0; r < K; ++r) wsum += sys.
weight[i][r];
583 if (wsum <= 0.0)
break;
584 std::vector<double> wv(nb, 0.0), xb(nb, 0.0);
585 for (std::size_t r = 0; r < K; ++r) {
586 if (!L.
enabled[i][r])
continue;
587 const std::size_t b = L.
qidx[i][r] - blk[0];
588 for (std::size_t p = 0; p < L.
kic[i][r]; ++p)
589 wv[b + p] = sys.
weight[i][r] / wsum;
592 for (std::size_t a = 0; a < nb; ++a) {
596 if (!(ni > 0.0) || !(wx > 0.0))
break;
601 for (std::size_t a = 0; a < nb; ++a)
602 for (std::size_t b = 0; b < nb; ++b)
603 G(blk[a], blk[b]) = sh.
ds(a, b) * psi.
h + sh.
s[a] * psi.
dh +
610 "fluid_drift_jacobian: multi-server GPS stations are not supported, as in "
612 std::vector<double> xk(K, 0.0), vk(K, 0.0), wk(K, 0.0);
613 for (std::size_t r = 0; r < K; ++r) {
615 const std::vector<std::size_t>& bk = t.
class_block[i][r];
616 for (std::size_t a = 0; a < bk.size(); ++a) xk[r] += x[bk[a]];
617 if (Ci ==
nullptr)
continue;
619 for (std::size_t a = 0; a < bk.size(); ++a)
620 for (std::size_t b = 0; b < bk.size(); ++b)
621 v += (*Ci)(bk[a] - blk[0], bk[b] - blk[0]);
622 vk[r] = std::max(0.0, v);
634 for (std::size_t r = 0; r < K; ++r) {
635 const std::vector<std::size_t>& bk = t.
class_block[i][r];
636 if (bk.empty() || !(xk[r] > 0.0))
continue;
637 for (std::size_t a = 0; a < bk.size(); ++a) {
638 for (std::size_t b = 0; b < bk.size(); ++b)
639 G(bk[a], bk[b]) += (a == b ? sk.
s[r] * a1.
h / xk[r] : 0.0) -
640 (sk.
s[r] * a1.
h / (xk[r] * xk[r])) * x[bk[a]];
641 for (std::size_t m = 0; m < K; ++m) {
642 const std::vector<std::size_t>& bm = t.
class_block[i][m];
643 for (std::size_t b = 0; b < bm.size(); ++b)
644 G(bk[a], bm[b]) += (a1.
h * sk.
ds(r, m) / xk[r]) * x[bk[a]];
647 for (std::size_t b = 0; b < nb; ++b)
648 G(bk[a], blk[b]) += (sk.
s[r] * a1.
dh / xk[r]) * x[bk[a]];
662 for (std::size_t e = 0; e < sys.
events.size(); ++e) {
665 for (std::size_t j = 0; j < n; ++j) {
667 if (v == 0.0)
continue;
693 const std::vector<double>& x,
695 std::vector<std::size_t> out;
696 for (std::size_t i = 0; i < cl.
sigma2.size(); ++i)
697 if (cl.
sigma2[i] > 0.0)
return out;
698 const double tol = std::sqrt(std::numeric_limits<double>::epsilon());
702 if (!t.
sys.
lld[i].empty())
continue;
703 const double c = t.
S[i];
704 if (!std::isfinite(c) || c <= 0.0)
continue;
706 if (blk.
empty())
continue;
708 for (std::size_t a = 0; a < blk.
size(); ++a) ni += x[blk[a]];
709 if (!(ni > 0.0))
continue;
710 if (std::fabs(ni - c) <= tol * std::max(1.0, c)) out.push_back(i);
721 const std::vector<double>& x,
722 const std::vector<std::size_t>& kink,
double rel) {
723 std::vector<double> y = x;
724 for (std::size_t k = 0; k < kink.size(); ++k) {
725 const std::vector<std::size_t>& blk = t.
station_block[kink[k]];
727 for (std::size_t a = 0; a < blk.
size(); ++a) ni += y[blk[a]];
728 if (!(ni > 0.0))
continue;
729 const double scale = t.
S[kink[k]] * (1.0 + rel) / ni;
730 for (std::size_t a = 0; a < blk.
size(); ++a) y[blk[a]] *= scale;
745 const std::vector<double>& r,
747 const std::size_t
nc = t.
cov_idx.size(), nev = r.size();
749 for (std::size_t a = 0; a <
nc; ++a)
750 for (std::size_t e = 0; e < nev; ++e) Dc(a, e) = t.
D(t.
cov_idx[a], e);
756 if (clampT && clampT->rows() ==
nc && clampT->cols() ==
nc) {
758 for (std::size_t a = 0; a <
nc; ++a)
759 for (std::size_t e = 0; e < nev; ++e) {
761 for (std::size_t b = 0; b <
nc; ++b) acc += (*clampT)(a, b) * Dc(b, e);
767 for (std::size_t a = 0; a <
nc; ++a)
768 for (std::size_t b = 0; b <
nc; ++b) {
770 for (std::size_t e = 0; e < nev; ++e) acc += Dc(a, e) * r[e] * Dc(b, e);
774 for (std::size_t a = 0; a <
nc; ++a)
775 for (std::size_t b = 0; b <
nc; ++b) Ac(a, b) = A(t.
cov_idx[a], t.
cov_idx[b]);
780 for (std::size_t a = 0; a <
nc; ++a)
781 for (std::size_t b = 0; b <
nc; ++b) Sigma(t.
cov_idx[a], t.
cov_idx[b]) = Sc(a, b);
811 const std::vector<double>& x,
817 "fluid_refine_meanfield: the refined mean field expansion needs a twice-differentiable "
818 "drift, but the first-order closure is only piecewise linear. Reach this function "
819 "through method 'refined', which converges the Gaussian closure first");
821 const std::size_t n = x.size();
823 detail::fluid_symmetrize(Sig);
829 const double eps = std::numeric_limits<double>::epsilon();
830 const double lmax = f.
s.empty() ? 0.0 : f.
s[0];
831 std::vector<std::size_t> keep;
832 for (std::size_t m = 0; m < f.
s.size(); ++m)
833 if (f.
s[m] > lmax * std::sqrt(eps) && f.
s[m] > 0.0) keep.push_back(m);
836 for (std::size_t a = 0; a < n; ++a) xnorm += x[a] * x[a];
837 xnorm = std::sqrt(xnorm);
838 const double scale = std::max(1.0, xnorm);
839 const double step = epsrel * scale;
842 std::vector<double> b(n, 0.0);
843 for (std::size_t idx = 0; idx < keep.size(); ++idx) {
844 const std::size_t m = keep[idx];
845 std::vector<double> xp = x, xm = x;
846 for (std::size_t a = 0; a < n; ++a) {
847 xp[a] += step * f.
U(a, m);
848 xm[a] -= step * f.
U(a, m);
852 for (std::size_t a = 0; a < n; ++a)
853 b[a] += f.
s[m] * (Fp[a] - 2.0 * F0[a] + Fm[a]) / (step * step);
855 for (std::size_t a = 0; a < n; ++a) b[a] *= 0.5;
862 const std::vector<double> sv =
svd_values(Ar);
863 const double cond = (sv.empty() || sv.back() == 0.0)
864 ? std::numeric_limits<double>::infinity()
865 : sv.front() / sv.back();
866 if (!std::isfinite(cond) || cond > 1.0 / std::sqrt(eps))
868 "fluid_refine_meanfield: the fluid Jacobian is numerically singular on the reachable "
869 "subspace (condition number " +
870 std::to_string(cond) +
871 "), so the refinement equation A V = -b has no meaningful solution. The fixed point "
872 "sits at a drift kink or the model is marginally stable; use method 'minnormal', which "
873 "resums the same correction without inverting A");
875 std::vector<double> rhs(Vb.
cols(), 0.0);
876 for (std::size_t j = 0; j < Vb.
cols(); ++j) {
878 for (std::size_t a = 0; a < n; ++a) acc += Vb(a, j) * b[a];
882 std::vector<double> vr(Vb.
cols(), 0.0);
883 for (std::size_t j = 0; j < Vb.
cols(); ++j) {
885 for (std::size_t k = 0; k < Vb.
cols(); ++k) acc += Arinv(j, k) * rhs[k];
888 std::vector<double> V(n, 0.0);
889 for (std::size_t a = 0; a < n; ++a) {
891 for (std::size_t j = 0; j < Vb.
cols(); ++j) acc += Vb(a, j) * vr[j];
899 double vnorm = 0.0, resid = 0.0;
900 for (std::size_t a = 0; a < n; ++a) vnorm += V[a] * V[a];
901 vnorm = std::sqrt(vnorm);
902 for (std::size_t a = 0; a < n; ++a) {
904 for (std::size_t j = 0; j < n; ++j) acc += A(a, j) * V[j];
907 info.
rank = keep.size();
911 if (vnorm > 0.5 * std::max(xnorm, std::sqrt(eps)))
913 "fluid_refine_meanfield: the 1/N refinement (norm " + std::to_string(vnorm) +
914 ") is not small against the mean-field fixed point (norm " + std::to_string(xnorm) +
915 "), so the asymptotic expansion is outside its range of validity at this population. "
916 "Use method 'minnormal'");
948 if (!std::is_same<T, double>::value)
950 "solver_fluid_moments: the fluid solver integrates its drift with LSODA, whose "
951 "coefficients assume double precision; rerun with --arith double");
953 const std::size_t M =
sn.nstations, K =
sn.nclasses;
954 std::string m =
opt.method;
955 if (m.size() > 6 && m.compare(0, 6,
"fluid.") == 0) m = m.substr(6);
956 if (!(m ==
"minnormal" || m ==
"refined"))
958 "' is not a moment-closure method; only 'minnormal' and 'refined' "
963 if (detail::fluid_has_time_varying_rates(
opt))
965 "solver_fluid_moments: the moment closures require an AUTONOMOUS drift, but "
966 "options.config.rate_traj / nhpp_sched / rate_sched make the rates time-varying. Use "
967 "method 'closing' or 'matrix'");
974 for (std::size_t r = 0; r < K; ++r)
975 if (!std::isfinite(
sn.classes[r].population))
977 "solver_fluid_moments: the 'refined' method supports closed models only: its "
978 "1/N correction is solved over the full state, including the source pool. Use "
979 "method 'minnormal' for open or mixed models");
986 const std::size_t maxstate =
opt.moment_maxstate;
987 if (terms.
nstate > maxstate)
989 "solver_fluid_moments: the moment-closure methods solve a " +
990 std::to_string(terms.
nstate) +
"x" + std::to_string(terms.
nstate) +
991 " Lyapunov equation, above the limit of " + std::to_string(maxstate) +
992 " set by moment_maxstate. Raise that limit or use method 'closing'");
996 std::vector<bool> share_sched(M,
false);
997 for (std::size_t i = 0; i < M; ++i) {
1003 std::size_t outer_max = 20;
1004 if (
opt.iter_max > 0) outer_max = std::min<std::size_t>(outer_max, std::max<std::size_t>(2,
opt.iter_max));
1017 double mom_tol = 1e-6;
1018 if (
opt.iter_tol > 0.0) mom_tol = std::min(mom_tol,
opt.iter_tol);
1019 const double outer_tol = mom_tol;
1022 cl.
sigma2.assign(M, 0.0);
1028 okcl.
sigma2.assign(M, 0.0);
1030 const double damp_min = 1.0 / 64.0;
1032 std::vector<double> x;
1033 std::size_t iters = 0, outer = 0;
1044 std::vector<bool> no_drift_var(M,
false);
1045 for (std::size_t i = 0; i < M; ++i)
1050 for (outer = 1; outer <= outer_max; ++outer) {
1052 "closure pass %zu: %zu ODE iterations so far", outer, iters);
1075 for (std::size_t i = 0; i < M; ++i) {
1076 if (!terms.
min_exact[i] && !no_drift_var[i])
continue;
1084 iters += mean.
iters;
1101 if (!kink.empty()) {
1102 const double probe[2] = {-1e-6, 1e-6};
1103 for (std::size_t side = 0; side < 2; ++side) {
1110 "solver_fluid_moments: the fluid fixed point sits on the saturation kink "
1111 "of station " + std::to_string(kink[0] + 1) +
" (population equals its " +
1112 std::to_string(terms.
S[kink[0]]) +
1113 " servers) and the two one-sided drift Jacobians there disagree on "
1114 "hyperbolicity, so which of them the linear noise approximation would use "
1115 "is decided by the integrator's rounding residue rather than by the model. "
1116 "This is the saturated boundary of a continuum of equilibria; use method "
1117 "'closing' for the mean only. Underlying: " + std::string(e.what()));
1127 bool at_seed =
true;
1128 for (std::size_t i = 0; i < M && at_seed; ++i)
1130 for (std::size_t i = 0; i < M && at_seed; ++i)
1131 if (cl.
cov[i].rows() != 0) at_seed =
false;
1132 if (step <= damp_min || at_seed)
throw;
1138 std::vector<double> s2new(M, 0.0);
1139 std::vector<Matrix<double>> covnew(M,
Matrix<double>(0, 0, 0.0));
1140 for (std::size_t i = 0; i < M; ++i) {
1141 const std::vector<std::size_t>& blk = terms.
station_block[i];
1142 if (blk.
empty())
continue;
1144 for (std::size_t a = 0; a < blk.
size(); ++a)
1145 for (std::size_t b = 0; b < blk.
size(); ++b) acc += Sigma(blk[a], blk[b]);
1146 s2new[i] = std::max(0.0, acc);
1147 if (!share_sched[i])
continue;
1149 for (std::size_t a = 0; a < blk.
size(); ++a)
1150 for (std::size_t b = 0; b < blk.
size(); ++b) B(a, b) = Sigma(blk[a], blk[b]);
1154 double l1new = 0.0, l1diff = 0.0;
1155 for (std::size_t i = 0; i < M; ++i) {
1156 l1new += std::fabs(s2new[i]);
1157 l1diff += std::fabs(s2new[i] - trycl.
sigma2[i]);
1159 double delta = l1diff / std::max(1.0, l1new);
1167 for (std::size_t i = 0; i < M; ++i) {
1168 if (covnew[i].rows() == 0)
continue;
1169 double dn = 0.0, nn = 0.0;
1170 for (std::size_t b = 0; b < covnew[i].cols(); ++b) {
1171 double dcol = 0.0, ncol = 0.0;
1172 for (std::size_t a = 0; a < covnew[i].rows(); ++a) {
1173 const double old = (trycl.
cov[i].rows() == covnew[i].rows()) ? trycl.
cov[i](a, b) : 0.0;
1174 dcol += std::fabs(covnew[i](a, b) - old);
1175 ncol += std::fabs(covnew[i](a, b));
1177 dn = std::max(dn, dcol);
1178 nn = std::max(nn, ncol);
1180 delta = std::max(delta, dn / std::max(1.0, nn));
1184 if (delta < outer_tol)
break;
1186 const std::size_t outer_iters = std::min(outer, outer_max);
1199 std::vector<double> refinement;
1200 std::vector<double> r;
1201 if (m ==
"refined") {
1214 const std::vector<double> xmf = mf.
xvec;
1229 bool drift_is_linear =
true;
1230 for (std::size_t i = 0; i < M && drift_is_linear; ++i)
1231 if (!terms.
min_exact[i] && !no_drift_var[i]) drift_is_linear =
false;
1232 if (drift_is_linear)
1233 refinement.assign(xmf.size(), 0.0);
1237 for (std::size_t a = 0; a < x.size(); ++a) {
1238 x[a] += refinement[a];
1239 if (x[a] < 0.0) x[a] = 0.0;
1245 for (std::size_t i = 0; i < M; ++i) {
1246 const std::vector<std::size_t>& blk = terms.
station_block[i];
1247 if (blk.
empty())
continue;
1249 for (std::size_t a = 0; a < blk.
size(); ++a)
1250 for (std::size_t b = 0; b < blk.
size(); ++b) acc += Sigma(blk[a], blk[b]);
1251 cl.
sigma2[i] = std::max(0.0, acc);
1262 std::vector<double> Seff = terms.
S;
1263 for (std::size_t i = 0; i < M; ++i) {
1264 const std::vector<double>& lld = terms.
sys.
lld[i];
1265 for (std::size_t k = 0; k < lld.size(); ++k) Seff[i] = std::max(Seff[i], lld[k]);
1276 for (std::size_t i = 0; i < M; ++i)
1277 for (std::size_t k = 0; k < K; ++k) {
1278 const std::vector<std::size_t>& blk = terms.
class_block[i][k];
1279 if (blk.
empty())
continue;
1280 double q = 0.0, g = 0.0;
1281 for (std::size_t a = 0; a < blk.
size(); ++a) {
1291 for (std::size_t e = 0; e < r.size() && e < terms.
emap.
rows(); ++e) {
1296 w += terms.
emap(e, o);
1297 if (w != 0.0) tn += r[e] * w;
1315 for (std::size_t i = 0; i < M; ++i) {
1317 if (nt != qn::NodeType::Source && nt != qn::NodeType::Sink)
continue;
1318 for (std::size_t k = 0; k < K; ++k) {
1330 for (std::size_t i = 0; i < M; ++i)
1331 for (std::size_t k = 0; k < K; ++k) {
1332 const std::vector<std::size_t>& blk = terms.
class_block[i][k];
1333 if (blk.
empty())
continue;
1335 for (std::size_t a = 0; a < blk.
size(); ++a)
1336 for (std::size_t b = 0; b < blk.
size(); ++b) acc += Sigma(blk[a], blk[b]);
1337 rep.
QVar(i, k) = std::max(0.0, acc);
1338 rep.
QStd(i, k) = std::sqrt(rep.
QVar(i, k));
1349 out.
XN.assign(K, 0.0);
1350 out.
CN.assign(K, 0.0);
1351 for (std::size_t k = 0; k < K; ++k) {
1352 const std::size_t rs =
sn.classes[k].refstat;
1353 if (rs >= 1 && rs <= M) out.
XN[k] = out.
TN(rs - 1, k);
1355 for (std::size_t i = 0; i < M; ++i) q += out.
QN(i, k);
1356 if (out.
XN[k] > 0.0) out.
CN[k] = q / out.
XN[k];
UnsupportedError(const std::string &what)
Raised when the moment closure cannot serve this model: the linearization at the fixed point is not h...
FluidNonHyperbolicError(const std::string &what)
A network plus its refreshed NetworkStruct.
static void loop(const char *fmt,...)
Announce an iteration loop and reset its reporting budget.
static void iter(long k, const char *fmt,...)
Report iteration k of the current loop.
Eigenvalues and singular values, backed by LAPACK.
The exception types the port throws.
The moment closures the fluid drift is built from: fluid_min_closure.m, fluid_capacity_closure....
The one exception the fluid fallback ladder catches.
The fluid drift: a port of solver_fluid_odes.m and the ode_jumps_new / ode_rate_base / ode_rates_clos...
Dense linear algebra over the templated number type: products, identity, inverse, and powers.
Running progress log of a LINE solver run (the "solver console").
Dense matrix and non-owning view.
ClosureValue fluid_lld_scaling(const std::vector< double > &lldrow, double n)
Port of fluid_lld_scaling.m: the limited load-dependent multiplier alpha at a CONTINUOUS population,...
Matrix< double > fluid_lyapunov(const Matrix< double > &A, const Matrix< double > &Qdiff, const Matrix< double > &D, FluidLyapunovInfo &info, double tol=-1.0)
Port of fluid_lyapunov.m: the stationary covariance of the linear noise approximation.
std::vector< double > fluid_nudge_off_kink(const FluidMomentTerms &t, const std::vector< double > &x, const std::vector< std::size_t > &kink, double rel)
A copy of x with every station in kink moved to c_i*(1 + rel), i.e.
ClosureValue fluid_capacity_closure(double n, double c, double s2, const std::vector< double > &lldrow, bool is_inf)
Port of fluid_capacity_closure.m: E[psi(X)] and its derivative, where psi(n) = min(n,...
void fluid_rates_closing_factors(const FluidOdeSystem &sys, const double *x, std::vector< double > &g)
Port of ode_rates_closing_factors: the state-dependent factor g(x), in place.
Matrix< double > fluid_drift_jacobian(const FluidMomentTerms &t, const std::vector< double > &x, const FluidClosure &cl)
Port of fluid_drift_jacobian.m: the analytic Jacobian of the fluid drift.
ShareValue fluid_share_closure(const std::vector< double > &x, const std::vector< double > &wv, const Matrix< double > &C, bool want_jac, bool want_cov=false)
Port of fluid_share_closure.m: E[w_j X_j / sum_m w_m X_m] by the delta method, and its Jacobian at fi...
std::vector< double > fluid_moment_drift(const FluidMomentTerms &t, const std::vector< double > &x, const FluidClosure &cl)
The drift F(x) = D r(x) under a closure: terms.driftFcn.
std::vector< double > fluid_moment_rates(const FluidMomentTerms &t, const std::vector< double > &x, const FluidClosure &cl)
The event rates r(x) under a closure: terms.ratesFcn.
FluidMomentTerms fluid_moment_terms(const qn::NetworkStruct< T > &sn, const FluidOptions &opt)
FluidSolution solver_fluid_moments(const qn::NetworkStruct< T > &sn, const FluidOptions &opt)
Port of solver_fluid_moments.m: the second-order fluid analysis backing minnormal and refined.
FluidOdeSystem fluid_ode_system(const qn::NetworkStruct< T > &sn)
Build the drift of sn: the port of ode_jumps_new and ode_rate_base fused into one pass.
FluidClosure fluid_blend_closure(const FluidClosure &a, const FluidClosure &b, double step)
a + step*(b - a) for a closure, entry by entry.
std::vector< double > fluid_moment_factors(const FluidMomentTerms &t, const std::vector< double > &x, const FluidClosure &cl)
The rate factors g(x) under a closure: terms.factorFcn.
ShareValue fluid_gps_share(const std::vector< double > &xk, const std::vector< double > &wk_in, const std::vector< double > &vk, bool want_jac)
Port of fluid_gps_share.m: the expected capacity share of a GPS station under a normal marginal,...
std::vector< std::size_t > fluid_kink_stations(const FluidMomentTerms &t, const std::vector< double > &x, const FluidClosure &cl)
Every station whose population sits ON the saturation kink n_i = c_i of the first-order rate factor,...
Matrix< double > fluid_jump_matrix(const FluidOdeSystem &sys)
The reference's dense jump matrix D, (nstates x nevents), rebuilt from the two-index event form this ...
FluidImmediateResult fluid_eliminate_immediate(const FluidOdeSystem &sys, double imm_tol=fluid_immediate_transition_tol())
bool fluid_coord_eliminated(const FluidMomentTerms &t, std::size_t s)
True when the immediate reduction folded coordinate s away, so the reduced drift holds no mass there ...
std::vector< double > fluid_refine_meanfield(const FluidMomentTerms &t, const std::vector< double > &x, const FluidClosure &cl, const Matrix< double > &Sigma, FluidRefineInfo &info, double epsrel=1e-4)
Port of fluid_refine_meanfield.m: the O(1/N) refined mean field correction of Gast (POMACS 2017).
bool fluid_hide_immediate(const qn::NetworkStruct< T > &sn, const Opt &opt)
Stochastic complementation of the INSTANTANEOUS coordinates of a fluid drift, the twin of ode_elimina...
Matrix< double > fluid_moment_lyapunov(const FluidMomentTerms &t, const Matrix< double > &A, const std::vector< double > &r, const Matrix< double > *clampT=nullptr)
local_lyapunov of solver_fluid_moments.m: the covariance on the coordinates that carry a real populat...
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
NodeType
Node kinds, with the values of MATLAB NodeType.
Matrix< T > inverse(const Matrix< T > &A)
Inverse by LU with one factorization and n back substitutions.
Matrix< T > matmul(const Matrix< T > &A, const Matrix< T > &B)
Matrix product A B.
std::vector< std::complex< double > > eig_values(const Matrix< double > &A)
Eigenvalues of a general real square matrix, in LAPACK's order.
std::vector< double > svd_values(const Matrix< double > &A)
Singular values in descending order.
Matrix< T > sylvester_solve(const Matrix< T > &A, const Matrix< T > &B, const Matrix< T > &C)
Solve A X + X B = C for X.
Matrix< T > eye(std::size_t n)
Identity of order n.
SvdFactors svd_full(const Matrix< double > &A)
Full SVD of a real matrix, singular values in descending order.
A queueing network and its refreshed NetworkStruct.
SolverFluid: the closing method, a port of solver_fluid.m, solver_fluid_iteration....
A = U diag(s) Vt, with U (m x m), s of length min(m,n) and Vt (n x n).
A closure's value and its first two derivatives with respect to the first mean.
The second moment the drift closes its non-linear terms with, i.e.
std::vector< Matrix< double > > cov
per station, 0x0 keeps the plug-in share
const Matrix< double > * cov_of(std::size_t i) const
std::vector< double > sigma2
per station; empty selects first order
double sigma2_of(std::size_t i) const
bool gaussian() const
any(sigma2 > 0), the reference's GLOBAL gaussian flag.
std::size_t event_idx
state entry whose g(x) drives this rate
double rate_base
the model-fixed part of the rate
Where each (station, class) block sits in the state vector.
std::vector< std::vector< std::size_t > > qidx
0-based first index of (i,r)
std::size_t nstates
length of the state vector
std::vector< std::vector< bool > > enabled
whether (i,r) is served at all
std::vector< std::vector< std::size_t > > kic
phases held by (i,r); 0 when disabled
The reference's MException('LINE:FluidNonHyperbolic'), as a type.
The second-order results of the moment-closure methods, i.e.
std::vector< std::vector< std::vector< std::size_t > > > class_block
state coordinates of each (station,class): Sigma is indexed by SERVICE PHASE, so reading a per-class ...
Matrix< double > Sigma
state-level covariance, on range(D)
Matrix< double > QStd
per station and class queue-length variance
std::vector< double > refinement
the 1/N correction, refined only
std::vector< double > sigma2
per-station population variance
Port of fluid_moment_terms.m: the event representation of the fluid population process,...
std::vector< bool > ev_is_departure
the leading n_departures events
std::vector< bool > is_ext
std::vector< double > S
servers, INF substituted, lld peak folded
std::vector< std::vector< std::vector< std::size_t > > > class_block
std::vector< std::size_t > cov_idx
coordinates carrying a real population
Matrix< double > emap
emap(e, o): expected firings of the ORIGINAL event o per firing of the reduced event e; the identity ...
std::vector< bool > min_exact
stations whose occupancy cannot reach their server count, where min(n,c) is the identity and the clos...
std::vector< std::vector< std::size_t > > station_block
std::vector< std::size_t > ev_class
0-based, from the event's coordinate
Matrix< double > absorb
Projector taking an initial condition onto the surviving coordinates.
Matrix< double > D
(nstate x nevents)
std::vector< std::size_t > ev_station
std::size_t n_departures
How many leading entries of events are DEPARTURES (a job completing at one block and starting at anot...
std::vector< double > nservers
per station, already finite
FluidClosure closure
The second moment the closures read; empty is the first-order drift.
std::vector< std::vector< double > > lld
sn.lldscaling(i,:) per station, EMPTY when the station has none or when every entry is one – the refe...
std::vector< FluidEvent > events
std::vector< std::vector< double > > weight
per station, per class (DPS, GPS)
std::vector< lang::SchedStrategy > sched
per station
Controls, defaulting to SolverOptions('Fluid') in the reference.
FluidClosure closure
options.config.moment_sigma2 and options.config.moment_cov: the second moment the drift's non-linear ...
What fluid_refine_meanfield reports about the correction it computed.
What the analyzer returns, in the same shape as the MVA solver's result.
bool has_moments
result.solverSpecific.moments: set only by minnormal and refined.
FluidMomentReport moments
std::vector< double > xvec
the converged fluid state
A share closure's value and Jacobian, and the joint-closure covariance.
static constexpr double FineTol
static constexpr double Zero
Singular value decomposition WITH the singular vectors, and the Moore-Penrose pseudo-inverse built fr...
The Sylvester equation A X + X B = C, and MATLAB's lyap(A,B,C).