Janus 2.0.0
High-performance C++20 dual-mode numerical framework
Loading...
Searching...
No Matches
Integrate.hpp
Go to the documentation of this file.
1#pragma once
11
17#include "janus/math/Linalg.hpp"
20#include <Eigen/Dense>
21#include <casadi/casadi.hpp>
22#include <functional>
23#include <limits>
24#include <optional>
25#include <string>
26#include <vector>
27
28namespace janus {
29
30namespace detail {
31inline NumericVector to_numeric_vector(std::initializer_list<double> init) {
32 NumericVector v(static_cast<Eigen::Index>(init.size()));
33 Eigen::Index idx = 0;
34 for (double val : init) {
35 v(idx++) = val;
36 }
37 return v;
38}
39} // namespace detail
40
41// ============================================================================
42// ODE Result structure
43// ============================================================================
44
52template <typename Scalar> struct OdeResult {
55
58
60 bool success = true;
61
63 std::string message = "";
64
66 int status = 0;
67};
68
76
84
90template <typename Scalar> struct SecondOrderOdeResult {
93
96
99
101 bool success = true;
102
104 std::string message = "";
105
107 int status = 0;
108};
109
117
132
133// ============================================================================
134// Quadrature Result structure
135// ============================================================================
136
142template <typename Scalar> struct QuadResult {
144 Scalar value;
145
147 double error = 0.0;
148};
149
150namespace detail {
151
152inline const char *method_name(SecondOrderIntegratorMethod method) {
153 switch (method) {
155 return "Stormer-Verlet";
157 return "RKN4";
158 }
159 throw InvalidArgument("solve_second_order_ivp: unsupported second-order method");
160}
161
162inline const char *method_name(MassMatrixIntegratorMethod method) {
163 switch (method) {
165 return "Rosenbrock-Euler";
167 return "BDF1";
168 }
169 throw InvalidArgument("solve_ivp_mass_matrix: unsupported mass-matrix method");
170}
171
172inline void validate_eval_count(const std::string &context, int n_eval) {
173 if (n_eval < 2) {
174 throw IntegrationError(context + ": n_eval must be at least 2");
175 }
176}
177
179 const std::string &context) {
180 if (opts.substeps <= 0) {
181 throw IntegrationError(context + ": substeps must be positive");
182 }
183}
184
186 const std::string &context) {
187 if (opts.substeps <= 0) {
188 throw IntegrationError(context + ": substeps must be positive");
189 }
190 if (opts.abstol <= 0.0) {
191 throw IntegrationError(context + ": abstol must be positive");
192 }
193 if (opts.reltol <= 0.0) {
194 throw IntegrationError(context + ": reltol must be positive");
195 }
196 if (opts.finite_difference_epsilon <= 0.0) {
197 throw IntegrationError(context + ": finite_difference_epsilon must be positive");
198 }
199 if (opts.max_newton_iterations <= 0) {
200 throw IntegrationError(context + ": max_newton_iterations must be positive");
201 }
202 if (opts.newton_tolerance <= 0.0) {
203 throw IntegrationError(context + ": newton_tolerance must be positive");
204 }
205}
206
208 if (q0.size() == 0) {
209 throw IntegrationError("solve_second_order_ivp: q0 must be non-empty");
210 }
211 if (q0.size() != v0.size()) {
212 throw IntegrationError("solve_second_order_ivp: q0 and v0 must have the same size");
213 }
214}
215
216inline double inf_norm(const NumericVector &x) { return x.lpNorm<Eigen::Infinity>(); }
217
218inline bool is_constant_zero(const SymbolicScalar &expr) {
219 if (expr.is_zero()) {
220 return true;
221 }
222
223 try {
224 casadi::Function probe("integrate_zero_probe", std::vector<casadi::MX>{},
225 std::vector<casadi::MX>{expr});
226 std::vector<casadi::DM> res = probe(std::vector<casadi::DM>{});
227 return std::abs(static_cast<double>(res.at(0))) <= 1e-14;
228 } catch (...) {
229 return false;
230 }
231}
232
233template <typename VectorFunc>
235 double epsilon) {
236 NumericVector f0 = func(x);
237 NumericMatrix J(f0.size(), x.size());
238 NumericVector x_perturbed = x;
239
240 for (Eigen::Index j = 0; j < x.size(); ++j) {
241 const double xj = x(j);
242 const double step = epsilon * std::max(1.0, std::abs(xj));
243 x_perturbed(j) = xj + step;
244 NumericVector f_plus = func(x_perturbed);
245 x_perturbed(j) = xj - step;
246 NumericVector f_minus = func(x_perturbed);
247 x_perturbed(j) = xj;
248 J.col(j) = ((f_plus - f_minus) / (2.0 * step)).eval();
249 }
250
251 return J;
252}
253
254template <typename MassFunc>
255NumericMatrix evaluate_mass_matrix(MassFunc &&mass_matrix, double t, const NumericVector &y,
256 const std::string &context) {
257 NumericMatrix M = mass_matrix(t, y);
258 if (M.rows() != y.size() || M.cols() != y.size()) {
259 throw IntegrationError(context + ": mass matrix must be square with size matching y");
260 }
261 return M;
262}
263
264template <typename RhsFunc, typename MassFunc>
265NumericVector rosenbrock_euler_step(RhsFunc &&rhs, MassFunc &&mass_matrix, const NumericVector &y,
266 double t, double dt, const MassMatrixIvpOptions &opts) {
267 NumericVector f = rhs(t, y);
268 if (f.size() != y.size()) {
269 throw IntegrationError("solve_ivp_mass_matrix: rhs dimension must match y");
270 }
271
272 NumericMatrix M = evaluate_mass_matrix(mass_matrix, t, y, "solve_ivp_mass_matrix");
273 auto rhs_at_t = [&](const NumericVector &state) { return rhs(t, state); };
275 NumericMatrix A = (M - dt * J).eval();
277 return (y + dt * k).eval();
278}
279
280template <typename RhsFunc, typename MassFunc>
281NumericVector bdf1_step(RhsFunc &&rhs, MassFunc &&mass_matrix, const NumericVector &y, double t,
282 double dt, const MassMatrixIvpOptions &opts) {
283 const double t_next = t + dt;
284
285 auto residual = [&](const NumericVector &trial) {
286 NumericVector f = rhs(t_next, trial);
287 if (f.size() != y.size()) {
288 throw IntegrationError("solve_ivp_mass_matrix: rhs dimension must match y");
289 }
290 NumericMatrix M = evaluate_mass_matrix(mass_matrix, t_next, trial, "solve_ivp_mass_matrix");
291 return (M * ((trial - y) / dt) - f).eval();
292 };
293
294 NumericVector guess = y;
295 try {
296 NumericVector f0 = rhs(t, y);
297 NumericMatrix M0 = evaluate_mass_matrix(mass_matrix, t, y, "solve_ivp_mass_matrix");
298 guess = (y + dt * janus::solve(M0, f0, opts.linear_solve_policy)).eval();
299 } catch (...) {
300 }
301
302 for (int iter = 0; iter < opts.max_newton_iterations; ++iter) {
303 NumericVector r = residual(guess);
304 if (inf_norm(r) <= opts.newton_tolerance) {
305 return guess;
306 }
307
308 NumericMatrix J =
311 guess += delta;
312 if (inf_norm(delta) <= opts.newton_tolerance) {
313 return guess;
314 }
315 }
316
317 NumericVector r = residual(guess);
318 throw IntegrationError("solve_ivp_mass_matrix: BDF1 Newton solve failed to converge; residual "
319 "inf-norm = " +
320 std::to_string(inf_norm(r)));
321}
322
323} // namespace detail
324
325// ============================================================================
326// quad: Definite Integral
327// ============================================================================
328
353template <typename Func, typename T>
354QuadResult<T> quad(Func &&func, T a, T b, double abstol = 1e-8, double reltol = 1e-6) {
355 // For numeric types, use Gauss-Kronrod quadrature
356 if constexpr (std::is_floating_point_v<T>) {
357 const auto &embedded = detail::gauss_kronrod_15_rule();
358
359 // Transform to [a, b]
360 T center = (a + b) / 2.0;
361 T halfwidth = (b - a) / 2.0;
362
363 T kronrod_sum = 0.0;
364 T gauss_sum = 0.0;
365
366 for (int i = 0; i < 15; ++i) {
367 T x = center + halfwidth * embedded.nodes[static_cast<std::size_t>(i)];
368 T fx = func(x);
369 kronrod_sum += embedded.primary_weights[static_cast<std::size_t>(i)] * fx;
370 gauss_sum += embedded.embedded_weights[static_cast<std::size_t>(i)] * fx;
371 }
372
373 kronrod_sum *= halfwidth;
374 gauss_sum *= halfwidth;
375
376 QuadResult<T> result;
377 result.value = kronrod_sum;
378 result.error = std::abs(kronrod_sum - gauss_sum);
379 return result;
380 } else {
381 // Symbolic path: This won't work with a lambda directly
382 // User should use the symbolic expression overload below
383 throw IntegrationError("quad with callable is not supported for symbolic types. "
384 "Use quad(expr, variable, a, b) instead.");
385 }
386}
387
400 double a, double b, double abstol = 1e-8,
401 double reltol = 1e-6) {
402 // Find all variables in the expression
403 std::vector<casadi::MX> all_vars = casadi::MX::symvar(expr);
404
405 // Separate: variable of integration vs parameters
406 std::vector<casadi::MX> parameters;
407 for (const auto &var : all_vars) {
408 if (!casadi::MX::is_equal(var, variable)) {
409 parameters.push_back(var);
410 }
411 }
412
413 // Build parameter vector
414 SymbolicScalar p = SymbolicScalar::vertcat(parameters);
415
416 // Create dummy state variable (integral accumulator)
417 SymbolicScalar dummy_x = SymbolicScalar::sym("__quad_x");
418
419 // Build integrator
420 // The ODE is: dx/dt = expr, where t is the variable of integration
421 casadi::MXDict dae = {{"x", dummy_x}, {"t", variable}, {"p", p}, {"ode", expr}};
422
423 casadi::Dict opts = {{"abstol", abstol}, {"reltol", reltol}};
424
425 casadi::Function integrator = casadi::integrator("quad_integrator", "cvodes", dae, a, b, opts);
426
427 // Evaluate with x0 = 0
428 casadi::DMDict arg = {{"x0", casadi::DM(0.0)}, {"p", casadi::DM::zeros(p.size1(), p.size2())}};
429
430 // For symbolic output, we need to call with symbolic parameters
431 casadi::MXDict sym_arg = {{"x0", casadi::MX(0.0)}, {"p", p}};
432
433 casadi::MXDict res = integrator(sym_arg);
434
436 result.value = res.at("xf");
437 result.error = abstol; // Nominal error bound
438 return result;
439}
440
480template <typename Func>
481OdeResult<double> solve_ivp(Func &&fun, std::pair<double, double> t_span, const NumericVector &y0,
482 int n_eval = 100, double abstol = 1e-8, double reltol = 1e-6) {
483 double t0 = t_span.first;
484 double tf = t_span.second;
485
486 OdeResult<double> result;
487 result.t = linspace(t0, tf, n_eval);
488 result.y.resize(y0.size(), n_eval);
489 result.y.col(0) = y0;
490
491 // Use RK4 with adaptive stepping (simplified: fixed step based on tolerance)
492 int n_state = y0.size();
493 double dt_base = (tf - t0) / (n_eval - 1);
494
495 // Substeps per output interval for accuracy
496 int substeps = std::max(1, static_cast<int>(std::ceil(1.0 / std::sqrt(reltol))));
497
498 NumericVector y = y0;
499
500 for (int i = 1; i < n_eval; ++i) {
501 double t = result.t(i - 1);
502 double dt = dt_base / substeps;
503
504 for (int s = 0; s < substeps; ++s) {
505 // Use shared step implementation
506 y = rk4_step(fun, y, t, dt);
507 t += dt;
508 }
509
510 result.y.col(i) = y;
511 }
512
513 result.success = true;
514 result.message = "Integration successful (RK4)";
515 result.status = 0;
516
517 return result;
518}
519
531template <typename Func>
532OdeResult<double> solve_ivp(Func &&fun, std::pair<double, double> t_span,
533 std::initializer_list<double> y0_init, int n_eval = 100,
534 double abstol = 1e-8, double reltol = 1e-6) {
535 return solve_ivp(std::forward<Func>(fun), t_span, detail::to_numeric_vector(y0_init), n_eval,
536 abstol, reltol);
537}
538
556template <typename AccelFunc>
557SecondOrderOdeResult<double>
558solve_second_order_ivp(AccelFunc &&acceleration, std::pair<double, double> t_span,
559 const NumericVector &q0, const NumericVector &v0, int n_eval = 100,
560 const SecondOrderIvpOptions &opts = {}) {
561 detail::validate_eval_count("solve_second_order_ivp", n_eval);
562 detail::validate_second_order_options(opts, "solve_second_order_ivp");
564
565 const double t0 = t_span.first;
566 const double tf = t_span.second;
567
568 SecondOrderOdeResult<double> result;
569 result.t = linspace(t0, tf, n_eval);
570 result.q.resize(q0.size(), n_eval);
571 result.v.resize(v0.size(), n_eval);
572 result.q.col(0) = q0;
573 result.v.col(0) = v0;
574
575 NumericVector q = q0;
576 NumericVector v = v0;
577 const double dt_base = (tf - t0) / static_cast<double>(n_eval - 1);
578
579 for (int i = 1; i < n_eval; ++i) {
580 double t = result.t(i - 1);
581 const double dt = dt_base / static_cast<double>(opts.substeps);
582
583 for (int s = 0; s < opts.substeps; ++s) {
584 SecondOrderStepResult<double> step;
586 step = stormer_verlet_step(acceleration, q, v, t, dt);
587 } else {
588 step = rkn4_step(acceleration, q, v, t, dt);
589 }
590 q = step.q;
591 v = step.v;
592 t += dt;
593 }
594
595 result.q.col(i) = q;
596 result.v.col(i) = v;
597 }
598
599 result.success = true;
600 result.message =
601 std::string("Integration successful (") + detail::method_name(opts.method) + ")";
602 result.status = 0;
603 return result;
604}
605
609template <typename AccelFunc>
611solve_second_order_ivp(AccelFunc &&acceleration, std::pair<double, double> t_span,
612 std::initializer_list<double> q0_init, std::initializer_list<double> v0_init,
613 int n_eval = 100, const SecondOrderIvpOptions &opts = {}) {
614 return solve_second_order_ivp(std::forward<AccelFunc>(acceleration), t_span,
616 detail::to_numeric_vector(v0_init), n_eval, opts);
617}
618
637template <typename RhsFunc, typename MassFunc>
638OdeResult<double> solve_ivp_mass_matrix(RhsFunc &&rhs, MassFunc &&mass_matrix,
639 std::pair<double, double> t_span, const NumericVector &y0,
640 int n_eval = 100, const MassMatrixIvpOptions &opts = {}) {
641 detail::validate_eval_count("solve_ivp_mass_matrix", n_eval);
642 detail::validate_mass_matrix_options(opts, "solve_ivp_mass_matrix");
643
644 const double t0 = t_span.first;
645 const double tf = t_span.second;
646 if (t0 == tf) {
647 throw IntegrationError("solve_ivp_mass_matrix: t_span endpoints must differ");
648 }
649 if (y0.size() == 0) {
650 throw IntegrationError("solve_ivp_mass_matrix: y0 must be non-empty");
651 }
652
653 OdeResult<double> result;
654 result.t = linspace(t0, tf, n_eval);
655 result.y.resize(y0.size(), n_eval);
656 result.y.col(0) = y0;
657
658 NumericVector y = y0;
659 const double dt_base = (tf - t0) / static_cast<double>(n_eval - 1);
660
661 for (int i = 1; i < n_eval; ++i) {
662 double t = result.t(i - 1);
663 const double dt = dt_base / static_cast<double>(opts.substeps);
664
665 for (int s = 0; s < opts.substeps; ++s) {
667 y = detail::rosenbrock_euler_step(rhs, mass_matrix, y, t, dt, opts);
668 } else {
669 y = detail::bdf1_step(rhs, mass_matrix, y, t, dt, opts);
670 }
671 t += dt;
672 }
673
674 result.y.col(i) = y;
675 }
676
677 result.success = true;
678 result.message =
679 std::string("Integration successful (") + detail::method_name(opts.method) + ")";
680 result.status = 0;
681 return result;
682}
683
687template <typename RhsFunc, typename MassFunc>
688OdeResult<double> solve_ivp_mass_matrix(RhsFunc &&rhs, MassFunc &&mass_matrix,
689 std::pair<double, double> t_span,
690 std::initializer_list<double> y0_init, int n_eval = 100,
691 const MassMatrixIvpOptions &opts = {}) {
692 return solve_ivp_mass_matrix(std::forward<RhsFunc>(rhs), std::forward<MassFunc>(mass_matrix),
693 t_span, detail::to_numeric_vector(y0_init), n_eval, opts);
694}
695
719 const SymbolicScalar &t_var, const SymbolicScalar &y_var,
720 std::pair<double, double> t_span, const NumericVector &y0,
721 int n_eval = 100, const MassMatrixIvpOptions &opts = {}) {
722 detail::validate_eval_count("solve_ivp_mass_matrix_expr", n_eval);
723 detail::validate_mass_matrix_options(opts, "solve_ivp_mass_matrix_expr");
724
725 const double t0 = t_span.first;
726 const double tf = t_span.second;
727 if (t0 == tf) {
728 throw IntegrationError("solve_ivp_mass_matrix_expr: t_span endpoints must differ");
729 }
730 const int n_state = static_cast<int>(y0.size());
731 if (n_state == 0) {
732 throw IntegrationError("solve_ivp_mass_matrix_expr: y0 must be non-empty");
733 }
734
735 SymbolicScalar t_normalized = SymbolicScalar::sym("t_norm");
736 SymbolicScalar rhs_sub =
737 SymbolicScalar::substitute(rhs_expr, t_var, t0 + (tf - t0) * t_normalized);
738 SymbolicScalar mass_sub =
739 SymbolicScalar::substitute(mass_expr, t_var, t0 + (tf - t0) * t_normalized);
740
741 if (mass_sub.size1() != n_state || mass_sub.size2() != n_state) {
742 throw IntegrationError(
743 "solve_ivp_mass_matrix_expr: mass matrix must be square with size matching y0");
744 }
745 if (rhs_sub.size1() != n_state || rhs_sub.size2() != 1) {
746 throw IntegrationError("solve_ivp_mass_matrix_expr: rhs must be a column vector matching "
747 "the size of y0");
748 }
749
750 casadi::DM y0_dm_full(n_state, 1);
751 for (int i = 0; i < n_state; ++i) {
752 y0_dm_full(i) = y0(i);
753 }
754
755 std::vector<double> t_eval_normalized(n_eval);
756 for (int i = 0; i < n_eval; ++i) {
757 t_eval_normalized[i] = static_cast<double>(i) / static_cast<double>(n_eval - 1);
758 }
759
760 std::vector<bool> row_has_structural_nnz(static_cast<std::size_t>(n_state), false);
761 for (int row = 0; row < n_state; ++row) {
762 for (int col = 0; col < n_state; ++col) {
763 if (!detail::is_constant_zero(mass_sub(row, col))) {
764 row_has_structural_nnz[static_cast<std::size_t>(row)] = true;
765 break;
766 }
767 }
768 }
769
770 std::vector<int> differential_indices;
771 std::vector<int> algebraic_indices;
772 for (int i = 0; i < n_state; ++i) {
773 if (row_has_structural_nnz[static_cast<std::size_t>(i)]) {
774 differential_indices.push_back(i);
775 } else {
776 algebraic_indices.push_back(i);
777 }
778 }
779
780 auto is_component_of = [](const casadi::MX &var, const casadi::MX &vector_expr) {
781 for (int i = 0; i < vector_expr.nnz(); ++i) {
782 if (casadi::MX::is_equal(var, vector_expr(i))) {
783 return true;
784 }
785 }
786 return false;
787 };
788
789 casadi::Dict integrator_opts = {
790 {"abstol", opts.abstol}, {"reltol", opts.reltol}, {"calc_ic", true}};
791 for (const auto &kv : opts.symbolic_integrator_options) {
792 integrator_opts[kv.first] = kv.second;
793 }
794
795 OdeResult<double> result;
796 result.t.resize(n_eval);
797 for (int i = 0; i < n_eval; ++i) {
798 result.t(i) = t0 + (tf - t0) * t_eval_normalized[i];
799 }
800 result.y.resize(n_state, n_eval);
801
802 if (algebraic_indices.empty()) {
803 SymbolicScalar z_var = SymbolicScalar::sym("y_dot_norm", n_state, 1);
804 SymbolicScalar alg = casadi::MX::mtimes(mass_sub, z_var) - (tf - t0) * rhs_sub;
805
806 std::vector<casadi::MX> all_vars = casadi::MX::symvar(casadi::MX::vertcat({rhs_sub, alg}));
807 std::vector<casadi::MX> parameters;
808 for (const auto &var : all_vars) {
809 if (casadi::MX::is_equal(var, t_normalized) || casadi::MX::is_equal(var, y_var) ||
810 casadi::MX::is_equal(var, z_var) || is_component_of(var, y_var) ||
811 is_component_of(var, z_var)) {
812 continue;
813 }
814 parameters.push_back(var);
815 }
816
817 casadi::MX p = casadi::MX::vertcat(parameters);
818 casadi::MXDict dae = {{"x", y_var}, {"z", z_var}, {"t", t_normalized},
819 {"p", p}, {"ode", z_var}, {"alg", alg}};
820
821 casadi::Function integrator = casadi::integrator("mass_matrix_ivp_integrator", "idas", dae,
822 0.0, t_eval_normalized, integrator_opts);
823
824 casadi::DMDict integrator_args;
825 integrator_args["x0"] = y0_dm_full;
826 integrator_args["z0"] = casadi::DM::zeros(n_state, 1);
827 integrator_args["p"] = casadi::DM::zeros(p.size1(), p.size2());
828 casadi::DMDict res_dm = integrator(integrator_args);
829
830 casadi::DM xf = res_dm.at("xf");
831 for (int i = 0; i < n_state; ++i) {
832 for (int j = 0; j < n_eval; ++j) {
833 result.y(i, j) = static_cast<double>(xf(i, j));
834 }
835 }
836 } else {
837 if (differential_indices.empty()) {
838 throw IntegrationError("solve_ivp_mass_matrix_expr: fully algebraic systems are not "
839 "supported");
840 }
841
842 const int n_diff = static_cast<int>(differential_indices.size());
843 const int n_alg = static_cast<int>(algebraic_indices.size());
844
845 SymbolicScalar x_var = SymbolicScalar::sym("y_diff", n_diff, 1);
846 SymbolicScalar z_var = SymbolicScalar::sym("y_alg", n_alg, 1);
847
848 SymbolicScalar partitioned_y = SymbolicScalar(n_state, 1);
849 int diff_cursor = 0;
850 int alg_cursor = 0;
851 for (int i = 0; i < n_state; ++i) {
852 if (row_has_structural_nnz[static_cast<std::size_t>(i)]) {
853 partitioned_y(i) = x_var(diff_cursor++);
854 } else {
855 partitioned_y(i) = z_var(alg_cursor++);
856 }
857 }
858
859 SymbolicScalar rhs_partitioned = SymbolicScalar::substitute(rhs_sub, y_var, partitioned_y);
860 SymbolicScalar mass_partitioned =
861 SymbolicScalar::substitute(mass_sub, y_var, partitioned_y);
862
863 SymbolicScalar rhs_diff(n_diff, 1);
864 SymbolicScalar rhs_alg(n_alg, 1);
865 for (int i = 0; i < n_diff; ++i) {
866 rhs_diff(i) = rhs_partitioned(differential_indices[static_cast<std::size_t>(i)]);
867 }
868 for (int i = 0; i < n_alg; ++i) {
869 rhs_alg(i) = rhs_partitioned(algebraic_indices[static_cast<std::size_t>(i)]);
870 }
871
872 SymbolicScalar mass_diff(n_diff, n_diff);
873 for (int i = 0; i < n_diff; ++i) {
874 for (int j = 0; j < n_diff; ++j) {
875 mass_diff(i, j) =
876 mass_partitioned(differential_indices[static_cast<std::size_t>(i)],
877 differential_indices[static_cast<std::size_t>(j)]);
878 }
879 }
880
881 SymbolicScalar ode = casadi::MX::solve(mass_diff, (tf - t0) * rhs_diff);
882
883 std::vector<casadi::MX> all_vars = casadi::MX::symvar(casadi::MX::vertcat(
884 {rhs_partitioned, casadi::MX::reshape(mass_partitioned, n_state * n_state, 1)}));
885 std::vector<casadi::MX> parameters;
886 for (const auto &var : all_vars) {
887 if (casadi::MX::is_equal(var, t_normalized) || casadi::MX::is_equal(var, x_var) ||
888 casadi::MX::is_equal(var, z_var) || is_component_of(var, x_var) ||
889 is_component_of(var, z_var)) {
890 continue;
891 }
892 parameters.push_back(var);
893 }
894
895 casadi::MX p = casadi::MX::vertcat(parameters);
896 casadi::MXDict dae = {{"x", x_var}, {"z", z_var}, {"t", t_normalized},
897 {"p", p}, {"ode", ode}, {"alg", rhs_alg}};
898
899 casadi::Function integrator =
900 casadi::integrator("mass_matrix_ivp_integrator_partitioned", "idas", dae, 0.0,
901 t_eval_normalized, integrator_opts);
902
903 casadi::DM x0_dm(n_diff, 1);
904 casadi::DM z0_dm(n_alg, 1);
905 for (int i = 0; i < n_diff; ++i) {
906 x0_dm(i) = y0(differential_indices[static_cast<std::size_t>(i)]);
907 }
908 for (int i = 0; i < n_alg; ++i) {
909 z0_dm(i) = y0(algebraic_indices[static_cast<std::size_t>(i)]);
910 }
911
912 casadi::DMDict integrator_args;
913 integrator_args["x0"] = x0_dm;
914 integrator_args["z0"] = z0_dm;
915 integrator_args["p"] = casadi::DM::zeros(p.size1(), p.size2());
916 casadi::DMDict res_dm = integrator(integrator_args);
917
918 casadi::DM xf = res_dm.at("xf");
919 casadi::DM zf = res_dm.at("zf");
920 for (int i = 0; i < n_eval; ++i) {
921 int diff_out = 0;
922 int alg_out = 0;
923 for (int state = 0; state < n_state; ++state) {
924 if (row_has_structural_nnz[static_cast<std::size_t>(state)]) {
925 result.y(state, i) = static_cast<double>(xf(diff_out++, i));
926 } else {
927 result.y(state, i) = static_cast<double>(zf(alg_out++, i));
928 }
929 }
930 }
931 }
932
933 result.success = true;
934 result.message = "Integration successful (IDAS mass matrix)";
935 result.status = 0;
936 return result;
937}
938
954template <typename Func>
955OdeResult<SymbolicScalar> solve_ivp_symbolic(Func &&fun, std::pair<double, double> t_span,
956 const Eigen::VectorXd &y0, int n_eval = 100,
957 double abstol = 1e-8, double reltol = 1e-6) {
958 double t0 = t_span.first;
959 double tf = t_span.second;
960 int n_state = y0.size();
961
962 // Create symbolic variables for the ODE
963 SymbolicScalar t_var = SymbolicScalar::sym("t");
964 SymbolicScalar y_var = SymbolicScalar::sym("y", n_state, 1);
965
966 // Evaluate the ODE function symbolically
967 SymbolicScalar ode_expr;
968 if constexpr (std::is_invocable_v<Func, casadi::MX, casadi::MX>) {
969 ode_expr = fun(t_var, y_var);
970 } else {
971 // Try calling with Eigen-wrapped MX
972 SymbolicVector y_sym(n_state);
973 for (int i = 0; i < n_state; ++i) {
974 y_sym(i) = y_var(i);
975 }
976 auto result = fun(t_var, y_sym);
977 // Convert back to MX
978 ode_expr = casadi::MX(n_state, 1);
979 for (int i = 0; i < n_state; ++i) {
980 ode_expr(i) = result(i);
981 }
982 }
983
984 // Normalize time to [0, 1] for CVODES stability
985 // t_real = t0 + (tf - t0) * t_normalized
986 // ode_normalized = ode_real * (tf - t0)
987 casadi::MX t_normalized = casadi::MX::sym("t_norm");
988 casadi::MX ode_normalized =
989 casadi::MX::substitute(ode_expr, t_var, t0 + (tf - t0) * t_normalized) * (tf - t0);
990
991 // Create evaluation time grid (normalized)
992 std::vector<double> t_eval_normalized(n_eval);
993 for (int i = 0; i < n_eval; ++i) {
994 t_eval_normalized[i] = static_cast<double>(i) / (n_eval - 1);
995 }
996
997 // Build integrator
998 casadi::MXDict dae = {{"x", y_var}, {"t", t_normalized}, {"ode", ode_normalized}};
999
1000 casadi::Dict opts = {{"abstol", abstol}, {"reltol", reltol}};
1001
1002 casadi::Function integrator =
1003 casadi::integrator("ivp_integrator", "cvodes", dae, 0.0, t_eval_normalized, opts);
1004
1005 // Convert y0 to DM
1006 casadi::DM y0_dm(n_state, 1);
1007 for (int i = 0; i < n_state; ++i) {
1008 y0_dm(i) = y0(i);
1009 }
1010
1011 // Evaluate integrator - explicitly construct DMDict to avoid ambiguity
1012 casadi::DMDict integrator_args;
1013 integrator_args["x0"] = y0_dm;
1014 casadi::DMDict res_dm = integrator(integrator_args);
1015
1017
1018 // Create symbolic time vector
1019 result.t.resize(n_eval);
1020 for (int i = 0; i < n_eval; ++i) {
1021 result.t(i) = t0 + (tf - t0) * t_eval_normalized[i];
1022 }
1023
1024 // Extract solution (xf contains all intermediate states)
1025 casadi::DM xf = res_dm.at("xf");
1026 result.y.resize(n_state, n_eval);
1027 for (int i = 0; i < n_state; ++i) {
1028 for (int j = 0; j < n_eval; ++j) {
1029 result.y(i, j) = static_cast<double>(xf(i, j));
1030 }
1031 }
1032
1033 result.success = true;
1034 result.message = "Integration successful (CVODES)";
1035 result.status = 0;
1036
1037 return result;
1038}
1039
1057 const SymbolicScalar &y_var,
1058 std::pair<double, double> t_span, const NumericVector &y0,
1059 int n_eval = 100, double abstol = 1e-8,
1060 double reltol = 1e-6) {
1061 double t0 = t_span.first;
1062 double tf = t_span.second;
1063 int n_state = y0.size();
1064
1065 // Normalize time to [0, 1]
1066 SymbolicScalar t_normalized = SymbolicScalar::sym("t_norm");
1067 SymbolicScalar ode_normalized =
1068 SymbolicScalar::substitute(ode_expr, t_var, t0 + (tf - t0) * t_normalized) * (tf - t0);
1069
1070 // Create evaluation time grid (normalized)
1071 std::vector<double> t_eval_normalized(n_eval);
1072 for (int i = 0; i < n_eval; ++i) {
1073 t_eval_normalized[i] = static_cast<double>(i) / (n_eval - 1);
1074 }
1075
1076 // Find parameters (variables in ode other than t and y)
1077 std::vector<casadi::MX> all_vars = casadi::MX::symvar(ode_normalized);
1078 std::vector<casadi::MX> parameters;
1079 for (const auto &var : all_vars) {
1080 if (!casadi::MX::is_equal(var, t_normalized) && !casadi::MX::is_equal(var, y_var)) {
1081 // Check each element of y_var
1082 bool is_y = false;
1083 for (int i = 0; i < y_var.size1() * y_var.size2(); ++i) {
1084 if (casadi::MX::is_equal(var, y_var(i))) {
1085 is_y = true;
1086 break;
1087 }
1088 }
1089 if (!is_y) {
1090 parameters.push_back(var);
1091 }
1092 }
1093 }
1094
1095 casadi::MX p = casadi::MX::vertcat(parameters);
1096
1097 // Build integrator
1098 casadi::MXDict dae = {{"x", y_var}, {"t", t_normalized}, {"p", p}, {"ode", ode_normalized}};
1099
1100 casadi::Dict opts = {{"abstol", abstol}, {"reltol", reltol}};
1101
1102 casadi::Function integrator =
1103 casadi::integrator("ivp_integrator", "cvodes", dae, 0.0, t_eval_normalized, opts);
1104
1105 // Convert y0 to DM
1106 casadi::DM y0_dm(n_state, 1);
1107 for (int i = 0; i < n_state; ++i) {
1108 y0_dm(i) = y0(i);
1109 }
1110
1111 // Empty parameters for now (user would need to provide values)
1112 casadi::DM p_dm = casadi::DM::zeros(p.size1(), p.size2());
1113
1114 // Evaluate integrator - explicitly construct DMDict to avoid ambiguity
1115 casadi::DMDict integrator_args;
1116 integrator_args["x0"] = y0_dm;
1117 integrator_args["p"] = p_dm;
1118 casadi::DMDict res_dm = integrator(integrator_args);
1119
1120 OdeResult<double> result;
1121
1122 // Create time vector
1123 result.t.resize(n_eval);
1124 for (int i = 0; i < n_eval; ++i) {
1125 result.t(i) = t0 + (tf - t0) * t_eval_normalized[i];
1126 }
1127
1128 // Extract solution
1129 casadi::DM xf = res_dm.at("xf");
1130 result.y.resize(n_state, n_eval);
1131 for (int i = 0; i < n_state; ++i) {
1132 for (int j = 0; j < n_eval; ++j) {
1133 result.y(i, j) = static_cast<double>(xf(i, j));
1134 }
1135 }
1136
1137 result.success = true;
1138 result.message = "Integration successful (CVODES)";
1139 result.status = 0;
1140
1141 return result;
1142}
1143
1144} // namespace janus
Scalar and element-wise arithmetic functions (abs, sqrt, pow, exp, log, etc.).
Single-step explicit ODE integrators for Janus framework.
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).
Stochastic quadrature rules (Gauss, Clenshaw-Curtis, Smolyak sparse grids).
Point distribution generators (linspace, cosine, sine, log, geometric).
Integration/ODE solver errors.
Definition JanusError.hpp:61
Input validation failed (e.g., mismatched sizes, invalid parameters).
Definition JanusError.hpp:31
Smooth approximation of ReLU function: softplus(x) = (1/beta) * log(1 + exp(beta * x)).
Definition Diagnostics.hpp:131
NumericVector bdf1_step(RhsFunc &&rhs, MassFunc &&mass_matrix, const NumericVector &y, double t, double dt, const MassMatrixIvpOptions &opts)
Definition Integrate.hpp:281
NumericMatrix finite_difference_jacobian(VectorFunc &&func, const NumericVector &x, double epsilon)
Definition Integrate.hpp:234
double inf_norm(const NumericVector &x)
Definition Integrate.hpp:216
void validate_mass_matrix_options(const MassMatrixIvpOptions &opts, const std::string &context)
Definition Integrate.hpp:185
void validate_eval_count(const std::string &context, int n_eval)
Definition Integrate.hpp:172
const char * method_name(SecondOrderIntegratorMethod method)
Definition Integrate.hpp:152
bool is_constant_zero(const SymbolicScalar &expr)
Definition Integrate.hpp:218
const EmbeddedQuadratureRule & gauss_kronrod_15_rule()
Definition Quadrature.hpp:78
void validate_second_order_options(const SecondOrderIvpOptions &opts, const std::string &context)
Definition Integrate.hpp:178
NumericMatrix evaluate_mass_matrix(MassFunc &&mass_matrix, double t, const NumericVector &y, const std::string &context)
Definition Integrate.hpp:255
void validate_second_order_initial_state(const NumericVector &q0, const NumericVector &v0)
Definition Integrate.hpp:207
NumericVector to_numeric_vector(std::initializer_list< double > init)
Definition Integrate.hpp:31
NumericVector rosenbrock_euler_step(RhsFunc &&rhs, MassFunc &&mass_matrix, const NumericVector &y, double t, double dt, const MassMatrixIvpOptions &opts)
Definition Integrate.hpp:265
Definition Diagnostics.hpp:19
JanusVector< SymbolicScalar > SymbolicVector
Eigen vector of MX elements.
Definition JanusTypes.hpp:72
JanusVector< Scalar > rk4_step(Func &&f, const JanusVector< Scalar > &x, Scalar t, Scalar dt)
Classic 4th-order Runge-Kutta integration step.
Definition IntegratorStep.hpp:131
Eigen::Matrix< Scalar, Eigen::Dynamic, Eigen::Dynamic > JanusMatrix
Dynamic-size matrix for both numeric and symbolic backends.
Definition JanusTypes.hpp:43
JanusMatrix< NumericScalar > NumericMatrix
Eigen::MatrixXd equivalent.
Definition JanusTypes.hpp:66
SecondOrderStepResult< Scalar > rkn4_step(AccelFunc &&acceleration, const JanusVector< Scalar > &q, const JanusVector< Scalar > &v, Scalar t, Scalar dt)
Classical 4th-order Runge-Kutta-Nystrom step for q'' = a(t, q).
Definition IntegratorStep.hpp:193
MassMatrixIntegratorMethod
Stepper selection for mass-matrix systems M(t, y) y' = f(t, y).
Definition Integrate.hpp:80
@ RosenbrockEuler
One-stage linearly implicit Rosenbrock-Euler.
Definition Integrate.hpp:81
@ Bdf1
Backward Euler / first-order BDF with Newton iterations.
Definition Integrate.hpp:82
JanusVector< NumericScalar > NumericVector
Eigen::VectorXd equivalent.
Definition JanusTypes.hpp:67
QuadResult< T > quad(Func &&func, T a, T b, double abstol=1e-8, double reltol=1e-6)
Compute definite integral using adaptive quadrature (numeric) or CVODES (symbolic).
Definition Integrate.hpp:354
OdeResult< SymbolicScalar > solve_ivp_symbolic(Func &&fun, std::pair< double, double > t_span, const Eigen::VectorXd &y0, int n_eval=100, double abstol=1e-8, double reltol=1e-6)
Solve IVP with symbolic ODE function (CasADi CVODES backend).
Definition Integrate.hpp:955
OdeResult< double > solve_ivp_mass_matrix(RhsFunc &&rhs, MassFunc &&mass_matrix, std::pair< double, double > t_span, const NumericVector &y0, int n_eval=100, const MassMatrixIvpOptions &opts={})
Solve M(t, y) y' = f(t, y) with a native numeric stiff integrator.
Definition Integrate.hpp:638
OdeResult< double > solve_ivp(Func &&fun, std::pair< double, double > t_span, const NumericVector &y0, int n_eval=100, double abstol=1e-8, double reltol=1e-6)
Solve initial value problem: dy/dt = f(t, y), y(t0) = y0.
Definition Integrate.hpp:481
OdeResult< double > solve_ivp_mass_matrix_expr(const SymbolicScalar &rhs_expr, const SymbolicScalar &mass_expr, const SymbolicScalar &t_var, const SymbolicScalar &y_var, std::pair< double, double > t_span, const NumericVector &y0, int n_eval=100, const MassMatrixIvpOptions &opts={})
Solve a symbolic mass-matrix IVP using CasADi IDAS.
Definition Integrate.hpp:718
auto eval(const Eigen::MatrixBase< Derived > &mat)
Evaluate a symbolic matrix to a numeric Eigen matrix.
Definition JanusIO.hpp:62
SecondOrderIntegratorMethod
Stepper selection for second-order systems q'' = a(t, q).
Definition Integrate.hpp:72
@ StormerVerlet
Symplectic Stormer-Verlet / velocity-Verlet.
Definition Integrate.hpp:73
@ RungeKuttaNystrom4
Classical 4th-order Runge-Kutta-Nystrom.
Definition Integrate.hpp:74
OdeResult< double > solve_ivp_expr(const SymbolicScalar &ode_expr, const SymbolicScalar &t_var, const SymbolicScalar &y_var, std::pair< double, double > t_span, const NumericVector &y0, int n_eval=100, double abstol=1e-8, double reltol=1e-6)
Solve IVP with a symbolic expression directly (expression-based API).
Definition Integrate.hpp:1056
SecondOrderStepResult< Scalar > stormer_verlet_step(AccelFunc &&acceleration, const JanusVector< Scalar > &q, const JanusVector< Scalar > &v, Scalar t, Scalar dt)
Stormer-Verlet / velocity-Verlet step for q'' = a(t, q).
Definition IntegratorStep.hpp:167
SecondOrderOdeResult< double > solve_second_order_ivp(AccelFunc &&acceleration, std::pair< double, double > t_span, const NumericVector &q0, const NumericVector &v0, int n_eval=100, const SecondOrderIvpOptions &opts={})
Solve a second-order IVP q'' = a(t, q), q(t0) = q0, v(t0) = v0.
Definition Integrate.hpp:558
auto solve(const Eigen::MatrixBase< DerivedA > &A, const Eigen::MatrixBase< DerivedB > &b)
Solves linear system Ax = b using the default backend policy.
Definition Linalg.hpp:408
JanusVector< T > linspace(const T &start, const T &end, int n)
Generates linearly spaced vector.
Definition Spacing.hpp:26
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
Configuration for linear system solve backend and algorithm.
Definition Linalg.hpp:86
Options for stiff mass-matrix integration.
Definition Integrate.hpp:121
double newton_tolerance
Definition Integrate.hpp:128
LinearSolvePolicy linear_solve_policy
Definition Integrate.hpp:129
double reltol
Definition Integrate.hpp:125
casadi::Dict symbolic_integrator_options
Definition Integrate.hpp:130
double abstol
Definition Integrate.hpp:124
int substeps
Definition Integrate.hpp:123
MassMatrixIntegratorMethod method
Definition Integrate.hpp:122
double finite_difference_epsilon
Definition Integrate.hpp:126
int max_newton_iterations
Definition Integrate.hpp:127
Result structure for ODE solvers.
Definition Integrate.hpp:52
bool success
Whether integration was successful.
Definition Integrate.hpp:60
std::string message
Descriptive message.
Definition Integrate.hpp:63
JanusVector< Scalar > t
Time points where solution was computed.
Definition Integrate.hpp:54
JanusMatrix< Scalar > y
Solution values at each time point (each column is a state at time t[i]).
Definition Integrate.hpp:57
int status
Status code (0 = success).
Definition Integrate.hpp:66
Result structure for quadrature (definite integration).
Definition Integrate.hpp:142
double error
Estimated error (only meaningful for numeric backend).
Definition Integrate.hpp:147
Scalar value
Integral value.
Definition Integrate.hpp:144
Options for second-order structure-preserving trajectory integration.
Definition Integrate.hpp:113
SecondOrderIntegratorMethod method
Definition Integrate.hpp:114
int substeps
Definition Integrate.hpp:115
Result structure for second-order IVP solvers.
Definition Integrate.hpp:90
JanusVector< Scalar > t
Time points where solution was computed.
Definition Integrate.hpp:92
int status
Status code (0 = success).
Definition Integrate.hpp:107
JanusMatrix< Scalar > v
Generalized velocities at each time point (each column is v(t[i])).
Definition Integrate.hpp:98
bool success
Whether integration was successful.
Definition Integrate.hpp:101
std::string message
Descriptive message.
Definition Integrate.hpp:104
JanusMatrix< Scalar > q
Generalized coordinates at each time point (each column is q(t[i])).
Definition Integrate.hpp:95
JanusVector< NumericScalar > NumericVector
Eigen::VectorXd equivalent.
Definition JanusTypes.hpp:67