1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
//! LSODE2 Lambdify Guide
//!
//! Goal:
//! solve a symbolic IVP using Lambdify (no ahead-of-time compilation).
//!
//! This is usually the best first symbolic workflow:
//! 1. write equations as strings (`Expr::parse_expression`)
//! 2. pick Jacobian structure (dense/sparse/banded)
//! 3. let LSODE2 build residual/Jacobian evaluators through Lambdify
//! 4. run through `UniversalODESolver` (same facade as other ODE solvers)
//!
//! Problem:
//! y1' = -10 y1 + 9 y2
//! y2' = y1 - y2
//! with y(0)=[1,0], t in [0,1]
//!
//! Why sparse here:
//! this small system can be dense too, but sparse setup demonstrates the
//! production-oriented route used on larger Jacobians.
//!
//! run cargo run --example lsode2_lambdify_guide
use RustedSciThe::numerical::LSODE2::{
Lsode2BackendConfig, Lsode2LinearSolverPolicy, Lsode2LinearSystemStructure,
Lsode2ProblemConfig, Lsode2ResidualJacobianSource, Lsode2SymbolicAssemblyBackend,
Lsode2SymbolicExecutionMode,
};
use RustedSciThe::symbolic::symbolic_engine::Expr;
use nalgebra::DVector;
fn main() {
// Step 1: define symbolic system, variables, and integration interval.
let config = Lsode2ProblemConfig::new(
vec![
Expr::parse_expression("-10.0*y1 + 9.0*y2"),
Expr::parse_expression("y1 - y2"),
],
vec!["y1".to_string(), "y2".to_string()],
"t".to_string(),
0.0,
DVector::from_vec(vec![1.0, 0.0]),
1.0,
0.02,
1e-6,
1e-8,
)
// Step 2: select symbolic source + execution mode.
//
// `ExprLegacy`:
// stable symbolic assembly backend.
// `LambdifyExpr`:
// runtime-generated Rust evaluator closures (no external compiler).
.with_residual_jacobian_source(Lsode2ResidualJacobianSource::Symbolic {
assembly: Lsode2SymbolicAssemblyBackend::ExprLegacy,
execution: Lsode2SymbolicExecutionMode::LambdifyExpr,
})
// Optional:
// even on Lambdify route we can shape generated runtime chunking.
// This is useful when evaluating heavy symbolic kernels on many cores.
.with_backend(
Lsode2BackendConfig::native_sparse_faer().with_generated_backend_target_chunks(4, 4),
)
// Step 3: choose linear algebra structure/policy.
//
// `Sparse + Auto` resolves to sparse LU backend in LSODE2 resolved plan.
.with_linear_system_structure(Lsode2LinearSystemStructure::Sparse)
.with_linear_solver_policy(Lsode2LinearSolverPolicy::Auto)
// Step 4: pick faithful native BDF stepping path.
.with_faithful_bdf_solve(100_000, 100_000);
// Step 5: run through universal ODE facade.
let mut solver = RustedSciThe::numerical::LSODE2::Lsode2Solver::new(config)
.expect("LSODE2 preparation failed");
let _ = solver.solve();
let (t, y) = solver.get_result();
let final_t = t[t.len() - 1];
let final_y1 = y[(y.nrows() - 1, 0)];
let final_y2 = y[(y.nrows() - 1, 1)];
println!("LSODE2 Lambdify guide");
println!("status = {}", solver.get_status().unwrap_or_default());
println!("final_t = {final_t:.6}");
println!("final_y = [{final_y1:.8e}, {final_y2:.8e}]");
println!(
"note = Lambdify route keeps setup simple; move to AOT when Jacobian evaluation becomes expensive."
);
println!(
"note = Dense/Banded routes use the same pattern: change linear structure + policy."
);
if let Some(stats) = solver.get_statistics() {
println!("stats = {}", stats.table_report());
}
}