16constexpr double kPi = 3.141592653589793238462643383279502884;
29std::vector<double> chebyshev_coeffs_from_samples(
const std::vector<double>& fj) {
30 const std::size_t Np1 = fj.size();
32 throw std::invalid_argument(
"chebyshev_coeffs_from_samples: need at least 2 samples");
34 const std::size_t N = Np1 - 1;
35 std::vector<double> c(Np1, 0.0);
36 const double pi_N = kPi /
static_cast<double>(N);
37 for (std::size_t k = 0; k <= N; ++k) {
39 for (std::size_t j = 0; j <= N; ++j) {
40 const double wj = (j == 0 || j == N) ? 0.5 : 1.0;
41 s += wj * fj[j] * std::cos(
static_cast<double>(k * j) * pi_N);
43 const double wk = (k == 0 || k == N) ? 0.5 : 1.0;
44 c[k] = (2.0 /
static_cast<double>(N)) * wk * s;
50double clenshaw_eval(
const std::vector<double>& c,
double s)
noexcept {
54 const std::size_t N = c.size() - 1;
63 for (std::size_t k = N; k > 0; --k) {
64 const double b_k = 2.0 * s * b_kp1 - b_kp2 + c[k];
68 return s * b_kp1 - b_kp2 + c[0];
85std::vector<double> chebyshev_derivative_coeffs(
const std::vector<double>& c) {
86 const std::size_t Np1 = c.size();
90 const std::size_t N = Np1 - 1;
92 std::vector<double> d(N, 0.0);
95 for (std::size_t kp = N; kp > 0; --kp) {
96 const double d_km1 = (kp + 1 < N ? d[kp + 1] : 0.0) + 2.0 *
static_cast<double>(kp) * c[kp];
117std::pair<double, double> tight_piece_bounds(
const std::vector<double>& c,
const std::vector<double>& d)
noexcept {
118 auto eval_s = [&](
double s) {
return clenshaw_eval(c, s); };
119 auto eval_deriv = [&](
double s) {
return clenshaw_eval(d, s); };
121 double lo = eval_s(-1.0);
123 const double at_p1 = eval_s(1.0);
132 const std::size_t N = c.size() - 1;
133 const std::size_t n_samples = 8 * std::max<std::size_t>(N, 4);
134 double prev_s = -1.0;
135 double prev_d = eval_deriv(prev_s);
136 for (std::size_t k = 1; k <= n_samples; ++k) {
137 const double cur_s = -1.0 + 2.0 *
static_cast<double>(k) /
static_cast<double>(n_samples);
138 const double cur_d = eval_deriv(cur_s);
139 if (prev_d == 0.0 || (prev_d * cur_d < 0.0)) {
141 double a_lo = prev_s, a_hi = cur_s;
142 double f_lo = prev_d;
143 for (
int it = 0; it < 60; ++it) {
144 const double mid = 0.5 * (a_lo + a_hi);
145 const double f_mid = eval_deriv(mid);
146 if (f_lo * f_mid <= 0.0) {
152 if (a_hi - a_lo < 1e-12) {
156 const double root_val = eval_s(0.5 * (a_lo + a_hi));
173 ParamScale scale,
const std::function<
double(
double)>& f) {
174 if (!(a_hi > a_lo)) {
175 throw std::invalid_argument(
"PiecewiseChebyshevCurve::build: a_hi must exceed a_lo");
178 throw std::invalid_argument(
"PiecewiseChebyshevCurve::build: LOG requires a_lo > 0");
181 throw std::invalid_argument(
"PiecewiseChebyshevCurve::build: need at least 1 piece");
184 throw std::invalid_argument(
"PiecewiseChebyshevCurve::build: degree must be >= 2");
187 const double t_lo = (scale ==
ParamScale::LOG) ? std::log(a_lo) : a_lo;
188 const double t_hi = (scale ==
ParamScale::LOG) ? std::log(a_hi) : a_hi;
189 const double dt = (t_hi - t_lo) /
static_cast<double>(n_pieces);
191 std::vector<Piece> pieces;
192 pieces.reserve(n_pieces);
193 double b_min = std::numeric_limits<double>::infinity();
194 double b_max = -std::numeric_limits<double>::infinity();
195 const std::size_t Np1 = degree + 1;
196 const double pi_over_deg = kPi /
static_cast<double>(degree);
198 for (std::size_t p = 0; p < n_pieces; ++p) {
200 piece.t_lo = t_lo +
static_cast<double>(p) * dt;
201 piece.t_hi = piece.t_lo + dt;
202 piece.t_mid = 0.5 * (piece.t_lo + piece.t_hi);
203 piece.inv_half_span = 2.0 / (piece.t_hi - piece.t_lo);
209 std::vector<double> fj(Np1);
210 for (std::size_t j = 0; j <= degree; ++j) {
211 const double s_j = std::cos(
static_cast<double>(j) * pi_over_deg);
212 const double t_j = piece.t_mid + 0.5 * dt * s_j;
214 const double v = f(a_j);
215 if (!std::isfinite(v)) {
216 throw std::invalid_argument(
"PiecewiseChebyshevCurve::build: sample function returned non-finite value");
220 piece.coeffs = chebyshev_coeffs_from_samples(fj);
221 piece.deriv_coeffs = chebyshev_derivative_coeffs(piece.coeffs);
227 const auto [piece_lo, piece_hi] = tight_piece_bounds(piece.coeffs, piece.deriv_coeffs);
228 if (piece_lo < b_min) {
231 if (piece_hi > b_max) {
234 pieces.push_back(std::move(piece));
237 return std::unique_ptr<PiecewiseChebyshevCurve>(
new PiecewiseChebyshevCurve(a_lo, a_hi, scale, std::move(pieces), b_min, b_max));
240PiecewiseChebyshevCurve::PiecewiseChebyshevCurve(
double a_lo,
double a_hi, ParamScale scale, std::vector<Piece> pieces,
double b_min,
double b_max)
241 : a_lo_(a_lo), a_hi_(a_hi), scale_(scale), pieces_(std::move(pieces)), b_min_(b_min), b_max_(b_max) {}
250 s.
pieces.reserve(pieces_.size());
251 for (
const auto& p : pieces_) {
252 s.
pieces.push_back(
PieceState{p.
t_lo, p.t_hi, p.inv_half_span, p.t_mid, p.coeffs, p.deriv_coeffs});
259 throw std::invalid_argument(
"PiecewiseChebyshevCurve::from_state: pieces is empty");
262 throw std::invalid_argument(
"PiecewiseChebyshevCurve::from_state: a_hi must exceed a_lo");
264 std::vector<Piece> pieces;
265 pieces.reserve(s.
pieces.size());
266 for (
auto& ps : s.
pieces) {
267 if (ps.coeffs.size() < 2) {
268 throw std::invalid_argument(
"PiecewiseChebyshevCurve::from_state: each piece needs >= 2 coefficients");
270 if (ps.deriv_coeffs.size() + 1 != ps.coeffs.size()) {
271 throw std::invalid_argument(
"PiecewiseChebyshevCurve::from_state: deriv_coeffs size must be coeffs.size() - 1");
273 if (!(ps.t_hi > ps.t_lo)) {
274 throw std::invalid_argument(
"PiecewiseChebyshevCurve::from_state: each piece must have t_hi > t_lo");
279 p.inv_half_span = ps.inv_half_span;
281 p.coeffs = std::move(ps.coeffs);
282 p.deriv_coeffs = std::move(ps.deriv_coeffs);
283 pieces.push_back(std::move(p));
290 const double dt0 = pieces.front().t_hi - pieces.front().t_lo;
291 const double eps = 1e-9 * std::max(1.0, std::abs(dt0));
292 for (std::size_t i = 0; i < pieces.size(); ++i) {
293 if (i > 0 && std::abs(pieces[i].t_lo - pieces[i - 1].t_hi) > eps) {
294 throw std::invalid_argument(
"PiecewiseChebyshevCurve::from_state: pieces must be contiguous in t (gap or overlap detected)");
296 const double dt = pieces[i].t_hi - pieces[i].t_lo;
297 if (std::abs(dt - dt0) > eps) {
298 throw std::invalid_argument(
"PiecewiseChebyshevCurve::from_state: pieces must be uniformly spaced in t (locate_piece assumes this)");
304double PiecewiseChebyshevCurve::to_t(
double a)
const noexcept {
305 return (scale_ == ParamScale::LOG) ? std::log(a) : a;
308double PiecewiseChebyshevCurve::dt_da(
double a)
const noexcept {
309 return (scale_ == ParamScale::LOG) ? 1.0 / a : 1.0;
312std::size_t PiecewiseChebyshevCurve::locate_piece(
double t)
const noexcept {
313 const std::size_t n = pieces_.size();
321 if (!std::isfinite(t)) {
326 const double t0 = pieces_.front().t_lo;
327 const double tn = pieces_.back().t_hi;
334 const double dt = pieces_.front().t_hi - pieces_.front().t_lo;
335 auto idx =
static_cast<std::ptrdiff_t
>((t - t0) / dt);
339 if (
static_cast<std::size_t
>(idx) >= n) {
342 return static_cast<std::size_t
>(idx);
346 const double t = to_t(a);
347 const std::size_t p = locate_piece(t);
348 const Piece& pc = pieces_[p];
349 const double s = (t - pc.t_mid) * pc.inv_half_span;
350 return clenshaw_eval(pc.coeffs, s);
354 const double t = to_t(a);
355 const std::size_t p = locate_piece(t);
356 const Piece& pc = pieces_[p];
357 const double s = (t - pc.t_mid) * pc.inv_half_span;
361 const double df_ds = clenshaw_eval(pc.deriv_coeffs, s);
362 return df_ds * pc.inv_half_span * dt_da(a);
366 return {b_min_, b_max_};
370 return {a_lo_, a_hi_};