Janus 2.0.0
High-performance C++20 dual-mode numerical framework
Loading...
Searching...
No Matches
IntegrateDiscrete.hpp
Go to the documentation of this file.
1#pragma once
7
13#include "janus/math/Logic.hpp"
14#include <Eigen/Dense>
15#include <algorithm>
16#include <string>
17
18namespace janus {
19
20namespace detail {
21
23constexpr double rms_fusion_epsilon = 1e-100;
24
25// Helper to compute dx
26template <typename DerivedX> auto compute_dx(const Eigen::MatrixBase<DerivedX> &x) {
27 return (x.tail(x.size() - 1) - x.head(x.size() - 1)).eval();
28}
29
30// Slice helpers to avoid verbose .segment calls
31template <typename Derived>
32auto slice(const Eigen::MatrixBase<Derived> &m, Eigen::Index start, Eigen::Index end) {
33 // Python style slice [start:end] (end exclusive)
34 // Eigen block takes (start, size)
35 // Handle negative indices python-style
36 Eigen::Index n = m.size();
37 Eigen::Index s = (start < 0) ? (n + start) : start;
38 Eigen::Index e = (end < 0) ? (n + end) : end;
39 if (e == 0 && end < 0)
40 e = n + end; // Handle case like [:-1] -> 0 if n=1? check logic
41 // Actually slice(0, -1) means 0 to n-1. Size n-1.
42 // If indices are out of bounds, clamp or let Eigen assert.
43
44 // Safety check for empty ranges
45 if (e <= s)
46 return m.head(0).eval();
47
48 return m.segment(s, e - s).eval();
49}
50
51template <typename DerivedA, typename DerivedB>
52auto concatenate(const Eigen::MatrixBase<DerivedA> &a, const Eigen::MatrixBase<DerivedB> &b) {
53 using Scalar = typename DerivedA::Scalar;
54 // Assume vectors
55 JanusVector<Scalar> res(a.size() + b.size());
56 res.head(a.size()) = a;
57 res.tail(b.size()) = b;
58 return res;
59}
60
61// --- Integration Methods ---
62
63template <typename DerivedF, typename DerivedX>
64auto integrate_forward_simpson(const Eigen::MatrixBase<DerivedF> &f,
65 const Eigen::MatrixBase<DerivedX> &x) {
66 auto x1 = slice(x, 0, -2);
67 auto x2 = slice(x, 1, -1);
68 auto x3 = slice(x, 2, x.size());
69
70 auto f1 = slice(f, 0, -2);
71 auto f2 = slice(f, 1, -1);
72 auto f3 = slice(f, 2, f.size());
73
74 // h = x2 - x1, hp = x3 - x2
75 auto h = (x2 - x1).eval();
76 auto hp = (x3 - x2).eval();
77
78 // q3 = 1 + hp / h
79 auto q3 = (1.0 + hp.array() / h.array()).eval();
80
81 // term = (f1 - f3 + 3*q3^2*(f1+f2) - 2*q3*(2*f1+f2)) / (6*q3*(q3-1))
82 // Use arrays for element-wise
83 auto num = f1.array() - f3.array() + 3.0 * q3.square() * (f1.array() + f2.array()) -
84 2.0 * q3 * (2.0 * f1.array() + f2.array());
85 auto den = 6.0 * q3 * (q3 - 1.0);
86
87 return (num / den).matrix().eval();
88}
89
90template <typename DerivedF, typename DerivedX>
91auto integrate_backward_simpson(const Eigen::MatrixBase<DerivedF> &f,
92 const Eigen::MatrixBase<DerivedX> &x) {
93 auto x1 = slice(x, 0, -2);
94 auto x2 = slice(x, 1, -1);
95 auto x3 = slice(x, 2, x.size());
96
97 auto f1 = slice(f, 0, -2);
98 auto f2 = slice(f, 1, -1);
99 auto f3 = slice(f, 2, f.size());
100
101 auto h = (x3 - x2).eval();
102 auto hm = (x2 - x1).eval();
103
104 // q1 = -hm / h
105 auto q1 = (-hm.array() / h.array()).eval();
106
107 // term = (f2 - f1 + 3*q1^2*(f2+f3) - 2*q1*(2*f2+f3)) / (6*q1*(q1-1))
108 auto num = f2.array() - f1.array() + 3.0 * q1.square() * (f2.array() + f3.array()) -
109 2.0 * q1 * (2.0 * f2.array() + f3.array());
110 auto den = 6.0 * q1 * (q1 - 1.0);
111
112 return (num / den).matrix().eval();
113}
114
115template <typename DerivedF, typename DerivedX>
116auto integrate_cubic(const Eigen::MatrixBase<DerivedF> &f, const Eigen::MatrixBase<DerivedX> &x) {
117 // Uses 4 points to compute integral over interval [x2, x3]
118 // Based on Lagrange polynomial through all 4 points
119 auto x1 = slice(x, 0, -3);
120 auto x2 = slice(x, 1, -2);
121 auto x3 = slice(x, 2, -1);
122 auto x4 = slice(x, 3, x.size());
123
124 auto f1 = slice(f, 0, -3);
125 auto f2 = slice(f, 1, -2);
126 auto f3 = slice(f, 2, -1);
127 auto f4 = slice(f, 3, f.size());
128
129 auto h = (x3 - x2).eval();
130 auto hm = (x2 - x1).eval();
131 auto hp = (x4 - x3).eval();
132
133 // q1 = -hm/h, q4 = 1 + hp/h
134 auto q1 = (-hm.array() / h.array()).eval();
135 auto q4 = (1.0 + hp.array() / h.array()).eval();
136
137 // Cubic formula from AeroSandbox reference
138 // avg_f = (
139 // 6*q1^3*q4^2*(f2+f3) - 4*q1^3*q4*(2*f2+f3) + 2*q1^3*(f2-f4)
140 // - 6*q1^2*q4^3*(f2+f3) + 3*q1^2*q4*(3*f2+f3) + 3*q1^2*(f4-f2)
141 // + 4*q1*q4^3*(2*f2+f3) - 3*q1*q4^2*(3*f2+f3) + q1*(f2-f4)
142 // + 2*q4^3*(f1-f2) + 3*q4^2*(f2-f1) + q4*(f1-f2)
143 // ) / (12*q1*q4*(q1-1)*(q1-q4)*(q4-1))
144
145 auto q1_2 = q1.square();
146 auto q1_3 = q1_2 * q1;
147 auto q4_2 = q4.square();
148 auto q4_3 = q4_2 * q4;
149
150 auto f2_arr = f2.array();
151 auto f3_arr = f3.array();
152 auto f1_arr = f1.array();
153 auto f4_arr = f4.array();
154
155 auto num = 6.0 * q1_3 * q4_2 * (f2_arr + f3_arr) - 4.0 * q1_3 * q4 * (2.0 * f2_arr + f3_arr) +
156 2.0 * q1_3 * (f2_arr - f4_arr) - 6.0 * q1_2 * q4_3 * (f2_arr + f3_arr) +
157 3.0 * q1_2 * q4 * (3.0 * f2_arr + f3_arr) + 3.0 * q1_2 * (f4_arr - f2_arr) +
158 4.0 * q1 * q4_3 * (2.0 * f2_arr + f3_arr) -
159 3.0 * q1 * q4_2 * (3.0 * f2_arr + f3_arr) + q1 * (f2_arr - f4_arr) +
160 2.0 * q4_3 * (f1_arr - f2_arr) + 3.0 * q4_2 * (f2_arr - f1_arr) +
161 q4 * (f1_arr - f2_arr);
162
163 auto den = 12.0 * q1 * q4 * (q1 - 1.0) * (q1 - q4) * (q4 - 1.0);
164
165 return (num / den).matrix().eval();
166}
167
168} // namespace detail
169
182template <typename DerivedF, typename DerivedX>
184integrate_discrete_intervals(const Eigen::MatrixBase<DerivedF> &f,
185 const Eigen::MatrixBase<DerivedX> &x, bool multiply_by_dx = true,
186 const std::string &method = "trapezoidal",
187 const std::string &method_endpoints = "lower_order") {
188 using Scalar = typename DerivedF::Scalar;
189
190 Eigen::Index n_points = f.size();
191 if (n_points < 2) {
192 // Return empty vector
193 return JanusVector<Scalar>(0);
194 }
195
196 auto dx = detail::compute_dx(x);
198
199 // Normalize method string
200 std::string m = method;
201 std::transform(m.begin(), m.end(), m.begin(), ::tolower);
202 // Replace spaces with underscores if needed loops? Simple find/replace for now.
203 std::replace(m.begin(), m.end(), ' ', '_');
204
205 if (m == "forward_euler" || m == "forward" || m == "euler_forward") {
206 avg_f = detail::slice(f, 0, -1);
207 } else if (m == "backward_euler" || m == "backward" || m == "euler_backward") {
208 avg_f = detail::slice(f, 1, n_points); // slice(1, n)
209 } else if (m == "trapezoidal" || m == "trapezoid" || m == "trapz") {
210 auto f_left = detail::slice(f, 0, -1);
211 auto f_right = detail::slice(f, 1, n_points);
212 avg_f = (f_left + f_right) * 0.5;
213 } else if (m == "forward_simpson" || m == "simpson_forward") {
215 // Degree 2, leaves (0, 1) intervals remaining?
216 // Forward simpson uses [0, 1, 2] to compute interval [0, 1].
217 // So given N points, we get integrals for intervals 0..N-2.
218 // We miss the last interval (N-2 to N-1).
219 // remaining_endpoint_intervals = (0, 1)
220 // Handled by endpoint logic below
221 } else if (m == "backward_simpson" || m == "simpson_backward") {
223 // remaining_endpoint_intervals = (1, 0)
224 } else if (m == "simpson") {
225 // Fused simpson: combines forward and backward with RMS fusion
226 if (n_points < 3) {
227 return integrate_discrete_intervals(f, x, multiply_by_dx, "trapezoidal",
228 method_endpoints);
229 }
230 auto res_fwd = detail::integrate_forward_simpson(f, x);
231 auto res_bwd = detail::integrate_backward_simpson(f, x);
232
233 auto first = detail::slice(res_fwd, 0, 1);
234 auto last = detail::slice(res_bwd, -1, res_bwd.size());
235
236 auto a = detail::slice(res_bwd, 0, -1);
237 auto b = detail::slice(res_fwd, 1, res_fwd.size());
238
239 // RMS fusion for middle intervals
240 auto mid_sq = (a.array().square() + b.array().square()) * 0.5 + detail::rms_fusion_epsilon;
241 JanusVector<Scalar> mid = janus::sqrt(mid_sq.matrix());
242
243 auto tmp = detail::concatenate(first, mid);
244 avg_f = detail::concatenate(tmp, last);
245 } else if (m.find("simpson") != std::string::npos && m != "simpson") {
246 // Generic simpson check, but specific variants matched above
247 throw IntegrationError("unknown Simpson variant: " + method);
248 } else if (m == "cubic" || m == "cubic_spline") {
249 if (n_points < 4) {
250 // Need at least 4 points for cubic, fallback to simpson
251 return integrate_discrete_intervals(f, x, multiply_by_dx, "simpson", method_endpoints);
252 }
253 avg_f = detail::integrate_cubic(f, x);
254 // remaining_endpoint_intervals = (1, 1) - misses first and last interval
255 } else if (m != "forward_euler" && m != "backward_euler" && m != "trapezoidal" &&
256 m != "forward" && m != "backward" && m != "euler_forward" && m != "euler_backward" &&
257 m != "trapz" && m != "trapezoid") {
258 throw IntegrationError(
259 "unknown method: " + method +
260 ". Use forward_euler, backward_euler, trapezoidal, simpson, or cubic.");
261 }
262
263 // --- Endpoint Handling ---
264
265 int remaining_start = 0;
266 int remaining_end = 0;
267 int degree = 1;
268
269 if (m.find("forward_simpson") != std::string::npos) {
270 degree = 2;
271 remaining_end = 1;
272 } else if (m.find("backward_simpson") != std::string::npos) {
273 degree = 2;
274 remaining_start = 1;
275 } else if (m.find("cubic") != std::string::npos) {
276 degree = 3;
277 remaining_start = 1;
278 remaining_end = 1;
279 } else if (m.find("euler") != std::string::npos) {
280 degree = 0;
281 }
282
283 std::string eff_endpoints = method_endpoints;
284
285 if (eff_endpoints == "lower_order") {
286 // Fallback strategy
287 std::string fallback_method = "trapezoidal";
288 if (degree >= 3)
289 fallback_method = "simpson"; // simplifies to simps recursion
290 // For Simpson, fallback is trapezoidal?
291 // Reference: if degree >= 3 endpoints="simpson". if endpoints="simpson", recurse with
292 // forward/backward simpson.
293
294 // Let's implement simpler: directly recurse with lower order for missing parts.
295
296 if (remaining_start > 0) {
297 // Need to fill beginning
298 // Taking f[: 1 + remaining_start]?
299 // Need enough points for trapezoidal (2 points).
300 // slice(f, 0, 1 + remaining_start)
301 // Call recursively with ignore endpoints
302 auto left_res = integrate_discrete_intervals(
303 detail::slice(f, 0, 1 + remaining_start), detail::slice(x, 0, 1 + remaining_start),
304 false, // Don't mul by dx yet
305 "trapezoidal", // Using trapezoidal as universal fallback
306 "ignore");
307 // Concatenate
308 avg_f = detail::concatenate(left_res, avg_f);
309 }
310
311 if (remaining_end > 0) {
312 auto right_res = integrate_discrete_intervals(
313 detail::slice(f, -(1 + remaining_end), x.size()),
314 detail::slice(x, -(1 + remaining_end), x.size()), false, "trapezoidal", "ignore");
315 avg_f = detail::concatenate(avg_f, right_res);
316 }
317
318 } else if (eff_endpoints == "ignore") {
319 // Do nothing, but need to slice dx if we return integrals
320 // dx is computed for full range?
321 // avg_f is smaller.
322 }
323
324 // dx slicing if endpoints ignored
325 Eigen::Index current_size = avg_f.size();
326 if (current_size < dx.size()) {
327 // Figure out offset based on remaining
328 // If remaining_start=1, dx starts at 1.
329 // But "ignore" might not tell us which side unless we know method.
330 // Simpson forward: missing end. dx should be head.
331 if (m.find("forward_simpson") != std::string::npos) {
332 dx = detail::slice(dx, 0, current_size);
333 } else if (m.find("backward_simpson") != std::string::npos) {
334 dx = detail::slice(dx, dx.size() - current_size, dx.size());
335 }
336 }
337
338 if (multiply_by_dx) {
339 return (avg_f.array() * dx.array()).matrix();
340 } else {
341 return avg_f;
342 }
343}
344
357template <typename DerivedF, typename DerivedX>
359integrate_discrete_squared_curvature(const Eigen::MatrixBase<DerivedF> &f,
360 const Eigen::MatrixBase<DerivedX> &x,
361 const std::string &method = "simpson") {
362 using Scalar = typename DerivedF::Scalar;
363
364 Eigen::Index n_points = f.size();
365 if (n_points < 3) {
366 return JanusVector<Scalar>(0);
367 }
368
369 std::string m = method;
370 std::transform(m.begin(), m.end(), m.begin(), ::tolower);
371 std::replace(m.begin(), m.end(), ' ', '_');
372
373 if (m == "simpson") {
374 // Forward Simpson: uses points [i, i+1, i+2] for interval [i, i+1]
375 auto x2 = detail::slice(x, 0, -2);
376 auto x3 = detail::slice(x, 1, -1);
377 auto x4 = detail::slice(x, 2, x.size());
378
379 auto f2 = detail::slice(f, 0, -2);
380 auto f3 = detail::slice(f, 1, -1);
381 auto f4 = detail::slice(f, 2, f.size());
382
383 auto h = (x3 - x2).eval();
384 auto hp = (x4 - x3).eval();
385
386 auto df = (f3 - f2).eval();
387 auto dfp = (f4 - f3).eval();
388
389 // res = 4 * (df*hp - dfp*h)^2 / (h * hp^2 * (h+hp)^2)
390 auto numer = (df.array() * hp.array() - dfp.array() * h.array()).square();
391 auto denom = h.array() * hp.array().square() * (h.array() + hp.array()).square();
392 auto res_forward = (4.0 * numer / denom).eval();
393
394 // Backward Simpson: uses points [i-1, i, i+1] for interval [i, i+1]
395 auto x1_b = detail::slice(x, 0, -2);
396 auto x2_b = detail::slice(x, 1, -1);
397 auto x3_b = detail::slice(x, 2, x.size());
398
399 auto f1_b = detail::slice(f, 0, -2);
400 auto f2_b = detail::slice(f, 1, -1);
401 auto f3_b = detail::slice(f, 2, f.size());
402
403 auto h_b = (x3_b - x2_b).eval();
404 auto hm_b = (x2_b - x1_b).eval();
405
406 auto dfm_b = (f2_b - f1_b).eval();
407 auto df_b = (f3_b - f2_b).eval();
408
409 // res = 4 * (df*hm - dfm*h)^2 / (h * hm^2 * (h+hm)^2)
410 auto numer_b = (df_b.array() * hm_b.array() - dfm_b.array() * h_b.array()).square();
411 auto denom_b = h_b.array() * hm_b.array().square() * (h_b.array() + hm_b.array()).square();
412 auto res_backward = (4.0 * numer_b / denom_b).eval();
413
414 // Fuse: first from forward, middle RMS of both, last from backward
415 auto first_interval = detail::slice(res_forward.matrix(), 0, 1);
416 auto last_interval = detail::slice(res_backward.matrix(), -1, res_backward.size());
417
418 auto a = detail::slice(res_backward.matrix(), 0, -1);
419 auto b = detail::slice(res_forward.matrix(), 1, res_forward.size());
420
421 // RMS fusion: sqrt((a^2 + b^2)/2 + eps)
422 auto mid_sq = (a.array().square() + b.array().square()) * 0.5 + detail::rms_fusion_epsilon;
423 JanusVector<Scalar> mid = janus::sqrt(mid_sq.matrix());
424
425 auto tmp = detail::concatenate(first_interval, mid);
426 return detail::concatenate(tmp, last_interval);
427 } else if (m == "hybrid_simpson_cubic") {
428 // Use gradient to estimate f' at each point
429 auto dfdx = janus::gradient(f, x, 2); // edge_order=2
430
431 auto h = detail::compute_dx(x);
432 auto df = (detail::slice(f, 1, f.size()) - detail::slice(f, 0, -1)).eval();
433 auto dfdx1 = detail::slice(dfdx, 0, -1);
434 auto dfdx2 = detail::slice(dfdx, 1, dfdx.size());
435
436 // res = 4*(dfdx1^2 + dfdx1*dfdx2 + dfdx2^2)/h + 12*df/h^2 * (df/h - dfdx1 - dfdx2)
437 auto term1 =
438 4.0 *
439 (dfdx1.array().square() + dfdx1.array() * dfdx2.array() + dfdx2.array().square()) /
440 h.array();
441 auto term2 = 12.0 * df.array() / h.array().square() *
442 (df.array() / h.array() - dfdx1.array() - dfdx2.array());
443
444 return (term1 + term2).matrix();
445 } else {
446 throw IntegrationError("unknown curvature method: " + method +
447 ". Use 'simpson' or 'hybrid_simpson_cubic'.");
448 }
449}
450
451} // namespace janus
Scalar and element-wise arithmetic functions (abs, sqrt, pow, exp, log, etc.).
Numerical differentiation and integration (gradient, trapz, cumtrapz, diff).
C++20 concepts constraining valid Janus scalar types.
Custom exception hierarchy for Janus framework.
Core type aliases for numeric and symbolic Eigen/CasADi interop.
Conditional selection, comparison, and logical operations.
Integration/ODE solver errors.
Definition JanusError.hpp:61
Smooth approximation of ReLU function: softplus(x) = (1/beta) * log(1 + exp(beta * x)).
Definition Diagnostics.hpp:131
auto slice(const Eigen::MatrixBase< Derived > &m, Eigen::Index start, Eigen::Index end)
Definition IntegrateDiscrete.hpp:32
auto concatenate(const Eigen::MatrixBase< DerivedA > &a, const Eigen::MatrixBase< DerivedB > &b)
Definition IntegrateDiscrete.hpp:52
auto compute_dx(const Eigen::MatrixBase< DerivedX > &x)
Definition IntegrateDiscrete.hpp:26
constexpr double rms_fusion_epsilon
Small epsilon to prevent division by zero in RMS fusion.
Definition IntegrateDiscrete.hpp:23
auto integrate_forward_simpson(const Eigen::MatrixBase< DerivedF > &f, const Eigen::MatrixBase< DerivedX > &x)
Definition IntegrateDiscrete.hpp:64
auto integrate_backward_simpson(const Eigen::MatrixBase< DerivedF > &f, const Eigen::MatrixBase< DerivedX > &x)
Definition IntegrateDiscrete.hpp:91
auto integrate_cubic(const Eigen::MatrixBase< DerivedF > &f, const Eigen::MatrixBase< DerivedX > &x)
Definition IntegrateDiscrete.hpp:116
Definition Diagnostics.hpp:19
auto gradient(const Eigen::MatrixBase< DerivedF > &f, const Spacing &dx=1.0, int edge_order=1, int n=1)
Computes gradient using second-order accurate central differences.
Definition Calculus.hpp:178
T square(const T &x)
Computes x^2 (more efficient than pow(x, 2)).
Definition Arithmetic.hpp:739
T sqrt(const T &x)
Computes the square root of a scalar.
Definition Arithmetic.hpp:46
JanusVector< typename DerivedF::Scalar > integrate_discrete_squared_curvature(const Eigen::MatrixBase< DerivedF > &f, const Eigen::MatrixBase< DerivedX > &x, const std::string &method="simpson")
Computes the integral of squared curvature over each interval.
Definition IntegrateDiscrete.hpp:359
JanusVector< typename DerivedF::Scalar > integrate_discrete_intervals(const Eigen::MatrixBase< DerivedF > &f, const Eigen::MatrixBase< DerivedX > &x, bool multiply_by_dx=true, const std::string &method="trapezoidal", const std::string &method_endpoints="lower_order")
Integrates discrete samples using reconstruction methods.
Definition IntegrateDiscrete.hpp:184
auto eval(const Eigen::MatrixBase< Derived > &mat)
Evaluate a symbolic matrix to a numeric Eigen matrix.
Definition JanusIO.hpp:62
Eigen::Matrix< Scalar, Eigen::Dynamic, 1 > JanusVector
Dynamic-size column vector for both numeric and symbolic backends.
Definition JanusTypes.hpp:49