Janus 2.0.0
High-performance C++20 dual-mode numerical framework
Loading...
Searching...
No Matches
Interpolate.hpp
Go to the documentation of this file.
1#pragma once
7
11#include "janus/math/Linalg.hpp"
12#include <algorithm>
13#include <casadi/casadi.hpp>
14#include <cmath>
15#include <optional>
16#include <vector>
17
18namespace janus {
19
20// ============================================================================
21// Interpolation Method Enum
22// ============================================================================
23
35
36// ============================================================================
37// Extrapolation Mode Enum
38// ============================================================================
39
47
48// ============================================================================
49// Extrapolation Configuration
50// ============================================================================
51
62
64 std::optional<double> lower_bound = std::nullopt;
65 std::optional<double> upper_bound = std::nullopt;
66
68 static ExtrapolationConfig clamp() { return {}; }
69
71 static ExtrapolationConfig linear(std::optional<double> lower = std::nullopt,
72 std::optional<double> upper = std::nullopt) {
73 return {ExtrapolationMode::Linear, lower, upper};
74 }
75};
76
77// ============================================================================
78// Implementation Details
79// ============================================================================
80
81namespace detail {
82
86inline std::string method_to_casadi_string(InterpolationMethod method) {
87 switch (method) {
89 return "linear";
91 return "bspline";
94 // These don't have native CasADi support, handled separately
95 return "linear"; // Fallback for internal use
96 default:
97 return "linear";
98 }
99}
100
101inline const char *hermite_symbolic_error_message() {
102 return "Interpolator: Hermite/Catmull-Rom is numeric-only because interval selection "
103 "requires runtime comparisons. Use BSpline for symbolic or optimization queries.";
104}
105
113template <typename Derived>
115flatten_fortran_order(const Eigen::MatrixBase<Derived> &values) {
116 using Scalar = typename Derived::Scalar;
117 JanusVector<Scalar> flattened(values.size());
118
119 Eigen::Index idx = 0;
120 for (Eigen::Index j = 0; j < values.cols(); ++j) {
121 for (Eigen::Index i = 0; i < values.rows(); ++i) {
122 flattened(idx++) = values(i, j);
123 }
124 }
125
126 return flattened;
127}
128
130 switch (method) {
132 return "interpn: symbolic table values are not supported for Hermite/Catmull-Rom "
133 "because interval selection remains numeric-only. Use BSpline for symbolic or "
134 "optimization queries.";
136 return "interpn: symbolic table values are not supported for Nearest because the "
137 "method is non-differentiable. Use Linear or BSpline.";
138 default:
139 return "interpn: symbolic table values are only supported for Linear and BSpline.";
140 }
141}
142
144 std::vector<std::vector<double>> grid;
146};
147
148inline InterpnGridData build_interpn_grid(const std::vector<NumericVector> &points,
149 const char *context) {
150 if (points.empty()) {
151 throw InterpolationError(std::string(context) + ": points cannot be empty");
152 }
153
154 const int n_dims = static_cast<int>(points.size());
155 InterpnGridData data;
156 data.grid.resize(n_dims);
157
158 for (int d = 0; d < n_dims; ++d) {
159 if (points[d].size() < 2) {
160 throw InterpolationError(std::string(context) + ": points[" + std::to_string(d) +
161 "] must have at least 2 points");
162 }
163
164 data.grid[d].resize(points[d].size());
165 Eigen::VectorXd::Map(data.grid[d].data(), points[d].size()) = points[d];
166
167 for (size_t i = 0; i + 1 < data.grid[d].size(); ++i) {
168 if (data.grid[d][i + 1] <= data.grid[d][i]) {
169 throw InterpolationError(std::string(context) + ": points[" + std::to_string(d) +
170 "] must be strictly increasing");
171 }
172 }
173
174 data.expected_size *= static_cast<int>(points[d].size());
175 }
176
177 return data;
178}
179
180inline void validate_bspline_grid(const std::vector<std::vector<double>> &grid,
181 const char *context) {
182 for (size_t d = 0; d < grid.size(); ++d) {
183 if (grid[d].size() < 4) {
184 throw InterpolationError(std::string(context) +
185 ": BSpline requires at least 4 grid points in dimension " +
186 std::to_string(d));
187 }
188 }
189}
190
191template <JanusScalar Scalar>
193 const char *context) {
194 if (xi.cols() != n_dims && xi.rows() != n_dims) {
195 throw InterpolationError(std::string(context) +
196 ": xi must have shape (n_points, n_dims) or (n_dims, n_points)");
197 }
198
199 const bool need_transpose = (xi.rows() == n_dims && xi.cols() != n_dims);
200 if (need_transpose) {
201 return xi.transpose().eval();
202 }
203 return xi.eval();
204}
205
206template <JanusScalar Scalar>
208 const std::vector<std::vector<double>> &grid) {
209 SymbolicScalar clamped(point.size(), 1);
210 for (int d = 0; d < point.size(); ++d) {
211 SymbolicScalar value =
212 std::is_same_v<Scalar, SymbolicScalar> ? point(d) : SymbolicScalar(point(d));
213 value = SymbolicScalar::fmax(value, grid[d].front());
214 value = SymbolicScalar::fmin(value, grid[d].back());
215 clamped(d) = value;
216 }
217 return clamped;
218}
219
220template <JanusScalar Scalar>
222 const std::vector<std::vector<double>> &grid) {
223 static_assert(std::is_floating_point_v<Scalar>);
224 for (int d = 0; d < point.size(); ++d) {
225 if (point(d) < grid[d].front() || point(d) > grid[d].back()) {
226 return true;
227 }
228 }
229 return false;
230}
231
233 const std::vector<std::vector<double>> &grid) {
234 SymbolicScalar out_of_bounds = SymbolicScalar(0);
235 for (int d = 0; d < point.size(); ++d) {
236 out_of_bounds = SymbolicScalar::logic_or(out_of_bounds, point(d) < grid[d].front());
237 out_of_bounds = SymbolicScalar::logic_or(out_of_bounds, point(d) > grid[d].back());
238 }
239 return out_of_bounds;
240}
241
242inline casadi::Function make_parametric_interpolant(const std::vector<std::vector<double>> &grid,
243 InterpolationMethod method) {
246 }
247 if (method == InterpolationMethod::BSpline) {
248 validate_bspline_grid(grid, "interpn");
249 }
250
251 casadi::Dict opts;
252 opts["inline"] = true;
253 return casadi::interpolant("interp_parametric", method_to_casadi_string(method), grid, 1, opts);
254}
255
256// ============================================================================
257// Catmull-Rom (C1 Hermite) Interpolation Helpers
258// ============================================================================
259
271inline double catmull_rom_slope(const std::vector<double> &x, const std::vector<double> &y,
272 size_t i) {
273 size_t n = x.size();
274 if (n < 2)
275 return 0.0;
276
277 if (i == 0) {
278 // Left endpoint: forward difference
279 return (y[1] - y[0]) / (x[1] - x[0]);
280 } else if (i == n - 1) {
281 // Right endpoint: backward difference
282 return (y[n - 1] - y[n - 2]) / (x[n - 1] - x[n - 2]);
283 } else {
284 // Interior: central difference using neighbors
285 return (y[i + 1] - y[i - 1]) / (x[i + 1] - x[i - 1]);
286 }
287}
288
306inline double hermite_interp_1d_numeric(const std::vector<double> &x, const std::vector<double> &y,
307 double query) {
308 size_t n = x.size();
309 if (n < 2) {
310 return y[0];
311 }
312
313 // Clamp query to grid bounds
314 query = std::max(query, x.front());
315 query = std::min(query, x.back());
316
317 // Find interval containing query using binary search
318 auto it = std::upper_bound(x.begin(), x.end(), query);
319 size_t idx = 0;
320 if (it == x.begin()) {
321 idx = 0;
322 } else if (it == x.end()) {
323 idx = n - 2;
324 } else {
325 idx = static_cast<size_t>(std::distance(x.begin(), it)) - 1;
326 }
327 if (idx >= n - 1)
328 idx = n - 2; // Safety clamp
329
330 // Get interval endpoints
331 double x0 = x[idx];
332 double x1 = x[idx + 1];
333 double y0 = y[idx];
334 double y1 = y[idx + 1];
335 double h = x1 - x0;
336
337 // Compute slopes at endpoints (Catmull-Rom)
338 double m0 = catmull_rom_slope(x, y, idx);
339 double m1 = catmull_rom_slope(x, y, idx + 1);
340
341 // Normalized coordinate
342 double t = (query - x0) / h;
343 t = std::max(0.0, std::min(1.0, t)); // Clamp to [0, 1]
344
345 double t2 = t * t;
346 double t3 = t2 * t;
347
348 // Hermite basis functions
349 double h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
350 double h10 = t3 - 2.0 * t2 + t;
351 double h01 = -2.0 * t3 + 3.0 * t2;
352 double h11 = t3 - t2;
353
354 // Interpolated value
355 return h00 * y0 + h10 * (m0 * h) + h01 * y1 + h11 * (m1 * h);
356}
357
361inline std::vector<int> flat_to_multi_index(int flat_idx, const std::vector<int> &dims) {
362 std::vector<int> multi(dims.size());
363 int remaining = flat_idx;
364 for (size_t d = 0; d < dims.size(); ++d) {
365 multi[d] = remaining % dims[d];
366 remaining /= dims[d];
367 }
368 return multi;
369}
370
374inline int multi_to_flat_index(const std::vector<int> &multi, const std::vector<int> &dims) {
375 int flat = 0;
376 int stride = 1;
377 for (size_t d = 0; d < dims.size(); ++d) {
378 flat += multi[d] * stride;
379 stride *= dims[d];
380 }
381 return flat;
382}
383
390inline double hermite_interpn_numeric(const std::vector<std::vector<double>> &grid,
391 const std::vector<double> &values,
392 const std::vector<double> &query) {
393 int n_dims = static_cast<int>(grid.size());
394
395 // Build dimension sizes
396 std::vector<int> dims(n_dims);
397 for (int d = 0; d < n_dims; ++d) {
398 dims[d] = static_cast<int>(grid[d].size());
399 }
400
401 // For N-D interpolation, we use recursive tensor product:
402 // 1. Interpolate along first dimension to reduce to (n-1)-D
403 // 2. Repeat until we have a scalar
404
405 // Start with the full values array
406 std::vector<double> current_values = values;
407 std::vector<int> current_dims = dims;
408
409 for (int d = 0; d < n_dims; ++d) {
410 const std::vector<double> &x = grid[d];
411 int n_x = current_dims[0]; // Size in first (current) dimension
412
413 // Compute size of remaining dimensions
414 int remaining_size = 1;
415 for (size_t dd = 1; dd < current_dims.size(); ++dd) {
416 remaining_size *= current_dims[dd];
417 }
418
419 // Interpolate along first dimension for each slice
420 std::vector<double> new_values(remaining_size);
421
422 for (int slice = 0; slice < remaining_size; ++slice) {
423 // Extract 1D slice along first dimension
424 std::vector<double> y_slice(n_x);
425 for (int i = 0; i < n_x; ++i) {
426 int flat_idx = i + slice * n_x;
427 y_slice[i] = current_values[flat_idx];
428 }
429
430 // Interpolate this slice
431 new_values[slice] = hermite_interp_1d_numeric(x, y_slice, query[d]);
432 }
433
434 // Update for next dimension
435 current_values = std::move(new_values);
436 current_dims.erase(current_dims.begin());
437 }
438
439 return current_values[0];
440}
441
442} // namespace detail
443
444// ============================================================================
445// Unified Interpolator Class
446// ============================================================================
447
490 private:
491 std::vector<std::vector<double>> m_grid;
492 std::vector<double> m_values;
493 casadi::Function m_casadi_fn;
495 int m_dims = 0;
496 bool m_valid = false;
497 bool m_use_custom_hermite = false;
498
499 // Extrapolation configuration
500 ExtrapolationConfig m_extrap_config;
501
502 // Precomputed boundary partial derivatives for linear extrapolation
503 // For each dimension d: slope of f w.r.t. x_d at left/right boundary
504 std::vector<double> m_slopes_left; // ∂f/∂x_d at x_d = x_d_min (per dimension)
505 std::vector<double> m_slopes_right; // ∂f/∂x_d at x_d = x_d_max (per dimension)
506
507 public:
511 Interpolator() = default;
512
521 Interpolator(const std::vector<NumericVector> &points, const NumericVector &values,
523 if (points.empty()) {
524 throw InterpolationError("Interpolator: points cannot be empty");
525 }
526
527 m_dims = static_cast<int>(points.size());
528 m_method = method;
529
530 // Convert to std::vector for internal storage
531 m_grid.resize(m_dims);
532 int expected_size = 1;
533 for (int d = 0; d < m_dims; ++d) {
534 if (points[d].size() < 2) {
535 throw InterpolationError("Interpolator: need at least 2 grid points in dimension " +
536 std::to_string(d));
537 }
538 m_grid[d].resize(points[d].size());
539 Eigen::VectorXd::Map(m_grid[d].data(), points[d].size()) = points[d];
540
541 if (!std::is_sorted(m_grid[d].begin(), m_grid[d].end())) {
542 throw InterpolationError("Interpolator: points[" + std::to_string(d) +
543 "] must be sorted");
544 }
545 expected_size *= static_cast<int>(points[d].size());
546 }
547
548 // Validate values size
549 if (values.size() != expected_size) {
550 throw InterpolationError("Interpolator: values size mismatch. Expected " +
551 std::to_string(expected_size) + ", got " +
552 std::to_string(values.size()));
553 }
554
555 m_values.resize(values.size());
556 Eigen::VectorXd::Map(m_values.data(), values.size()) = values;
557
558 // Method-specific setup
559 setup_method(method);
560 }
561
574 if (x.size() != y.size()) {
575 throw InterpolationError("Interpolator: x and y must have same size");
576 }
577 if (x.size() < 2) {
578 throw InterpolationError("Interpolator: need at least 2 grid points");
579 }
580
581 m_dims = 1;
582 m_method = method;
583
584 // Store grid and values
585 m_grid.resize(1);
586 m_grid[0].resize(x.size());
587 Eigen::VectorXd::Map(m_grid[0].data(), x.size()) = x;
588
589 if (!std::is_sorted(m_grid[0].begin(), m_grid[0].end())) {
590 throw InterpolationError("Interpolator: x grid must be sorted");
591 }
592
593 m_values.resize(y.size());
594 Eigen::VectorXd::Map(m_values.data(), y.size()) = y;
595
596 // Method-specific setup
597 setup_method(method);
598 }
599
609 Interpolator(const std::vector<NumericVector> &points, const NumericVector &values,
611 : Interpolator(points, values, method) {
612 m_extrap_config = extrap;
613 compute_boundary_slopes();
614 }
615
633 ExtrapolationConfig extrap)
634 : Interpolator(x, y, method) {
635 m_extrap_config = extrap;
636 compute_boundary_slopes();
637 }
638
639 // ========================================================================
640 // Properties
641 // ========================================================================
642
646 int dims() const { return m_dims; }
647
651 InterpolationMethod method() const { return m_method; }
652
656 bool valid() const { return m_valid; }
657
658 // ========================================================================
659 // Scalar Query (1D only)
660 // ========================================================================
661
669 template <JanusScalar Scalar> Scalar operator()(const Scalar &query) const {
670 if (!m_valid)
671 throw InterpolationError("Interpolator: not initialized");
672 if (m_dims != 1)
673 throw InterpolationError("Interpolator: scalar query only valid for 1D");
674
675 if constexpr (std::is_floating_point_v<Scalar>) {
676 return eval_numeric_scalar(query);
677 } else {
678 if (m_use_custom_hermite) {
680 }
681 return eval_symbolic_scalar(query);
682 }
683 }
684
685 // ========================================================================
686 // N-D Point Query
687 // ========================================================================
688
699 template <JanusScalar Scalar> Scalar operator()(const JanusVector<Scalar> &query) const {
700 if (!m_valid)
701 throw InterpolationError("Interpolator: not initialized");
702
703 if (query.size() != m_dims) {
704 throw InterpolationError("Interpolator: query size must match dims. Expected " +
705 std::to_string(m_dims) + ", got " +
706 std::to_string(query.size()));
707 }
708
709 if constexpr (std::is_floating_point_v<Scalar>) {
710 return eval_numeric_point(query);
711 } else {
712 if (m_use_custom_hermite) {
714 }
715 return eval_symbolic_point(query);
716 }
717 }
718
719 // ========================================================================
720 // Batch Query (Multiple Points)
721 // ========================================================================
722
733 template <typename Derived>
734 auto operator()(const Eigen::MatrixBase<Derived> &queries) const
736 using Scalar = typename Derived::Scalar;
737
738 if (!m_valid)
739 throw InterpolationError("Interpolator: not initialized");
740
741 int n_points;
742 if (m_dims == 1) {
743 // 1D: flatten input and treat each element as a query point
744 n_points = static_cast<int>(queries.size());
745 } else {
746 // N-D: determine shape
747 bool is_transposed = (queries.rows() == m_dims && queries.cols() != m_dims);
748 n_points =
749 is_transposed ? static_cast<int>(queries.cols()) : static_cast<int>(queries.rows());
750 }
751
752 JanusVector<Scalar> result(n_points);
753
754 if constexpr (std::is_floating_point_v<Scalar>) {
755 if (m_dims == 1) {
756 // 1D: iterate over all elements using coefficient access
757 for (int i = 0; i < n_points; ++i) {
758 // Use linear coefficient access (works for any Eigen expression)
759 Scalar val;
760 if (queries.cols() == 1) {
761 val = queries(i, 0);
762 } else {
763 val = queries(0, i);
764 }
765 result(i) = eval_numeric_scalar(val);
766 }
767 } else {
768 // N-D: handle row/col orientation
769 bool is_transposed = (queries.rows() == m_dims && queries.cols() != m_dims);
770 for (int i = 0; i < n_points; ++i) {
771 NumericVector point(m_dims);
772 for (int d = 0; d < m_dims; ++d) {
773 point(d) = is_transposed ? queries(d, i) : queries(i, d);
774 }
775 result(i) = eval_numeric_point(point);
776 }
777 }
778 } else {
779 if (m_use_custom_hermite) {
781 }
782 if (m_dims == 1) {
783 for (int i = 0; i < n_points; ++i) {
784 Scalar val;
785 if (queries.cols() == 1) {
786 val = queries(i, 0);
787 } else {
788 val = queries(0, i);
789 }
790 result(i) = eval_symbolic_scalar(val);
791 }
792 } else {
793 bool is_transposed = (queries.rows() == m_dims && queries.cols() != m_dims);
794 for (int i = 0; i < n_points; ++i) {
795 SymbolicVector point(m_dims);
796 for (int d = 0; d < m_dims; ++d) {
797 point(d) = is_transposed ? queries(d, i) : queries(i, d);
798 }
799 result(i) = eval_symbolic_point(point);
800 }
801 }
802 }
803
804 return result;
805 }
806
807 private:
808 // ========================================================================
809 // Setup
810 // ========================================================================
811
812 void setup_method(InterpolationMethod method) {
814 // Hermite uses custom implementation, no CasADi interpolant needed
815 m_use_custom_hermite = true;
816 m_valid = true;
817 } else if (method == InterpolationMethod::BSpline) {
818 // BSpline requires at least 4 points per dimension for cubic
819 for (int d = 0; d < m_dims; ++d) {
820 if (m_grid[d].size() < 4) {
821 throw InterpolationError(
822 "Interpolator: BSpline requires at least 4 grid points in dimension " +
823 std::to_string(d));
824 }
825 }
826 m_casadi_fn = casadi::interpolant("interp", "bspline", m_grid, m_values);
827 m_valid = true;
828 } else if (method == InterpolationMethod::Nearest) {
829 // Nearest neighbor - use linear CasADi but round in eval
830 m_casadi_fn = casadi::interpolant("interp", "linear", m_grid, m_values);
831 m_valid = true;
832 } else {
833 // Linear (default)
834 m_casadi_fn = casadi::interpolant("interp", "linear", m_grid, m_values);
835 m_valid = true;
836 }
837 }
838
845 void compute_boundary_slopes() {
846 if (m_extrap_config.mode != ExtrapolationMode::Linear) {
847 return;
848 }
849
850 m_slopes_left.resize(m_dims, 0.0);
851 m_slopes_right.resize(m_dims, 0.0);
852
853 // For each dimension, compute partial derivative at boundaries
854 for (int d = 0; d < m_dims; ++d) {
855 const auto &grid_d = m_grid[d];
856 size_t n_d = grid_d.size();
857 if (n_d < 2)
858 continue;
859
860 double x_min = grid_d.front();
861 double x_max = grid_d.back();
862 double h_left = grid_d[1] - grid_d[0];
863 double h_right = grid_d[n_d - 1] - grid_d[n_d - 2];
864
865 // Create query point at center of other dimensions
866 std::vector<double> query_base(m_dims);
867 for (int dd = 0; dd < m_dims; ++dd) {
868 // Use midpoint of grid for other dimensions
869 query_base[dd] = 0.5 * (m_grid[dd].front() + m_grid[dd].back());
870 }
871
872 // Left boundary: finite difference using first two grid points
873 query_base[d] = x_min;
874 std::vector<casadi::DM> args0 = {casadi::DM(query_base)};
875 double f0 = static_cast<double>(m_casadi_fn(args0)[0]);
876
877 query_base[d] = x_min + h_left;
878 std::vector<casadi::DM> args1 = {casadi::DM(query_base)};
879 double f1 = static_cast<double>(m_casadi_fn(args1)[0]);
880
881 m_slopes_left[d] = (f1 - f0) / h_left;
882
883 // Right boundary: finite difference using last two grid points
884 query_base[d] = x_max - h_right;
885 std::vector<casadi::DM> args2 = {casadi::DM(query_base)};
886 double f2 = static_cast<double>(m_casadi_fn(args2)[0]);
887
888 query_base[d] = x_max;
889 std::vector<casadi::DM> args3 = {casadi::DM(query_base)};
890 double f3 = static_cast<double>(m_casadi_fn(args3)[0]);
891
892 m_slopes_right[d] = (f3 - f2) / h_right;
893 }
894 }
895
899 double apply_output_bounds(double value) const {
900 if (m_extrap_config.lower_bound.has_value()) {
901 value = std::max(value, m_extrap_config.lower_bound.value());
902 }
903 if (m_extrap_config.upper_bound.has_value()) {
904 value = std::min(value, m_extrap_config.upper_bound.value());
905 }
906 return value;
907 }
908
912 SymbolicScalar apply_output_bounds_sym(const SymbolicScalar &value) const {
913 SymbolicScalar result = value;
914 if (m_extrap_config.lower_bound.has_value()) {
915 result = SymbolicScalar::fmax(result, m_extrap_config.lower_bound.value());
916 }
917 if (m_extrap_config.upper_bound.has_value()) {
918 result = SymbolicScalar::fmin(result, m_extrap_config.upper_bound.value());
919 }
920 return result;
921 }
922
923 // ========================================================================
924 // Numeric Evaluation
925 // ========================================================================
926
927 double eval_numeric_scalar(double query) const {
928 const double x_min = m_grid[0].front();
929 const double x_max = m_grid[0].back();
930
931 // Check if we need linear extrapolation
932 if (m_extrap_config.mode == ExtrapolationMode::Linear && !m_slopes_left.empty()) {
933 if (query < x_min) {
934 // Left extrapolation: y = y[0] + slope_left * (x - x_min)
935 double result = m_values.front() + m_slopes_left[0] * (query - x_min);
936 return apply_output_bounds(result);
937 } else if (query > x_max) {
938 // Right extrapolation: y = y[n-1] + slope_right * (x - x_max)
939 double result = m_values.back() + m_slopes_right[0] * (query - x_max);
940 return apply_output_bounds(result);
941 }
942 // In bounds: fall through to normal interpolation
943 }
944
945 // Clamp to bounds (default behavior or in-bounds query)
946 double clamped = std::max(query, x_min);
947 clamped = std::min(clamped, x_max);
948
949 double result;
950 if (m_use_custom_hermite) {
951 result = detail::hermite_interp_1d_numeric(m_grid[0], m_values, clamped);
952 } else if (m_method == InterpolationMethod::Nearest) {
953 // Snap to nearest grid point
954 auto it = std::lower_bound(m_grid[0].begin(), m_grid[0].end(), clamped);
955 size_t idx = std::distance(m_grid[0].begin(), it);
956 if (idx > 0 && (idx == m_grid[0].size() || std::abs(clamped - m_grid[0][idx - 1]) <
957 std::abs(clamped - m_grid[0][idx]))) {
958 idx--;
959 }
960 result = m_values[idx];
961 } else {
962 // Linear/BSpline: Use CasADi
963 std::vector<casadi::DM> args = {casadi::DM(clamped)};
964 std::vector<casadi::DM> res = m_casadi_fn(args);
965 result = static_cast<double>(res[0]);
966 }
967
968 return apply_output_bounds(result);
969 }
970
971 double eval_numeric_point(const NumericVector &query) const {
972 // Check for out-of-bounds dimensions and compute extrapolation corrections
973 double extrap_correction = 0.0;
974 bool needs_extrapolation = false;
975
976 if (m_extrap_config.mode == ExtrapolationMode::Linear && !m_slopes_left.empty()) {
977 for (int d = 0; d < m_dims; ++d) {
978 double val = query(d);
979 double x_min = m_grid[d].front();
980 double x_max = m_grid[d].back();
981
982 if (val < x_min) {
983 // Left extrapolation for dimension d
984 extrap_correction += m_slopes_left[d] * (val - x_min);
985 needs_extrapolation = true;
986 } else if (val > x_max) {
987 // Right extrapolation for dimension d
988 extrap_correction += m_slopes_right[d] * (val - x_max);
989 needs_extrapolation = true;
990 }
991 }
992 }
993
994 // Build clamped query
995 std::vector<double> clamped(m_dims);
996 for (int d = 0; d < m_dims; ++d) {
997 double val = query(d);
998 val = std::max(val, m_grid[d].front());
999 val = std::min(val, m_grid[d].back());
1000 clamped[d] = val;
1001 }
1002
1003 double result;
1004 if (m_use_custom_hermite) {
1005 result = detail::hermite_interpn_numeric(m_grid, m_values, clamped);
1006 } else if (m_method == InterpolationMethod::Nearest) {
1007 // TODO: Implement N-D nearest neighbor
1008 // For now, fall through to linear
1009 std::vector<casadi::DM> args = {casadi::DM(clamped)};
1010 std::vector<casadi::DM> res = m_casadi_fn(args);
1011 result = static_cast<double>(res[0]);
1012 } else {
1013 // Linear/BSpline: Use CasADi
1014 std::vector<casadi::DM> args = {casadi::DM(clamped)};
1015 std::vector<casadi::DM> res = m_casadi_fn(args);
1016 result = static_cast<double>(res[0]);
1017 }
1018
1019 // Add extrapolation correction
1020 if (needs_extrapolation) {
1021 result += extrap_correction;
1022 }
1023
1024 return apply_output_bounds(result);
1025 }
1026
1027 // ========================================================================
1028 // Symbolic Evaluation
1029 // ========================================================================
1030
1031 SymbolicScalar eval_symbolic_scalar(const SymbolicScalar &query) const {
1032 const double x_min = m_grid[0].front();
1033 const double x_max = m_grid[0].back();
1034
1035 // Clamped query for interpolation
1036 SymbolicScalar clamped = SymbolicScalar::fmax(query, x_min);
1037 clamped = SymbolicScalar::fmin(clamped, x_max);
1038
1039 // Get interpolated result at clamped location
1040 std::vector<casadi::MX> args = {clamped};
1041 std::vector<casadi::MX> res = m_casadi_fn(args);
1042 SymbolicScalar interp_result = res[0];
1043
1044 // Handle extrapolation if configured
1045 if (m_extrap_config.mode == ExtrapolationMode::Linear && !m_slopes_left.empty()) {
1046 // Left extrapolation: y = y[0] + slope_left * (x - x_min)
1047 SymbolicScalar left_extrap = m_values.front() + m_slopes_left[0] * (query - x_min);
1048
1049 // Right extrapolation: y = y[n-1] + slope_right * (x - x_max)
1050 SymbolicScalar right_extrap = m_values.back() + m_slopes_right[0] * (query - x_max);
1051
1052 // Use if_else for smooth symbolic branching
1053 SymbolicScalar result = casadi::MX::if_else(
1054 query < x_min, left_extrap,
1055 casadi::MX::if_else(query > x_max, right_extrap, interp_result));
1056
1057 return apply_output_bounds_sym(result);
1058 }
1059
1060 return apply_output_bounds_sym(interp_result);
1061 }
1062
1063 SymbolicScalar eval_symbolic_point(const SymbolicVector &query) const {
1064 // Build clamped query
1065 casadi::MX clamped(m_dims, 1);
1066 for (int d = 0; d < m_dims; ++d) {
1067 casadi::MX val = query(d);
1068 val = casadi::MX::fmax(val, m_grid[d].front());
1069 val = casadi::MX::fmin(val, m_grid[d].back());
1070 clamped(d) = val;
1071 }
1072
1073 // Get interpolated result at clamped location
1074 std::vector<casadi::MX> args = {clamped};
1075 std::vector<casadi::MX> res = m_casadi_fn(args);
1076 SymbolicScalar interp_result = res[0];
1077
1078 // Handle N-D extrapolation if configured
1079 if (m_extrap_config.mode == ExtrapolationMode::Linear && !m_slopes_left.empty()) {
1080 // Add extrapolation corrections for each out-of-bounds dimension
1081 SymbolicScalar extrap_correction = 0.0;
1082
1083 for (int d = 0; d < m_dims; ++d) {
1084 double x_min = m_grid[d].front();
1085 double x_max = m_grid[d].back();
1086 casadi::MX val = query(d);
1087
1088 // Left extrapolation correction: slope * (x - x_min) when x < x_min
1089 SymbolicScalar left_corr = m_slopes_left[d] * (val - x_min);
1090 SymbolicScalar left_contrib = casadi::MX::if_else(val < x_min, left_corr, 0.0);
1091
1092 // Right extrapolation correction: slope * (x - x_max) when x > x_max
1093 SymbolicScalar right_corr = m_slopes_right[d] * (val - x_max);
1094 SymbolicScalar right_contrib = casadi::MX::if_else(val > x_max, right_corr, 0.0);
1095
1096 extrap_correction = extrap_correction + left_contrib + right_contrib;
1097 }
1098
1099 return apply_output_bounds_sym(interp_result + extrap_correction);
1100 }
1101
1102 return apply_output_bounds_sym(interp_result);
1103 }
1104};
1105
1106// ============================================================================
1107// Free Function API (Backwards Compatibility)
1108// ============================================================================
1109
1126template <typename Scalar>
1127JanusVector<Scalar> interpn(const std::vector<NumericVector> &points,
1128 const NumericVector &values_flat, const JanusMatrix<Scalar> &xi,
1130 std::optional<Scalar> fill_value = std::nullopt) {
1131 const auto grid_data = detail::build_interpn_grid(points, "interpn");
1132 const int n_dims = static_cast<int>(points.size());
1133 JanusMatrix<Scalar> xi_work = detail::normalize_query_matrix(xi, n_dims, "interpn");
1134 const int n_points = static_cast<int>(xi_work.rows());
1135
1136 // Validate values size
1137 if (values_flat.size() != grid_data.expected_size) {
1138 throw InterpolationError("interpn: values_flat size mismatch. Expected " +
1139 std::to_string(grid_data.expected_size) + ", got " +
1140 std::to_string(values_flat.size()));
1141 }
1142
1143 // Create interpolator
1144 Interpolator interp(points, values_flat, method);
1145
1146 // Prepare result
1147 JanusVector<Scalar> result(n_points);
1148
1149 // Handle fill_value for out-of-bounds
1150 if (fill_value.has_value()) {
1151 for (int i = 0; i < n_points; ++i) {
1152 bool out_of_bounds = false;
1153 if constexpr (std::is_floating_point_v<Scalar>) {
1154 NumericVector point = xi_work.row(i).transpose();
1155 out_of_bounds = detail::point_out_of_bounds_numeric(point, grid_data.grid);
1156 }
1157
1158 if (out_of_bounds) {
1159 result(i) = fill_value.value();
1160 } else {
1161 JanusVector<Scalar> point = xi_work.row(i).transpose();
1162 result(i) = interp(point);
1163 }
1164 }
1165 } else {
1166 // No fill_value, allow extrapolation (clamp)
1167 for (int i = 0; i < n_points; ++i) {
1168 JanusVector<Scalar> point = xi_work.row(i).transpose();
1169 result(i) = interp(point);
1170 }
1171 }
1172
1173 return result;
1174}
1175
1191template <typename Scalar>
1192SymbolicVector interpn(const std::vector<NumericVector> &points, const SymbolicVector &values_flat,
1193 const JanusMatrix<Scalar> &xi,
1195 std::optional<SymbolicScalar> fill_value = std::nullopt) {
1196 const auto grid_data = detail::build_interpn_grid(points, "interpn");
1197 const int n_dims = static_cast<int>(points.size());
1198 JanusMatrix<Scalar> xi_work = detail::normalize_query_matrix(xi, n_dims, "interpn");
1199 const int n_points = static_cast<int>(xi_work.rows());
1200
1201 if (values_flat.size() != grid_data.expected_size) {
1202 throw InterpolationError("interpn: values_flat size mismatch. Expected " +
1203 std::to_string(grid_data.expected_size) + ", got " +
1204 std::to_string(values_flat.size()));
1205 }
1206
1207 casadi::Function interp = detail::make_parametric_interpolant(grid_data.grid, method);
1208 const SymbolicScalar coeffs = janus::as_mx(values_flat);
1209
1210 SymbolicVector result(n_points);
1211 for (int i = 0; i < n_points; ++i) {
1212 JanusVector<Scalar> point = xi_work.row(i).transpose();
1213 SymbolicScalar clamped_point = detail::clamp_query_point(point, grid_data.grid);
1214 SymbolicScalar interp_value = interp(std::vector<SymbolicScalar>{clamped_point, coeffs})[0];
1215
1216 if (fill_value.has_value()) {
1217 if constexpr (std::is_floating_point_v<Scalar>) {
1218 result(i) = detail::point_out_of_bounds_numeric(point, grid_data.grid)
1219 ? fill_value.value()
1220 : interp_value;
1221 } else {
1222 SymbolicScalar out_of_bounds =
1223 detail::point_out_of_bounds_symbolic(point, grid_data.grid);
1224 result(i) =
1225 SymbolicScalar::if_else(out_of_bounds, fill_value.value(), interp_value);
1226 }
1227 } else {
1228 result(i) = interp_value;
1229 }
1230 }
1231
1232 return result;
1233}
1234
1235} // namespace janus
C++20 concepts constraining valid Janus scalar types.
Custom exception hierarchy for Janus framework.
Core type aliases for numeric and symbolic Eigen/CasADi interop.
Linear algebra operations (solve, inverse, determinant, eigendecomposition, norms).
Interpolation-specific errors.
Definition JanusError.hpp:51
Definition Interpolate.hpp:489
Interpolator(const std::vector< NumericVector > &points, const NumericVector &values, InterpolationMethod method=InterpolationMethod::Linear)
Construct N-dimensional interpolator.
Definition Interpolate.hpp:521
Scalar operator()(const Scalar &query) const
Evaluate interpolant at a scalar point (1D only).
Definition Interpolate.hpp:669
Interpolator(const NumericVector &x, const NumericVector &y, InterpolationMethod method=InterpolationMethod::Linear)
Construct 1D interpolator (convenience overload).
Definition Interpolate.hpp:572
Interpolator(const std::vector< NumericVector > &points, const NumericVector &values, InterpolationMethod method, ExtrapolationConfig extrap)
Construct N-dimensional interpolator with extrapolation config.
Definition Interpolate.hpp:609
InterpolationMethod method() const
Get the interpolation method.
Definition Interpolate.hpp:651
bool valid() const
Check if interpolator is valid (initialized).
Definition Interpolate.hpp:656
Scalar operator()(const JanusVector< Scalar > &query) const
Evaluate N-D interpolant at a single point.
Definition Interpolate.hpp:699
auto operator()(const Eigen::MatrixBase< Derived > &queries) const -> JanusVector< typename Derived::Scalar >
Evaluate interpolant at multiple points.
Definition Interpolate.hpp:734
Interpolator()=default
Default constructor (invalid state).
int dims() const
Get number of dimensions.
Definition Interpolate.hpp:646
Interpolator(const NumericVector &x, const NumericVector &y, InterpolationMethod method, ExtrapolationConfig extrap)
Definition Interpolate.hpp:632
auto slice(const Eigen::MatrixBase< Derived > &m, Eigen::Index start, Eigen::Index end)
Definition IntegrateDiscrete.hpp:32
JanusVector< typename Derived::Scalar > flatten_fortran_order(const Eigen::MatrixBase< Derived > &values)
Flatten N-D values array in Fortran order for CasADi.
Definition Interpolate.hpp:115
std::string method_to_casadi_string(InterpolationMethod method)
Convert InterpolationMethod enum to CasADi string.
Definition Interpolate.hpp:86
double hermite_interp_1d_numeric(const std::vector< double > &x, const std::vector< double > &y, double query)
Evaluate 1D Catmull-Rom cubic Hermite spline (numeric only).
Definition Interpolate.hpp:306
double hermite_interpn_numeric(const std::vector< std::vector< double > > &grid, const std::vector< double > &values, const std::vector< double > &query)
N-dimensional Catmull-Rom interpolation via tensor product (numeric only).
Definition Interpolate.hpp:390
int multi_to_flat_index(const std::vector< int > &multi, const std::vector< int > &dims)
Compute flat index from multi-dimensional index (Fortran order).
Definition Interpolate.hpp:374
casadi::Function make_parametric_interpolant(const std::vector< std::vector< double > > &grid, InterpolationMethod method)
Definition Interpolate.hpp:242
bool point_out_of_bounds_numeric(const JanusVector< Scalar > &point, const std::vector< std::vector< double > > &grid)
Definition Interpolate.hpp:221
double catmull_rom_slope(const std::vector< double > &x, const std::vector< double > &y, size_t i)
Compute Catmull-Rom slope at a point using neighboring values.
Definition Interpolate.hpp:271
void validate_bspline_grid(const std::vector< std::vector< double > > &grid, const char *context)
Definition Interpolate.hpp:180
InterpnGridData build_interpn_grid(const std::vector< NumericVector > &points, const char *context)
Definition Interpolate.hpp:148
std::vector< int > flat_to_multi_index(int flat_idx, const std::vector< int > &dims)
Compute multi-dimensional index from flat index (Fortran order).
Definition Interpolate.hpp:361
const char * hermite_symbolic_error_message()
Definition Interpolate.hpp:101
JanusMatrix< Scalar > normalize_query_matrix(const JanusMatrix< Scalar > &xi, int n_dims, const char *context)
Definition Interpolate.hpp:192
SymbolicScalar point_out_of_bounds_symbolic(const SymbolicVector &point, const std::vector< std::vector< double > > &grid)
Definition Interpolate.hpp:232
SymbolicScalar clamp_query_point(const JanusVector< Scalar > &point, const std::vector< std::vector< double > > &grid)
Definition Interpolate.hpp:207
std::string symbolic_values_method_error_message(InterpolationMethod method)
Definition Interpolate.hpp:129
Definition Diagnostics.hpp:19
JanusVector< SymbolicScalar > SymbolicVector
Eigen vector of MX elements.
Definition JanusTypes.hpp:72
InterpolationMethod
Supported interpolation methods.
Definition Interpolate.hpp:29
@ Linear
Piecewise linear (C0 continuous) - fast, simple.
Definition Interpolate.hpp:30
@ Nearest
Nearest neighbor - fast, non-differentiable.
Definition Interpolate.hpp:33
@ Hermite
Cubic Hermite/Catmull-Rom (C1 continuous) - smooth gradients.
Definition Interpolate.hpp:31
@ BSpline
Cubic B-spline (C2 continuous) - smoothest, good for optimization.
Definition Interpolate.hpp:32
Eigen::Matrix< Scalar, Eigen::Dynamic, Eigen::Dynamic > JanusMatrix
Dynamic-size matrix for both numeric and symbolic backends.
Definition JanusTypes.hpp:43
ExtrapolationMode
Controls behavior when query points fall outside grid bounds.
Definition Interpolate.hpp:43
@ Clamp
Clamp queries to grid bounds (default, safe).
Definition Interpolate.hpp:44
@ Linear
Linear extrapolation from boundary slope (1D only).
Definition Interpolate.hpp:45
JanusVector< Scalar > interpn(const std::vector< NumericVector > &points, const NumericVector &values_flat, const JanusMatrix< Scalar > &xi, InterpolationMethod method=InterpolationMethod::Linear, std::optional< Scalar > fill_value=std::nullopt)
N-dimensional interpolation on regular grids (backwards compatibility).
Definition Interpolate.hpp:1127
JanusVector< NumericScalar > NumericVector
Eigen::VectorXd equivalent.
Definition JanusTypes.hpp:67
@ Hermite
Definition AutoDiff.hpp:31
Eigen::Matrix< Scalar, Eigen::Dynamic, 1 > JanusVector
Dynamic-size column vector for both numeric and symbolic backends.
Definition JanusTypes.hpp:49
casadi::MX SymbolicScalar
CasADi MX symbolic scalar.
Definition JanusTypes.hpp:70
SymbolicScalar as_mx(const SymbolicVector &v)
Get the underlying MX representation of a SymbolicVector.
Definition JanusTypes.hpp:173
Configuration for extrapolation behavior.
Definition Interpolate.hpp:60
std::optional< double > upper_bound
Definition Interpolate.hpp:65
ExtrapolationMode mode
Definition Interpolate.hpp:61
std::optional< double > lower_bound
Safety bounds applied to output values (nullopt = unbounded).
Definition Interpolate.hpp:64
static ExtrapolationConfig linear(std::optional< double > lower=std::nullopt, std::optional< double > upper=std::nullopt)
Factory: create linear extrapolation config with optional bounds.
Definition Interpolate.hpp:71
static ExtrapolationConfig clamp()
Factory: create clamp config (default, safe).
Definition Interpolate.hpp:68
Definition Interpolate.hpp:143
std::vector< std::vector< double > > grid
Definition Interpolate.hpp:144
int expected_size
Definition Interpolate.hpp:145