use crate::DType;
use numr::runtime::Runtime;
use numr::tensor::Tensor;
#[cfg(feature = "sparse")]
use numr::sparse::CsrData;
#[cfg(feature = "sparse")]
pub use crate::integrate::impl_generic::ode::direct_solver_config::{
DirectSolverConfig, SparseSolverStrategy,
};
#[derive(Debug, Clone)]
pub struct SparseJacobianConfig<R: Runtime<DType = DType>> {
pub enabled: bool,
#[cfg(feature = "sparse")]
pub pattern: Option<CsrData<R>>,
pub gmres_tol: f64,
pub max_gmres_iter: usize,
#[cfg(feature = "sparse")]
pub preconditioner: numr::algorithm::iterative::PreconditionerType,
#[cfg(feature = "sparse")]
pub solver_strategy: SparseSolverStrategy,
#[cfg(feature = "sparse")]
pub direct_solver_config: DirectSolverConfig,
_phantom: std::marker::PhantomData<R>,
}
impl<R: Runtime<DType = DType>> Default for SparseJacobianConfig<R> {
fn default() -> Self {
Self {
enabled: false,
#[cfg(feature = "sparse")]
pattern: None,
gmres_tol: 1e-10,
max_gmres_iter: 100,
#[cfg(feature = "sparse")]
preconditioner: numr::algorithm::iterative::PreconditionerType::Ilu0,
#[cfg(feature = "sparse")]
solver_strategy: SparseSolverStrategy::default(),
#[cfg(feature = "sparse")]
direct_solver_config: DirectSolverConfig::default(),
_phantom: std::marker::PhantomData,
}
}
}
impl<R: Runtime<DType = DType>> SparseJacobianConfig<R> {
pub fn disabled() -> Self {
Self {
enabled: false,
..Default::default()
}
}
#[cfg(feature = "sparse")]
pub fn enabled() -> Self {
Self {
enabled: true,
..Default::default()
}
}
#[cfg(feature = "sparse")]
pub fn with_pattern(mut self, pattern: CsrData<R>) -> Self {
self.pattern = Some(pattern);
self
}
pub fn with_gmres_tol(mut self, tol: f64) -> Self {
self.gmres_tol = tol;
self
}
#[cfg(feature = "sparse")]
pub fn with_preconditioner(
mut self,
precond: numr::algorithm::iterative::PreconditionerType,
) -> Self {
self.preconditioner = precond;
self
}
#[cfg(feature = "sparse")]
pub fn with_solver_strategy(mut self, strategy: SparseSolverStrategy) -> Self {
self.solver_strategy = strategy;
self
}
#[cfg(feature = "sparse")]
pub fn with_direct_lu() -> Self {
Self {
enabled: true,
solver_strategy: SparseSolverStrategy::DirectLU,
..Default::default()
}
}
#[cfg(feature = "sparse")]
pub fn with_direct_solver_config(mut self, config: DirectSolverConfig) -> Self {
self.direct_solver_config = config;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ODEMethod {
RK23,
#[default]
RK45,
DOP853,
BDF,
Radau,
LSODA,
Verlet,
Leapfrog,
}
impl ODEMethod {
pub fn order(&self) -> usize {
match self {
Self::RK23 => 3,
Self::RK45 => 5,
Self::DOP853 => 8,
Self::BDF => 5, Self::Radau => 5,
Self::LSODA => 5, Self::Verlet => 2,
Self::Leapfrog => 2,
}
}
pub fn error_order(&self) -> usize {
match self {
Self::RK23 => 2,
Self::RK45 => 4,
Self::DOP853 => 5,
Self::BDF => 4, Self::Radau => 3, Self::LSODA => 4,
Self::Verlet => 2,
Self::Leapfrog => 2,
}
}
pub fn is_implicit(&self) -> bool {
matches!(self, Self::BDF | Self::Radau | Self::LSODA)
}
pub fn is_symplectic(&self) -> bool {
matches!(self, Self::Verlet | Self::Leapfrog)
}
pub fn is_stiff_solver(&self) -> bool {
matches!(self, Self::BDF | Self::Radau | Self::LSODA)
}
}
#[derive(Debug, Clone)]
pub struct ODEOptions {
pub method: ODEMethod,
pub rtol: f64,
pub atol: f64,
pub h0: Option<f64>,
pub max_step: Option<f64>,
pub min_step: Option<f64>,
pub max_steps: usize,
pub dense_output: bool,
}
impl Default for ODEOptions {
fn default() -> Self {
Self {
method: ODEMethod::default(),
rtol: 1e-3,
atol: 1e-6,
h0: None,
max_step: None,
min_step: None,
max_steps: 10000,
dense_output: false,
}
}
}
impl ODEOptions {
pub fn with_tolerances(rtol: f64, atol: f64) -> Self {
Self {
rtol,
atol,
..Default::default()
}
}
pub fn with_method(method: ODEMethod) -> Self {
Self {
method,
..Default::default()
}
}
pub fn method(mut self, method: ODEMethod) -> Self {
self.method = method;
self
}
pub fn tolerances(mut self, rtol: f64, atol: f64) -> Self {
self.rtol = rtol;
self.atol = atol;
self
}
pub fn initial_step(mut self, h0: f64) -> Self {
self.h0 = Some(h0);
self
}
pub fn step_bounds(mut self, min: f64, max: f64) -> Self {
self.min_step = Some(min);
self.max_step = Some(max);
self
}
pub fn max_steps(mut self, n: usize) -> Self {
self.max_steps = n;
self
}
}
#[derive(Debug, Clone)]
pub struct BDFOptions<R: Runtime<DType = DType>> {
pub max_order: usize,
pub newton_tol: f64,
pub max_newton_iter: usize,
pub numerical_jacobian: bool,
pub sparse_jacobian: SparseJacobianConfig<R>,
}
impl<R: Runtime<DType = DType>> Default for BDFOptions<R> {
fn default() -> Self {
Self {
max_order: 5,
newton_tol: 1e-6,
max_newton_iter: 10,
numerical_jacobian: true,
sparse_jacobian: SparseJacobianConfig::default(),
}
}
}
impl<R: Runtime<DType = DType>> BDFOptions<R> {
pub fn max_order(mut self, order: usize) -> Self {
self.max_order = order.clamp(1, 5);
self
}
pub fn newton_params(mut self, tol: f64, max_iter: usize) -> Self {
self.newton_tol = tol;
self.max_newton_iter = max_iter;
self
}
pub fn with_sparse_jacobian(mut self, config: SparseJacobianConfig<R>) -> Self {
self.sparse_jacobian = config;
self
}
}
#[derive(Debug, Clone)]
pub struct RadauOptions<R: Runtime<DType = DType>> {
pub newton_tol: f64,
pub max_newton_iter: usize,
pub simplified_newton: bool,
pub sparse_jacobian: SparseJacobianConfig<R>,
}
impl<R: Runtime<DType = DType>> Default for RadauOptions<R> {
fn default() -> Self {
Self {
newton_tol: 1e-6,
max_newton_iter: 10,
simplified_newton: true,
sparse_jacobian: SparseJacobianConfig::default(),
}
}
}
impl<R: Runtime<DType = DType>> RadauOptions<R> {
pub fn newton_params(mut self, tol: f64, max_iter: usize) -> Self {
self.newton_tol = tol;
self.max_newton_iter = max_iter;
self
}
pub fn with_sparse_jacobian(mut self, config: SparseJacobianConfig<R>) -> Self {
self.sparse_jacobian = config;
self
}
}
#[derive(Debug, Clone)]
pub struct LSODAOptions {
pub stiff_threshold: usize,
pub nonstiff_threshold: usize,
pub max_adams_order: usize,
pub max_bdf_order: usize,
}
impl Default for LSODAOptions {
fn default() -> Self {
Self {
stiff_threshold: 3,
nonstiff_threshold: 10,
max_adams_order: 12,
max_bdf_order: 5,
}
}
}
#[derive(Debug, Clone)]
pub struct SymplecticOptions {
pub dt: f64,
pub n_steps: Option<usize>,
}
impl Default for SymplecticOptions {
fn default() -> Self {
Self {
dt: 0.01,
n_steps: None,
}
}
}
impl SymplecticOptions {
pub fn with_dt(dt: f64) -> Self {
Self { dt, n_steps: None }
}
pub fn with_n_steps(n_steps: usize) -> Self {
Self {
dt: 0.0, n_steps: Some(n_steps),
}
}
}
#[derive(Debug, Clone)]
pub struct BVPOptions {
pub rtol: f64,
pub atol: f64,
pub max_iter: usize,
pub initial_mesh_size: usize,
pub max_mesh_size: usize,
}
impl Default for BVPOptions {
fn default() -> Self {
Self {
rtol: 1e-3,
atol: 1e-6,
max_iter: 100,
initial_mesh_size: 10,
max_mesh_size: 1000,
}
}
}
impl BVPOptions {
pub fn with_tolerances(rtol: f64, atol: f64) -> Self {
Self {
rtol,
atol,
..Default::default()
}
}
pub fn mesh_params(mut self, initial: usize, max: usize) -> Self {
self.initial_mesh_size = initial;
self.max_mesh_size = max;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DAEVariableType {
Differential,
Algebraic,
}
#[derive(Debug, Clone)]
pub struct DAEOptions<R: Runtime<DType = DType>> {
pub variable_types: Option<Vec<DAEVariableType>>,
pub ic_tol: f64,
pub max_ic_iter: usize,
pub newton_tol: f64,
pub max_newton_iter: usize,
pub max_order: usize,
pub exclude_algebraic_from_error: bool,
pub return_yp: bool,
pub sparse_jacobian: SparseJacobianConfig<R>,
}
impl<R: Runtime<DType = DType>> Default for DAEOptions<R> {
fn default() -> Self {
Self {
variable_types: None,
ic_tol: 1e-10,
max_ic_iter: 20,
newton_tol: 1e-6,
max_newton_iter: 10,
max_order: 5,
exclude_algebraic_from_error: true,
return_yp: false,
sparse_jacobian: SparseJacobianConfig::default(),
}
}
}
impl<R: Runtime<DType = DType>> DAEOptions<R> {
pub fn with_variable_types(mut self, types: Vec<DAEVariableType>) -> Self {
self.variable_types = Some(types);
self
}
pub fn with_ic_tolerance(mut self, tol: f64) -> Self {
self.ic_tol = tol;
self
}
pub fn with_newton_params(mut self, tol: f64, max_iter: usize) -> Self {
self.newton_tol = tol;
self.max_newton_iter = max_iter;
self
}
pub fn with_max_order(mut self, order: usize) -> Self {
self.max_order = order.clamp(1, 5);
self
}
pub fn with_exclude_algebraic(mut self, exclude: bool) -> Self {
self.exclude_algebraic_from_error = exclude;
self
}
pub fn with_return_yp(mut self, return_yp: bool) -> Self {
self.return_yp = return_yp;
self
}
pub fn with_sparse_jacobian(mut self, config: SparseJacobianConfig<R>) -> Self {
self.sparse_jacobian = config;
self
}
}
#[derive(Debug, Clone)]
pub struct DAEResultTensor<R: Runtime<DType = DType>> {
pub t: Tensor<R>,
pub y: Tensor<R>,
pub yp: Option<Tensor<R>>,
pub success: bool,
pub message: Option<String>,
pub nfev: usize,
pub njac: usize,
pub n_ic_iter: usize,
pub naccept: usize,
pub nreject: usize,
}
impl<R: Runtime<DType = DType>> DAEResultTensor<R> {
pub fn y_final_vec(&self) -> Vec<f64> {
let shape = self.y.shape();
if shape.len() != 2 || shape[0] == 0 {
return vec![];
}
let n_steps = shape[0];
let n_vars = shape[1];
let all_data: Vec<f64> = self.y.to_vec();
let last_row_start = (n_steps - 1) * n_vars;
all_data[last_row_start..].to_vec()
}
pub fn yp_final_vec(&self) -> Option<Vec<f64>> {
self.yp.as_ref().map(|yp| {
let shape = yp.shape();
if shape.len() != 2 || shape[0] == 0 {
return vec![];
}
let n_steps = shape[0];
let n_vars = shape[1];
let all_data: Vec<f64> = yp.to_vec();
let last_row_start = (n_steps - 1) * n_vars;
all_data[last_row_start..].to_vec()
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EventDirection {
#[default]
Any,
Increasing,
Decreasing,
}
#[derive(Debug, Clone)]
pub struct EventSpec {
pub terminal: bool,
pub direction: EventDirection,
}
impl Default for EventSpec {
fn default() -> Self {
Self {
terminal: false,
direction: EventDirection::Any,
}
}
}
impl EventSpec {
pub fn terminal() -> Self {
Self {
terminal: true,
direction: EventDirection::Any,
}
}
pub fn non_terminal() -> Self {
Self {
terminal: false,
direction: EventDirection::Any,
}
}
pub fn direction(mut self, dir: EventDirection) -> Self {
self.direction = dir;
self
}
}
#[derive(Debug, Clone)]
pub struct EventRecord<R: Runtime<DType = DType>> {
pub t: f64,
pub y: Tensor<R>,
pub event_index: usize,
pub event_value: f64,
}
#[derive(Debug, Clone)]
pub struct EventOptions {
pub root_tol: f64,
pub max_root_iter: usize,
}
impl Default for EventOptions {
fn default() -> Self {
Self {
root_tol: 1e-10,
max_root_iter: 100,
}
}
}
impl EventOptions {
pub fn with_tolerance(mut self, tol: f64) -> Self {
self.root_tol = tol;
self
}
pub fn with_max_iter(mut self, max_iter: usize) -> Self {
self.max_root_iter = max_iter;
self
}
}
#[derive(Debug, Clone)]
pub struct ODEResultWithEvents<R: Runtime<DType = DType>> {
pub t: Tensor<R>,
pub y: Tensor<R>,
pub success: bool,
pub message: Option<String>,
pub nfev: usize,
pub naccept: usize,
pub nreject: usize,
pub method: ODEMethod,
pub events: Vec<EventRecord<R>>,
pub terminated_by_event: bool,
pub terminal_event_index: Option<usize>,
}
impl<R: Runtime<DType = DType>> ODEResultWithEvents<R> {
pub fn y_final_vec(&self) -> Vec<f64> {
let shape = self.y.shape();
if shape.len() != 2 || shape[0] == 0 {
return vec![];
}
let n_steps = shape[0];
let n_vars = shape[1];
let all_data: Vec<f64> = self.y.to_vec();
let last_row_start = (n_steps - 1) * n_vars;
all_data[last_row_start..].to_vec()
}
pub fn events_for(&self, event_index: usize) -> Vec<&EventRecord<R>> {
self.events
.iter()
.filter(|e| e.event_index == event_index)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ode_method() {
assert_eq!(ODEMethod::RK23.order(), 3);
assert_eq!(ODEMethod::RK23.error_order(), 2);
assert_eq!(ODEMethod::RK45.order(), 5);
assert_eq!(ODEMethod::RK45.error_order(), 4);
assert_eq!(ODEMethod::DOP853.order(), 8);
assert_eq!(ODEMethod::DOP853.error_order(), 5);
}
#[test]
fn test_stiff_methods() {
assert!(ODEMethod::BDF.is_implicit());
assert!(ODEMethod::BDF.is_stiff_solver());
assert!(!ODEMethod::BDF.is_symplectic());
assert!(ODEMethod::Radau.is_implicit());
assert!(ODEMethod::LSODA.is_stiff_solver());
}
#[test]
fn test_symplectic_methods() {
assert!(ODEMethod::Verlet.is_symplectic());
assert!(ODEMethod::Leapfrog.is_symplectic());
assert!(!ODEMethod::Verlet.is_implicit());
assert!(!ODEMethod::RK45.is_symplectic());
}
#[test]
fn test_ode_options() {
let opts = ODEOptions::default();
assert_eq!(opts.method, ODEMethod::RK45);
assert_eq!(opts.rtol, 1e-3);
assert_eq!(opts.atol, 1e-6);
let opts = ODEOptions::with_tolerances(1e-6, 1e-9);
assert_eq!(opts.rtol, 1e-6);
assert_eq!(opts.atol, 1e-9);
}
#[test]
fn test_bdf_options() {
use numr::runtime::cpu::CpuRuntime;
let opts = BDFOptions::<CpuRuntime>::default();
assert_eq!(opts.max_order, 5);
assert_eq!(opts.max_newton_iter, 10);
assert!(!opts.sparse_jacobian.enabled);
let opts = BDFOptions::<CpuRuntime>::default()
.max_order(3)
.newton_params(1e-8, 20);
assert_eq!(opts.max_order, 3);
assert_eq!(opts.newton_tol, 1e-8);
assert_eq!(opts.max_newton_iter, 20);
}
#[test]
fn test_radau_options() {
use numr::runtime::cpu::CpuRuntime;
let opts = RadauOptions::<CpuRuntime>::default();
assert!(opts.simplified_newton);
assert!(!opts.sparse_jacobian.enabled);
let opts = RadauOptions::<CpuRuntime>::default().newton_params(1e-10, 15);
assert_eq!(opts.newton_tol, 1e-10);
assert_eq!(opts.max_newton_iter, 15);
}
#[test]
fn test_lsoda_options() {
let opts = LSODAOptions::default();
assert_eq!(opts.stiff_threshold, 3);
assert_eq!(opts.nonstiff_threshold, 10);
assert_eq!(opts.max_adams_order, 12);
assert_eq!(opts.max_bdf_order, 5);
}
#[test]
fn test_symplectic_options() {
let opts = SymplecticOptions::default();
assert_eq!(opts.dt, 0.01);
assert!(opts.n_steps.is_none());
let opts = SymplecticOptions::with_dt(0.001);
assert_eq!(opts.dt, 0.001);
let opts = SymplecticOptions::with_n_steps(1000);
assert_eq!(opts.n_steps, Some(1000));
}
#[test]
fn test_bvp_options() {
let opts = BVPOptions::default();
assert_eq!(opts.initial_mesh_size, 10);
assert_eq!(opts.max_mesh_size, 1000);
let opts = BVPOptions::with_tolerances(1e-6, 1e-9).mesh_params(20, 500);
assert_eq!(opts.rtol, 1e-6);
assert_eq!(opts.initial_mesh_size, 20);
assert_eq!(opts.max_mesh_size, 500);
}
#[test]
fn test_dae_variable_type() {
let diff = DAEVariableType::Differential;
let alg = DAEVariableType::Algebraic;
assert_ne!(diff, alg);
assert_eq!(diff, DAEVariableType::Differential);
}
#[test]
fn test_dae_options() {
use numr::runtime::cpu::CpuRuntime;
let opts = DAEOptions::<CpuRuntime>::default();
assert!(opts.variable_types.is_none());
assert_eq!(opts.ic_tol, 1e-10);
assert_eq!(opts.max_ic_iter, 20);
assert_eq!(opts.newton_tol, 1e-6);
assert_eq!(opts.max_order, 5);
assert!(opts.exclude_algebraic_from_error);
assert!(!opts.return_yp);
let opts = DAEOptions::<CpuRuntime>::default()
.with_variable_types(vec![
DAEVariableType::Differential,
DAEVariableType::Algebraic,
])
.with_ic_tolerance(1e-12)
.with_newton_params(1e-8, 15)
.with_max_order(3)
.with_exclude_algebraic(false)
.with_return_yp(true);
assert!(opts.variable_types.is_some());
assert_eq!(opts.variable_types.as_ref().unwrap().len(), 2);
assert_eq!(opts.ic_tol, 1e-12);
assert_eq!(opts.newton_tol, 1e-8);
assert_eq!(opts.max_newton_iter, 15);
assert_eq!(opts.max_order, 3);
assert!(!opts.exclude_algebraic_from_error);
assert!(opts.return_yp);
}
}