Brick
Loading...
Searching...
No Matches
spline_base.h
1#pragma once
2
3#include <vector>
4
5#include "core/math/geometry/rotation2d.h"
6#include "core/math/geometry/translation2d.h"
7#include "core/math/units.h"
8
9using namespace units::literals;
10
11/**
12 * @brief Evaluated state sample at a specific parameter u or distance s along a spline.
13 */
15 double u = 0.0; ///< Spline parameter in range [0, 1]
16 double s = 0.0; ///< Arc length along spline from start in inches
17 Translation2d position; ///< 2D position (x, y)
18 Translation2d velocity; ///< 1D/2D parametric velocity vector (dx/du, dy/du)
19 Translation2d acceleration; ///< 1D/2D parametric acceleration vector (d^2x/du^2, d^2y/du^2)
20 Rotation2d heading; ///< Heading angle theta along path
21 units::Curvature curvature = 0_radpm; ///< Path curvature (rad/meter)
22};
23
24/**
25 * @brief Abstract base class for parametric 2D Hermite splines.
26 *
27 * Provides arc-length parameterization, heading, curvature, sampling,
28 * and high-accuracy Gauss-Legendre quadrature integration.
29 */
31 public:
32 virtual ~SplineBase() = default;
33
34 /**
35 * @brief Computes 2D position at spline parameter u.
36 * @param u Parameter in range [0, 1].
37 * @return 2D position vector.
38 */
39 virtual Translation2d position(double u) const = 0;
40
41 /**
42 * @brief Computes 2D parametric velocity (first derivative) at parameter u.
43 * @param u Parameter in range [0, 1].
44 * @return First derivative vector (dx/du, dy/du).
45 */
46 virtual Translation2d velocity(double u) const = 0;
47
48 /**
49 * @brief Computes 2D parametric acceleration (second derivative) at parameter u.
50 * @param u Parameter in range [0, 1].
51 * @return Second derivative vector (d^2x/du^2, d^2y/du^2).
52 */
53 virtual Translation2d acceleration(double u) const = 0;
54
55 /**
56 * @brief Computes heading angle theta at spline parameter u.
57 * @param u Parameter in range [0, 1].
58 * @return Rotation2d heading angle.
59 */
60 Rotation2d heading(double u) const {
61 const Translation2d vel = velocity(clamp_u(u));
62 return vel.norm().to(units::in) > 1e-9 ? vel.theta() : Rotation2d();
63 }
64
65 /**
66 * @brief Computes path curvature at parameter u.
67 * @param u Parameter in range [0, 1].
68 * @return units::Curvature in rad/meter.
69 */
70 units::Curvature curvature(double u) const {
71 const Translation2d vel = velocity(clamp_u(u));
72 const Translation2d acc = acceleration(clamp_u(u));
73 const units::Length speed = vel.norm();
74 if (speed < 1e-4_in) {
75 return 0_radpm;
76 }
77
78 const units::Area cross_product = (vel.x() * acc.y()) - (vel.y() * acc.x());
79 return (cross_product * 1_rad) / units::cube(speed);
80 }
81
82 /**
83 * @brief Returns total arc length of the spline in inches.
84 * @return Total arc length.
85 */
86 double length() const { return arc_lengths_.empty() ? 0.0 : arc_lengths_.back(); }
87
88 /**
89 * @brief Inverts arc distance s to spline parameter u.
90 * @param s Arc distance along spline in inches.
91 * @return Spline parameter u in range [0, 1].
92 */
93 double u_from_s(double s) const {
94 if (arc_lengths_.empty() || us_.empty()) {
95 return 0.0;
96 }
97 if (s <= 0.0) {
98 return 0.0;
99 }
100 if (s >= length()) {
101 return 1.0;
102 }
103
104 auto upper = std::lower_bound(arc_lengths_.begin(), arc_lengths_.end(), s);
105 const size_t idx = static_cast<size_t>(upper - arc_lengths_.begin());
106 if (idx == 0) {
107 return us_.front();
108 }
109
110 const double s0 = arc_lengths_[idx - 1];
111 const double s1 = arc_lengths_[idx];
112 const double u0 = us_[idx - 1];
113 const double u1 = us_[idx];
114 const double alpha = (s - s0) / std::max(s1 - s0, 1e-9);
115 return u0 + ((u1 - u0) * alpha);
116 }
117
118 /**
119 * @brief Samples complete state (position, velocity, acceleration, heading, curvature) at
120 * parameter u.
121 * @param u Parameter in range [0, 1].
122 * @return SplineSample struct.
123 */
124 SplineSample sample(double u) const {
125 const double clamped_u = clamp_u(u);
126 return SplineSample{
127 .u = clamped_u,
128 .s = s_from_u(clamped_u),
129 .position = position(clamped_u),
130 .velocity = velocity(clamped_u),
131 .acceleration = acceleration(clamped_u),
132 .heading = heading(clamped_u),
133 .curvature = curvature(clamped_u),
134 };
135 }
136
137 /**
138 * @brief Samples complete state at arc distance s.
139 * @param s Arc distance in inches.
140 * @return SplineSample struct.
141 */
142 SplineSample sample_by_s(double s) const {
143 SplineSample out = sample(u_from_s(s));
144 out.s = clamp_value(s, 0.0, length());
145 return out;
146 }
147
148 /**
149 * @brief Samples 2D positions uniformly spaced in parameter u.
150 * @param count Number of sample points.
151 * @return Vector of 2D translation points.
152 */
153 std::vector<Translation2d> sample_by_count(int count) const {
154 std::vector<Translation2d> points;
155 if (count <= 0) {
156 return points;
157 }
158 if (count == 1) {
159 points.push_back(position(0.0));
160 return points;
161 }
162
163 points.reserve(static_cast<size_t>(count));
164 for (int i = 0; i < count; ++i) {
165 const double u = static_cast<double>(i) / static_cast<double>(count - 1);
166 points.push_back(position(u));
167 }
168 return points;
169 }
170
171 /**
172 * @brief Samples 2D positions uniformly spaced by arc distance ds.
173 * @param ds Arc distance step in inches.
174 * @return Vector of 2D translation points.
175 */
176 std::vector<Translation2d> sample_by_spacing(double ds) const {
177 std::vector<Translation2d> points;
178 if (ds <= 0.0 || length() <= 0.0) {
179 return points;
180 }
181
182 for (double s = 0.0; s < length(); s += ds) {
183 points.push_back(sample_by_s(s).position);
184 }
185 points.push_back(position(1.0));
186 return points;
187 }
188
189 protected:
190 /**
191 * @brief Evaluates polynomial P(u) using Horner's method.
192 * @note Loop iterates backwards (from highest degree to lowest: coeffs[N-1] down to coeffs[0])
193 * which is the correct iteration order for Horner's method.
194 */
195 template <size_t N>
196 static double eval_poly(const std::array<double, N>& coeffs, double u) {
197 double out = 0.0;
198 for (size_t i = N; i-- > 0;) {
199 out = (out * u) + coeffs[i];
200 }
201 return out;
202 }
203
204 /**
205 * @brief Evaluates first derivative P'(u) using Horner's method.
206 */
207 template <size_t N>
208 static double eval_poly_derivative(const std::array<double, N>& coeffs, double u) {
209 double out = 0.0;
210 for (size_t i = N - 1; i-- > 0;) {
211 out = (out * u) + (coeffs[i + 1] * static_cast<double>(i + 1));
212 }
213 return out;
214 }
215
216 /**
217 * @brief Evaluates second derivative P''(u) using Horner's method.
218 */
219 template <size_t N>
220 static double eval_poly_second_derivative(const std::array<double, N>& coeffs, double u) {
221 double out = 0.0;
222 for (size_t i = N - 2; i-- > 0;) {
223 out = (out * u) + (coeffs[i + 2] * static_cast<double>((i + 1) * (i + 2)));
224 }
225 return out;
226 }
227
228 /**
229 * @brief Computes high-accuracy arc length over sub-interval [u0, u1] using 5-point
230 * Gauss-Legendre quadrature.
231 */
232 double integrate_segment_length(double u0, double u1) const {
233 constexpr double x[5] = {
234 0.0, -0.5384693101056831, 0.5384693101056831, -0.9061798459386640, 0.9061798459386640
235 };
236 constexpr double w[5] = {
237 0.5688888888888889, 0.4786286704993665, 0.4786286704993665, 0.2369268850561891,
238 0.2369268850561891
239 };
240
241 const double half_len = (u1 - u0) * 0.5;
242 const double mid = (u0 + u1) * 0.5;
243 double sum = 0.0;
244 for (int i = 0; i < 5; ++i) {
245 const double u = mid + (half_len * x[i]);
246 sum += w[i] * velocity(u).norm().to(units::in);
247 }
248 return half_len * sum;
249 }
250
251 /**
252 * @brief Recursively builds the arc table to satisfy a linear interpolation error bound.
253 */
255 double u0, double u1, double s0, double segment_length, double max_err
256 ) {
257 double mid_u = (u0 + u1) * 0.5;
258 double len1 = integrate_segment_length(u0, mid_u);
259 double len2 = integrate_segment_length(mid_u, u1);
260
261 double actual_mid_s = s0 + len1;
262 double linear_mid_s = s0 + segment_length * 0.5;
263
264 double integral_err = units::abs((len1 + len2) - segment_length);
265 double interp_err = units::abs(actual_mid_s - linear_mid_s);
266
267 if ((integral_err > max_err || interp_err > max_err) && (u1 - u0) > 1e-5) {
268 build_arc_table_recursive(u0, mid_u, s0, len1, max_err);
269 build_arc_table_recursive(mid_u, u1, actual_mid_s, len2, max_err);
270 } else {
271 us_.push_back(u1);
272 arc_lengths_.push_back(s0 + len1 + len2);
273 }
274 }
275
276 /**
277 * @brief Builds pre-computed parameter u and arc-length lookup tables using adaptive
278 * refinement.
279 * @param max_err Maximum allowable interpolation error in inches.
280 */
281 void build_arc_table(double max_err = 1e-4) {
282 us_.clear();
283 arc_lengths_.clear();
284
285 us_.push_back(0.0);
286 arc_lengths_.push_back(0.0);
287
288 const int initial_segments = 10;
289 double s0 = 0.0;
290 for (int i = 0; i < initial_segments; ++i) {
291 double u0 = static_cast<double>(i) / initial_segments;
292 double u1 = static_cast<double>(i + 1) / initial_segments;
293 double len = integrate_segment_length(u0, u1);
294 build_arc_table_recursive(u0, u1, s0, len, max_err);
295 s0 = arc_lengths_.back();
296 }
297 }
298
299 static double clamp_u(double u) { return clamp_value(u, 0.0, 1.0); }
300
301 static double clamp_value(double value, double lo, double hi) {
302 if (value < lo) {
303 return lo;
304 }
305 if (value > hi) {
306 return hi;
307 }
308 return value;
309 }
310
311 private:
312 double s_from_u(double u) const {
313 if (us_.empty() || arc_lengths_.empty()) {
314 return 0.0;
315 }
316 if (u <= 0.0) {
317 return 0.0;
318 }
319 if (u >= 1.0) {
320 return length();
321 }
322
323 auto upper = std::lower_bound(us_.begin(), us_.end(), u);
324 const size_t idx = static_cast<size_t>(upper - us_.begin());
325 if (idx == 0) {
326 return arc_lengths_.front();
327 }
328
329 const double u0 = us_[idx - 1];
330 const double u1 = us_[idx];
331 const double s0 = arc_lengths_[idx - 1];
332 const double s1 = arc_lengths_[idx];
333 const double alpha = (u - u0) / std::max(u1 - u0, 1e-9);
334 return s0 + ((s1 - s0) * alpha);
335 }
336
337 std::vector<double> us_;
338 std::vector<double> arc_lengths_;
339};
340
341/**
342 * @brief CRTP base class for Hermite splines of specific polynomial order.
343 * @tparam Order The polynomial order (e.g., 3 for Cubic, 5 for Quintic).
344 */
345template <size_t Order>
346class HermiteSpline : public SplineBase {
347 public:
348 Translation2d position(double u) const override {
349 const double clamped_u = clamp_u(u);
350 return Translation2d(eval_poly(x_, clamped_u) * 1_in, eval_poly(y_, clamped_u) * 1_in);
351 }
352
353 Translation2d velocity(double u) const override {
354 const double clamped_u = clamp_u(u);
355 return Translation2d(
356 eval_poly_derivative(x_, clamped_u) * 1_in, eval_poly_derivative(y_, clamped_u) * 1_in
357 );
358 }
359
360 Translation2d acceleration(double u) const override {
361 const double clamped_u = clamp_u(u);
362 return Translation2d(
363 eval_poly_second_derivative(x_, clamped_u) * 1_in,
364 eval_poly_second_derivative(y_, clamped_u) * 1_in
365 );
366 }
367
368 protected:
369 std::array<double, Order + 1> x_{};
370 std::array<double, Order + 1> y_{};
371};
CRTP base class for Hermite splines of specific polynomial order.
Definition spline_base.h:346
Translation2d position(double u) const override
Computes 2D position at spline parameter u.
Definition spline_base.h:348
Translation2d acceleration(double u) const override
Computes 2D parametric acceleration (second derivative) at parameter u.
Definition spline_base.h:360
Translation2d velocity(double u) const override
Computes 2D parametric velocity (first derivative) at parameter u.
Definition spline_base.h:353
constexpr Rotation2d theta() const
Gets the angle of the vector from the x axis.
Definition translation2d.h:82
constexpr Q x() const
Gets the x component.
Definition translation2d.h:64
constexpr Q norm() const
Gets the norm of the vector.
Definition translation2d.h:93
constexpr Q y() const
Gets the y component.
Definition translation2d.h:73
Definition rotation2d.h:16
constexpr Rotation2d()=default
Default Constructor for Rotation2d.
Abstract base class for parametric 2D Hermite splines.
Definition spline_base.h:30
SplineSample sample_by_s(double s) const
Samples complete state at arc distance s.
Definition spline_base.h:142
virtual Translation2d position(double u) const =0
Computes 2D position at spline parameter u.
virtual Translation2d acceleration(double u) const =0
Computes 2D parametric acceleration (second derivative) at parameter u.
static double eval_poly_derivative(const std::array< double, N > &coeffs, double u)
Evaluates first derivative P'(u) using Horner's method.
Definition spline_base.h:208
static double eval_poly(const std::array< double, N > &coeffs, double u)
Evaluates polynomial P(u) using Horner's method.
Definition spline_base.h:196
void build_arc_table_recursive(double u0, double u1, double s0, double segment_length, double max_err)
Recursively builds the arc table to satisfy a linear interpolation error bound.
Definition spline_base.h:254
Rotation2d heading(double u) const
Computes heading angle theta at spline parameter u.
Definition spline_base.h:60
SplineSample sample(double u) const
Samples complete state (position, velocity, acceleration, heading, curvature) at parameter u.
Definition spline_base.h:124
double u_from_s(double s) const
Inverts arc distance s to spline parameter u.
Definition spline_base.h:93
double length() const
Returns total arc length of the spline in inches.
Definition spline_base.h:86
static double eval_poly_second_derivative(const std::array< double, N > &coeffs, double u)
Evaluates second derivative P''(u) using Horner's method.
Definition spline_base.h:220
double integrate_segment_length(double u0, double u1) const
Computes high-accuracy arc length over sub-interval [u0, u1] using 5-point Gauss-Legendre quadrature.
Definition spline_base.h:232
void build_arc_table(double max_err=1e-4)
Builds pre-computed parameter u and arc-length lookup tables using adaptive refinement.
Definition spline_base.h:281
virtual Translation2d velocity(double u) const =0
Computes 2D parametric velocity (first derivative) at parameter u.
std::vector< Translation2d > sample_by_count(int count) const
Samples 2D positions uniformly spaced in parameter u.
Definition spline_base.h:153
units::Curvature curvature(double u) const
Computes path curvature at parameter u.
Definition spline_base.h:70
std::vector< Translation2d > sample_by_spacing(double ds) const
Samples 2D positions uniformly spaced by arc distance ds.
Definition spline_base.h:176
Definition units.h:691
Evaluated state sample at a specific parameter u or distance s along a spline.
Definition spline_base.h:14
units::Curvature curvature
Path curvature (rad/meter).
Definition spline_base.h:21
Translation2d acceleration
1D/2D parametric acceleration vector (d^2x/du^2, d^2y/du^2)
Definition spline_base.h:19
Translation2d position
2D position (x, y)
Definition spline_base.h:17
double s
Arc length along spline from start in inches.
Definition spline_base.h:16
double u
Spline parameter in range [0, 1].
Definition spline_base.h:15
Translation2d velocity
1D/2D parametric velocity vector (dx/du, dy/du)
Definition spline_base.h:18
Rotation2d heading
Heading angle theta along path.
Definition spline_base.h:20