5#ifndef LINE_SOLVERS_FLUID_FLUID_SYMODES_H
6#define LINE_SOLVERS_FLUID_FLUID_SYMODES_H
82 std::vector<lang::SchedStrategy>
sched;
85 std::vector<double>
S;
86 std::vector<double>
x0;
115 std::size_t c, std::size_t ki,
double& coeff,
const FluidLayout& L,
117 const std::vector<double>& fcfs_w, std::size_t var) {
121 if (method ==
"closing") {
127 for (std::size_t k = 1; k < L.
kic[i][c]; ++k)
141 default: f.
type =
"lin";
break;
150 f.
type = (method ==
"softmin") ?
"fcfsws" :
"fcfsw";
151 coeff *= fcfs_w[var];
154 default: f.
type =
"lin";
break;
184void sym_conservation(FluidSymSystem& sys,
const qn::NetworkStruct<T>& sn) {
185 sys.inv_states.clear();
186 sys.inv_mass.clear();
187 const bool have_x0 = sys.x0.size() == sys.nstates;
188 for (std::size_t c = 0; c < sn.chains.size(); ++c) {
189 bool closed =
true, any =
false;
191 for (std::size_t r = 0; r < sn.chains[c].size(); ++r) {
192 if (!sn.chains[c][r])
continue;
194 if (!std::isfinite(sn.classes[r].population)) {
198 pop += sn.classes[r].population;
200 if (!any || !closed)
continue;
201 std::vector<std::size_t> states;
202 for (std::size_t s = 0; s < sys.nstates; ++s) {
203 const std::size_t i = sys.state_station[s] - 1, r = sys.state_class[s] - 1;
208 if (states.empty())
continue;
209 sys.inv_states.push_back(states);
210 sys.inv_mass.push_back(pop);
213 std::map<std::pair<std::size_t, std::size_t>, std::vector<std::size_t>> groups;
214 for (std::size_t s = 0; s < sys.nstates; ++s)
216 groups[std::make_pair(sys.state_station[s], sys.state_class[s])].push_back(s);
217 for (std::map<std::pair<std::size_t, std::size_t>, std::vector<std::size_t>>::const_iterator
219 it != groups.end(); ++it) {
222 for (std::size_t k = 0; k < it->second.size(); ++k) mass += sys.x0[it->second[k]];
224 mass = (sys.form ==
"J") ? 1.0 : 0.0;
225 sys.inv_states.push_back(it->second);
226 sys.inv_mass.push_back(mass);
240 double pstar,
const std::vector<double>& init_sol) {
241 const std::size_t M =
sn.nstations, K =
sn.nclasses;
242 std::string method = method_in;
243 if (method.compare(0, 4,
"fld.") == 0) method = method.substr(4);
246 if (method ==
"default" || method ==
"matrix" || method ==
"pnorm") {
248 sys.
method = (method ==
"default") ?
"matrix" : method;
249 }
else if (method ==
"closing" || method ==
"statedep" || method ==
"softmin") {
254 "fluid_symodes: the symbolic ODE export is unsupported for method '" + method_in +
255 "'; supported are default, matrix, pnorm, closing, statedep and softmin");
261 for (std::size_t i = 0; i < M; ++i) {
263 sys.
sched[i] =
sn.stations[i].sched;
267 for (std::size_t r = 0; r < K; ++r) sys.
class_names[r] =
sn.classes[r].name;
269 double closed_pop = 0.0;
270 for (std::size_t r = 0; r < K; ++r)
271 if (std::isfinite(
sn.classes[r].population)) closed_pop +=
sn.classes[r].population;
272 sys.
S.assign(M, 1.0);
273 for (std::size_t i = 0; i < M; ++i) {
274 const double c =
sn.stations[i].nservers;
275 sys.
S[i] = std::isfinite(c) ? c : closed_pop;
280 if (sys.
form ==
"W") {
290 sys.
smoothing = (pstar > 0.0) ?
"pnorm" :
"min";
291 sys.
pstar.assign(M, pstar);
296 for (std::size_t i = 0; i < M; ++i)
297 for (std::size_t r = 0; r < K; ++r) {
298 const std::size_t p = L.
kic[i][r];
299 if (p == 0)
continue;
300 for (std::size_t k = 0; k < p; ++k) {
307 throw NumericError(
ErrorKind{
"Degenerate"},
"fluid_symodes: the W-form state metadata does not match the drift");
308 detail::sym_conservation(sys,
sn);
313 const bool sd = (method ==
"statedep" || method ==
"softmin");
315 for (std::size_t i = 0; i < M; ++i)
318 "' method does not support open models, so their ODE "
319 "system cannot be exported; use 'matrix' or 'closing'");
325 for (std::size_t i = 0; i < M; ++i)
326 for (std::size_t c = 0; c < K; ++c)
327 for (std::size_t k = 0; k < L.
kic[i][c]; ++k) {
334 for (std::size_t i = 0; i < M; ++i) {
337 for (std::size_t r = 0; r < K; ++r) {
338 const double w = (r <
sn.stations[i].schedparam.size())
345 for (std::size_t r = 0; r < K; ++r) sys.
dpsw(i, r) /= tot;
350 for (std::size_t i = 0; i < M; ++i) {
352 for (std::size_t c = 0; c < K; ++c) {
353 if (!L.
enabled[i][c])
continue;
354 for (std::size_t k = 0; k < L.
kic[i][c]; ++k)
361 std::vector<std::vector<std::vector<double>>> mu(M, std::vector<std::vector<double>>(K));
362 std::vector<std::vector<std::vector<double>>> phi(M, std::vector<std::vector<double>>(K));
363 std::vector<std::vector<std::vector<double>>> pie(M, std::vector<std::vector<double>>(K));
364 for (std::size_t i = 0; i < M; ++i)
365 for (std::size_t r = 0; r < K; ++r) {
367 pie[i][r] = std::vector<double>{1.0};
370 detail::fluid_mu_phi(
sn.service[i][r], mu[i][r], phi[i][r]);
371 pie[i][r] = detail::fluid_pie(
sn.service[i][r]);
373 const std::size_t NS =
sn.nof_stateful();
374 const bool have_rt =
sn.rt.rows() == NS * K;
375 std::vector<std::size_t> sf(M, 0);
376 for (std::size_t i = 0; i < M; ++i) sf[i] =
sn.stateful_of_station(i + 1) - 1;
377 const auto route = [&](std::size_t i, std::size_t c, std::size_t j, std::size_t l) ->
double {
378 if (!have_rt)
return 0.0;
382 std::vector<std::vector<double>> cols;
384 for (std::size_t i = 0; i < M; ++i)
385 for (std::size_t c = 0; c < K; ++c) {
386 if (!L.
enabled[i][c])
continue;
387 for (std::size_t j = 0; j < M; ++j)
388 for (std::size_t l = 0; l < K; ++l) {
389 if (!(route(i, c, j, l) > 0.0))
continue;
390 for (std::size_t ki = 0; ki < L.
kic[i][c]; ++ki)
391 for (std::size_t kj = 0; kj < L.
kic[j][l]; ++kj) {
397 if (!detail::sym_handled_sched(
sn.stations[i].sched))
continue;
399 const double pj = kj < pie[j][l].size() ? pie[j][l][kj] : 0.0;
400 double base = phi[i][c][ki] * mu[i][c][ki] * route(i, c, j, l) * pj;
401 if (!(base > 0.0))
continue;
402 std::vector<double> col(L.
nstates, 0.0);
403 col[L.
qidx[i][c] + ki] -= 1.0;
404 col[L.
qidx[j][l] + kj] += 1.0;
405 const std::size_t var = L.
qidx[i][c] + ki;
407 detail::sym_factor(method,
sn.stations[i].sched, i, c, ki, base, L,
410 sys.
coeff.push_back(base);
417 for (std::size_t i = 0; i < M; ++i)
418 for (std::size_t c = 0; c < K; ++c) {
419 if (!L.
enabled[i][c])
continue;
420 if (sd && !detail::sym_handled_sched(
sn.stations[i].sched))
continue;
421 for (std::size_t ki = 0; ki + 1 < L.
kic[i][c]; ++ki)
422 for (std::size_t kp = 0; kp < L.
kic[i][c]; ++kp) {
423 if (kp == ki)
continue;
425 if (!(base > 0.0))
continue;
426 std::vector<double> col(L.
nstates, 0.0);
427 col[L.
qidx[i][c] + ki] = -1.0;
428 col[L.
qidx[i][c] + kp] = 1.0;
429 const std::size_t var = L.
qidx[i][c] + ki;
431 detail::sym_factor(method,
sn.stations[i].sched, i, c, ki, base, L, sys.
S,
434 sys.
coeff.push_back(base);
442 for (std::size_t e = 0; e < sys.
nevents; ++e)
443 for (std::size_t s = 0; s < L.
nstates; ++s) sys.
J(s, e) = cols[e][s];
444 if (method ==
"softmin") sys.
alpha = 20.0;
446 detail::sym_conservation(sys,
sn);
452 std::vector<std::string>
rhs;
459inline std::string sym_state_index(std::size_t v) {
461 std::snprintf(buf,
sizeof(buf),
"%llu",
static_cast<unsigned long long>(v));
462 return std::string(buf);
466inline std::string sym_plain_num(
double v) {
468 if (v == std::floor(v) && std::fabs(v) < 1e15) {
469 std::snprintf(buf,
sizeof(buf),
"%lld",
static_cast<long long>(v));
470 return std::string(buf);
472 std::snprintf(buf,
sizeof(buf),
"%.17g", v);
473 return std::string(buf);
477inline std::string sym_join_sum(
const std::vector<std::string>& parts,
const std::string& offset) {
478 if (parts.empty())
return "(" + offset +
")";
480 for (std::size_t i = 0; i < parts.size(); ++i) {
481 if (i) body +=
" + ";
484 return (offset ==
"0") ?
"(" + body +
")" :
"(" + offset +
" + " + body +
")";
488inline std::string sym_station_sum(
const FluidSymSystem& sys, std::size_t station1,
489 const std::vector<std::string>& vars,
const std::string& offset) {
490 std::vector<std::string> parts;
491 for (std::size_t s = 0; s < sys.nstates; ++s)
492 if (sys.state_station[s] == station1) parts.push_back(vars[s]);
493 return sym_join_sum(parts, offset);
498inline std::string sym_phase_weighted_station_sum(
const FluidSymSystem& sys, std::size_t station1,
499 const std::vector<std::string>& vars,
500 const std::string& offset) {
501 std::vector<std::string> parts;
502 for (std::size_t s = 0; s < sys.nstates; ++s) {
503 if (sys.state_station[s] != station1)
continue;
504 const double w = sys.fcfs_phase_w[s];
505 if (w == 0.0)
continue;
506 parts.push_back(
"(" + sym_plain_num(w) +
")*" + vars[s]);
508 return sym_join_sum(parts, offset);
519inline std::string sym_softmin_expr(
const std::string& x,
const std::string& y,
double alpha) {
520 const std::string a = sym_plain_num(alpha);
521 return "((" + x +
")*exp(-(" + a +
")*(" + x +
")) + (" + y +
")*exp(-(" + a +
")*(" + y +
522 ")))/(exp(-(" + a +
")*(" + x +
")) + exp(-(" + a +
")*(" + y +
")))";
535 std::vector<std::string> out;
536 for (std::size_t g = 0; g < sys.
inv_states.size(); ++g) {
538 for (std::size_t k = 0; k < sys.
inv_states[g].size(); ++k) {
539 if (k > 0) e +=
" + ";
540 e +=
"x" + detail::sym_state_index(sys.
inv_states[g][k] + 1);
542 out.push_back(e +
" - " + detail::sym_plain_num(sys.
inv_mass[g]));
567 for (std::size_t s = 0; s < sys.
nstates; ++s)
568 out.
vars[s] =
"x" + detail::sym_state_index(s + 1);
574 if (sys.
form ==
"W") {
579 "fluid_symbolic_drift: the drift of this method scales rates by min(n_i, S_i), "
580 "which is not differentiable at n_i = S_i, so it has no Jacobian there. Set pstar "
581 "to use the p-norm smoothing, or use the 'softmin' method");
582 std::vector<std::string> theta(sys.
nstates,
"0");
583 for (std::size_t s = 0; s < sys.
nstates; ++s) {
586 const double S = sys.
S[i - 1];
587 const double p = sys.
pstar[i - 1];
591 theta[s] = out.
vars[s];
594 if (S <= 0.0 || p <= 0.0) {
595 theta[s] = out.
vars[s];
598 const std::string ni = detail::sym_station_sum(sys, i, out.
vars, eps0);
599 theta[s] = out.
vars[s] +
"/(1 + (" + ni +
"/" + detail::sym_plain_num(S) +
")^" +
600 detail::sym_plain_num(p) +
")^(1/" + detail::sym_plain_num(p) +
")";
602 for (std::size_t s = 0; s < sys.
nstates; ++s) {
603 std::vector<std::string> terms;
604 for (std::size_t t = 0; t < sys.
nstates; ++t) {
605 const double w = sys.
W(t, s);
606 if (w == 0.0 || theta[t] ==
"0")
continue;
607 terms.push_back(
"(" + detail::sym_plain_num(w) +
")*(" + theta[t] +
")");
609 if (sys.
alambda[s] != 0.0) terms.push_back(detail::sym_plain_num(sys.
alambda[s]));
610 if (terms.empty())
continue;
612 for (std::size_t i = 0; i < terms.size(); ++i) {
613 if (i) body +=
" + ";
627 std::vector<std::string> rate(sys.
nevents,
"0");
628 for (std::size_t e = 0; e < sys.
nevents; ++e) {
632 if (f.
type ==
"lin") {
634 }
else if (f.
type ==
"ext1") {
639 for (std::size_t k = 0; k < f.
others.size(); ++k) {
640 if (k) body +=
" + ";
643 factor =
"(1 - (" + body +
"))";
645 }
else if (f.
type ==
"fcfsws") {
646 const std::string ni = detail::sym_station_sum(sys, f.
station, out.
vars,
"0");
647 const std::string nhat =
648 detail::sym_phase_weighted_station_sum(sys, f.
station, out.
vars, eps0);
650 detail::sym_softmin_expr(ni, detail::sym_plain_num(sys.
S[f.
station - 1]),
655 "fluid_symbolic_drift: event " + detail::sym_state_index(e + 1) +
656 " scales its rate by the non-smooth factor '" + f.
type +
657 "', which has no derivative where the regime switches, so the system has no "
658 "Jacobian. Use the 'softmin' method, or the p-norm smoothing of the 'matrix' "
661 rate[e] =
"(" + detail::sym_plain_num(sys.
coeff[e]) +
")*(" + factor +
")";
663 for (std::size_t s = 0; s < sys.
nstates; ++s) {
664 std::vector<std::string> terms;
665 for (std::size_t e = 0; e < sys.
nevents; ++e) {
666 const double j = sys.
J(s, e);
667 if (j == 0.0)
continue;
668 terms.push_back(
"(" + detail::sym_plain_num(j) +
")*(" + rate[e] +
")");
670 if (terms.empty())
continue;
672 for (std::size_t i = 0; i < terms.size(); ++i) {
673 if (i) body +=
" + ";
684 std::vector<std::string>
rhs;
685 std::vector<std::vector<std::string> >
J;
691inline bool sym_in_station(
const FluidSymSystem& sys, std::size_t station1, std::size_t m) {
708inline std::string sym_softmin_deriv(
const std::string& a,
const std::string& b,
double alpha) {
709 const std::string al = sym_plain_num(alpha);
710 const std::string ea =
"exp(-(" + al +
")*(" + a +
"))";
711 const std::string eb =
"exp(-(" + al +
")*(" + b +
"))";
712 const std::string D =
"(" + ea +
" + " + eb +
")";
713 const std::string sm = sym_softmin_expr(a, b, alpha);
714 return "(" + ea +
"*(1 - (" + al +
")*(" + a +
") + (" + al +
")*(" + sm +
")))/" + D;
718inline std::string sym_mul(
const std::string& u,
const std::string& v) {
719 if (u ==
"0" || v ==
"0")
return "0";
720 if (u ==
"1")
return v;
721 if (v ==
"1")
return u;
722 return "(" + u +
")*(" + v +
")";
726inline std::string sym_sum(
const std::vector<std::string>& parts) {
728 for (std::size_t i = 0; i < parts.size(); ++i) {
729 if (parts[i] ==
"0")
continue;
730 if (!body.empty()) body +=
" + ";
733 return body.empty() ? std::string(
"0") : body;
768 const std::size_t n = sys.
nstates;
769 out.
J.assign(n, std::vector<std::string>(n,
"0"));
772 if (sys.
form ==
"W") {
776 std::vector<std::vector<std::string> > dtheta(n, std::vector<std::string>(n,
"0"));
777 for (std::size_t s = 0; s < n; ++s) {
780 const double S = sys.
S[i - 1], p = sys.
pstar[i - 1];
781 if ((s < sys.
is_inf.size() && sys.
is_inf[s]) || S <= 0.0 || p <= 0.0) {
785 const std::string ni = detail::sym_station_sum(sys, i, out.
vars, eps0);
786 const std::string Sn = detail::sym_plain_num(S), pn = detail::sym_plain_num(p);
787 const std::string base =
"(1 + ((" + ni +
")/" + Sn +
")^" + pn +
")";
788 const std::string Q = base +
"^(1/" + pn +
")";
789 const std::string dQ = base +
"^(1/" + pn +
" - 1)*((" + ni +
")/" + Sn +
")^(" + pn +
791 for (std::size_t m = 0; m < n; ++m) {
792 std::vector<std::string> parts;
793 if (m == s) parts.push_back(
"1/(" + Q +
")");
794 if (detail::sym_in_station(sys, i, m))
795 parts.push_back(
"-(" + out.
vars[s] +
")*(" + dQ +
")/(" + Q +
")^2");
796 dtheta[s][m] = detail::sym_sum(parts);
799 for (std::size_t s = 0; s < n; ++s)
800 for (std::size_t m = 0; m < n; ++m) {
801 std::vector<std::string> parts;
802 for (std::size_t t = 0; t < n; ++t) {
803 const double w = sys.
W(t, s);
804 if (w == 0.0 || dtheta[t][m] ==
"0")
continue;
805 parts.push_back(detail::sym_mul(detail::sym_plain_num(w), dtheta[t][m]));
807 out.
J[s][m] = detail::sym_sum(parts);
815 std::vector<std::vector<std::string> > drate(sys.
nevents, std::vector<std::string>(n,
"0"));
816 for (std::size_t e = 0; e < sys.
nevents; ++e) {
819 const std::string cf = detail::sym_plain_num(sys.
coeff[e]);
820 if (f.
type ==
"lin") {
822 }
else if (f.
type ==
"ext1") {
823 for (std::size_t k = 0; k < f.
others.size(); ++k)
824 drate[e][f.
others[k] - 1] =
"-(" + cf +
")";
825 }
else if (f.
type ==
"fcfsws") {
831 const std::string ni = detail::sym_station_sum(sys, f.
station, out.
vars,
"0");
832 const std::string nhat =
833 detail::sym_phase_weighted_station_sum(sys, f.
station, out.
vars, eps0);
834 const std::string Sn = detail::sym_plain_num(sys.
S[f.
station - 1]);
835 const std::string sm = detail::sym_softmin_expr(ni, Sn, sys.
alpha);
836 const std::string dsm = detail::sym_softmin_deriv(ni, Sn, sys.
alpha);
837 for (std::size_t m = 0; m < n; ++m) {
838 std::vector<std::string> parts;
839 if (m == v) parts.push_back(
"(" + sm +
")/(" + nhat +
")");
840 if (detail::sym_in_station(sys, f.
station, m)) {
841 parts.push_back(
"(" + out.
vars[v] +
")*(" + dsm +
")/(" + nhat +
")");
844 parts.push_back(
"-(" + out.
vars[v] +
")*(" + sm +
")*(" +
845 detail::sym_plain_num(w) +
")/(" + nhat +
")^2");
847 const std::string body = detail::sym_sum(parts);
848 drate[e][m] = body ==
"0" ? std::string(
"0") : detail::sym_mul(cf, body);
852 "fluid_symbolic_jacobian: event " + detail::sym_state_index(e + 1) +
853 " scales its rate by the non-smooth factor '" + f.
type +
854 "', which has no derivative where the regime switches, so the system has no "
855 "Jacobian. Use the 'softmin' method, or the p-norm smoothing of the 'matrix' "
860 for (std::size_t s = 0; s < n; ++s)
861 for (std::size_t m = 0; m < n; ++m) {
862 std::vector<std::string> parts;
863 for (std::size_t e = 0; e < sys.
nevents; ++e) {
864 const double j = sys.
J(s, e);
865 if (j == 0.0 || drate[e][m] ==
"0")
continue;
866 parts.push_back(detail::sym_mul(detail::sym_plain_num(j), drate[e][m]));
868 out.
J[s][m] = detail::sym_sum(parts);
NumericError(const std::string &what)
UnsupportedError(const std::string &what)
A network plus its refreshed NetworkStruct.
The exception types the port throws.
The matrix fluid method: a port of solver_fluid_matrix.m, the formulation of Ruuskanen,...
The fluid drift: a port of solver_fluid_odes.m and the ode_jumps_new / ode_rate_base / ode_rates_clos...
Dense matrix and non-owning view.
std::vector< std::string > fluid_conservation_constraints(const FluidSymSystem &sys)
The invariants of sys as expressions g(x) = 0 over the drift's x1 ... xn.
FluidLayout fluid_layout(const qn::NetworkStruct< T > &sn)
Port of the layout half of solver_fluid_odes.m.
FluidMatrixSystem fluid_matrix_system(const qn::NetworkStruct< T > &sn, const std::vector< double > &init_sol, double pstar)
Assemble the matrix-form drift of sn.
FluidSymbolicDrift fluid_symbolic_drift(const FluidSymSystem &sys)
Port of @@SolverFLD/getSymbolicDrift: the right-hand side of the mean-field ODE system as expression ...
FluidSymbolicJacobian fluid_symbolic_jacobian(const FluidSymSystem &sys)
Port of @@SolverFLD/getJacobian: d f_i / d x_j of the mean-field drift, as expression strings.
FluidSymSystem fluid_symodes(const qn::NetworkStruct< T > &sn, const std::string &method_in, double pstar, const std::vector< double > &init_sol)
Build the symbolic system of sn under opt.method.
SchedStrategy
Scheduling disciplines, with the values of MATLAB SchedStrategy.
const char * sched_to_text(SchedStrategy s)
Conservation laws of a layered queueing network, enumerated from its structure.
A queueing network and its refreshed NetworkStruct.
The <kind> part of a LINE:<category>:<kind> identifier (see _kb/17-error-vocabulary....
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 assembled matrix-form drift and the maps that read metrics off it.
std::vector< double > alambda
(n) external arrivals per state
Matrix< double > W
(n x n) drift generator
std::vector< bool > is_inf
(n) state belongs to an INF station
std::vector< double > x0
(n) initial state
std::vector< bool > is_source
(n) state belongs to an EXT station
The symbolic system, in whichever of the two forms the method implies.
std::vector< double > inv_mass
the mass that group holds
std::vector< double > coeff
std::vector< std::vector< std::size_t > > inv_states
0-based states of each conserved group
std::vector< std::size_t > event_var
0-based state index driving the event
std::vector< std::size_t > state_station
std::vector< double > fcfs_phase_w
std::string form
"W" or "J"
std::vector< std::size_t > state_phase
1-based
std::vector< double > pstar
std::string smoothing
"min" or "pnorm"
std::vector< std::string > station_names
std::vector< double > alambda
std::vector< bool > is_inf
(n) state belongs to an INF station
double alpha
softmin sharpness, set only for that method
std::vector< bool > is_source
std::vector< std::size_t > state_class
std::vector< std::string > sched_names
std::vector< std::string > class_names
std::string method
resolved: default becomes matrix
std::vector< lang::SchedStrategy > sched
std::vector< double > S
servers per station, infinite already substituted
std::vector< SymFactor > factor
The drift as expression strings, one per state variable, plus their names.
std::vector< std::string > vars
std::vector< std::string > rhs
What @@SolverFLD/getJacobian returns: d f_i / d x_j as expression strings.
std::vector< std::string > vars
the state variable names
std::vector< std::string > rhs
the drift, one per variable
std::vector< std::vector< std::string > > J
J[i][j] = d f_i / d x_j.
The state-dependent factor attached to one event's driving variable.
std::vector< std::size_t > others
ext1: the other phases, 1-based state indices
std::string type
lin, min, pnorm, ext1, dpsmin, dpspw, fcfsw, fcfsws
std::size_t station
1-based
static constexpr double FineTol