LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
fes_map_interp.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_API_FES_MAP_INTERP_H
6#define LINE_API_FES_MAP_INTERP_H
7
8/**
9 * @file
10 * @ingroup api_fes
11 * Shape-preserving interpolation of the flow-equivalent descriptors, and the
12 * population grid they are evaluated on.
13 *
14 * Templated port of matlab/src/api/fes/fes_map_interp.m, fes_map_interp_edge.m and
15 * fes_map_grid.m, mirrored by the JAR and native Python.
16 *
17 * Fritsch and Carlson slopes are used, with the noncentered three-point endpoint
18 * rule of de Boor, so the interpolant never overshoots and a monotone sequence of
19 * throughputs stays monotone. The algorithm is written out rather than delegated
20 * to MATLAB's pchip so that the four codebases return identical values; the
21 * MATLAB port is checked against the built-in to machine precision.
22 *
23 * ARITHMETIC: field operations only, exact at T = Rational.
24 */
25
26#include <algorithm>
27#include <cmath>
28#include <cstddef>
29#include <vector>
30
31#include "line/num/number.h"
32#include "line/util/matrix.h"
33
34namespace line {
35namespace fes {
36
37/** Leading populations kept in full by the default grid. */
38const std::size_t FES_MAP_GRID_NHEAD = 10;
39/** Equispaced points above them. */
40const std::size_t FES_MAP_GRID_NTAIL = 10;
41
42/**
43 * Populations at which the inter-departure MAP is evaluated. Fitting one MAP per
44 * population is wasteful because the processes of neighbouring populations are
45 * similar; the reference evaluates the first ten populations and ten further
46 * equispaced points.
47 */
48inline std::vector<std::size_t> fes_map_grid(std::size_t n, std::size_t nhead = FES_MAP_GRID_NHEAD,
49 std::size_t ntail = FES_MAP_GRID_NTAIL) {
50 std::vector<std::size_t> grid;
51 if (n <= nhead + ntail) {
52 for (std::size_t k = 1; k <= n; ++k) grid.push_back(k);
53 return grid;
54 }
55 std::vector<bool> taken(n + 1, false);
56 for (std::size_t k = 1; k <= nhead; ++k) taken[k] = true;
57 for (std::size_t j = 0; j < ntail; ++j) {
58 const double t = (ntail == 1) ? static_cast<double>(n)
59 : static_cast<double>(nhead + 1) +
60 static_cast<double>(j) *
61 static_cast<double>(n - nhead - 1) /
62 static_cast<double>(ntail - 1);
63 taken[static_cast<std::size_t>(std::llround(t))] = true;
64 }
65 for (std::size_t k = 1; k <= n; ++k)
66 if (taken[k]) grid.push_back(k);
67 return grid;
68}
69
70namespace detail {
71
72/** Noncentered three-point endpoint slope with the monotonicity clamps of de Boor. */
73template <class T>
74T interp_edge(const T& h1, const T& h2, const T& del1, const T& del2) {
75 const T zero = num_traits<T>::from_int(0);
76 const T two = num_traits<T>::from_int(2);
77 const T three = num_traits<T>::from_int(3);
78 const T d = T(((two * h1 + h2) * del1 - h1 * del2) / (h1 + h2));
79 const int sd = (d > zero) - (d < zero);
80 const int s1 = (del1 > zero) - (del1 < zero);
81 const int s2 = (del2 > zero) - (del2 < zero);
82 if (sd != s1) return zero;
83 const T ad = (d < zero) ? T(-d) : d;
84 const T a1 = (del1 < zero) ? T(-(three * del1)) : T(three * del1);
85 if (s1 != s2 && ad > a1) return three * del1;
86 return d;
87}
88
89} // namespace detail
90
91/**
92 * Monotone piecewise cubic Hermite interpolation of one series.
93 *
94 * @param x sample abscissae, strictly increasing
95 * @param y sample values
96 * @param xq query abscissae
97 */
98template <class T>
99std::vector<T> fes_map_interp(const std::vector<T>& x, const std::vector<T>& y,
100 const std::vector<T>& xq) {
101 const std::size_t n = x.size();
102 const T zero = num_traits<T>::from_int(0);
103 const T two = num_traits<T>::from_int(2);
104 const T three = num_traits<T>::from_int(3);
105 std::vector<T> yq(xq.size(), zero);
106 if (n == 1) {
107 for (std::size_t q = 0; q < xq.size(); ++q) yq[q] = y[0];
108 return yq;
109 }
110
111 std::vector<T> h(n - 1), delta(n - 1);
112 for (std::size_t i = 0; i + 1 < n; ++i) {
113 h[i] = x[i + 1] - x[i];
114 delta[i] = T((y[i + 1] - y[i]) / h[i]);
115 }
116
117 std::vector<T> d(n, zero);
118 if (n == 2) {
119 d[0] = delta[0];
120 d[1] = delta[0];
121 } else {
122 for (std::size_t i = 1; i + 1 < n; ++i) {
123 if (delta[i - 1] * delta[i] > zero) {
124 const T w1 = two * h[i] + h[i - 1];
125 const T w2 = h[i] + two * h[i - 1];
126 d[i] = T((w1 + w2) / (w1 / delta[i - 1] + w2 / delta[i]));
127 }
128 }
129 d[0] = detail::interp_edge(h[0], h[1], delta[0], delta[1]);
130 d[n - 1] = detail::interp_edge(h[n - 2], h[n - 3], delta[n - 2], delta[n - 3]);
131 }
132
133 for (std::size_t q = 0; q < xq.size(); ++q) {
134 const T t = xq[q];
135 std::size_t i;
136 if (!(t > x[0])) {
137 i = 0;
138 } else if (!(t < x[n - 1])) {
139 i = n - 2;
140 } else {
141 i = 0;
142 while (i + 2 < n && !(x[i + 1] > t)) ++i;
143 }
144 const T s = t - x[i];
145 const T c2 = T((three * delta[i] - two * d[i] - d[i + 1]) / h[i]);
146 const T c3 = T((d[i] - two * delta[i] + d[i + 1]) / (h[i] * h[i]));
147 yq[q] = y[i] + s * (d[i] + s * (c2 + s * c3));
148 }
149 return yq;
150}
151
152} // namespace fes
153} // namespace line
154
155#endif // LINE_API_FES_MAP_INTERP_H
Dense matrix and non-owning view.
const std::size_t FES_MAP_GRID_NTAIL
Equispaced points above them.
const std::size_t FES_MAP_GRID_NHEAD
Leading populations kept in full by the default grid.
std::vector< T > fes_map_interp(const std::vector< T > &x, const std::vector< T > &y, const std::vector< T > &xq)
Monotone piecewise cubic Hermite interpolation of one series.
std::vector< std::size_t > fes_map_grid(std::size_t n, std::size_t nhead=FES_MAP_GRID_NHEAD, std::size_t ntail=FES_MAP_GRID_NTAIL)
Populations at which the inter-departure MAP is evaluated.
Number-type abstraction for the templated API port.