use crate::numerical::BDF::BDF_solver::{BDF, BdfJacobian, BdfLinearBackend};
use crate::symbolic::symbolic_engine::Expr;
use crate::symbolic::symbolic_ivp::{
IvpBackendError, IvpSymbolicAssemblyBackend, SharedIvpParameterValues,
SymbolicIvpProblemOptions,
};
use crate::symbolic::symbolic_ivp_generated::{
DenseIvpGeneratedBackendMode, IvpBackendStatistics, SymbolicIvpGeneratedBackendConfig,
prepare_generated_symbolic_ivp_problem, prepare_generated_symbolic_ivp_residual_problem,
};
extern crate nalgebra as na;
use crate::Utils::plots::plots;
use crate::numerical::BDF::common::NumberOrVec;
use na::{DMatrix, DVector};
use csv::Writer;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Instant;
type BdfNativeJacobianFactory =
dyn Fn(Option<SharedIvpParameterValues>) -> Box<dyn FnMut(f64, &DVector<f64>) -> BdfJacobian>;
type BdfNativeRhs = Arc<dyn Fn(f64, &DVector<f64>) -> DVector<f64> + Send + Sync>;
type BdfNativeDenseJac = Arc<dyn Fn(f64, &DVector<f64>) -> DMatrix<f64> + Send + Sync>;
#[derive(Clone)]
pub struct BdfSolverOptions {
pub eq_system: Vec<Expr>,
pub values: Vec<String>,
pub arg: String,
pub method: String,
pub t0: f64,
pub y0: DVector<f64>,
pub t_bound: f64,
pub max_step: f64,
pub rtol: f64,
pub atol: f64,
pub jac_sparsity: Option<DMatrix<f64>>,
pub vectorized: bool,
pub first_step: Option<f64>,
pub max_bdf_order: usize,
pub equation_parameters: Option<Vec<String>>,
pub equation_parameter_values: Option<DVector<f64>>,
pub generated_backend_config: SymbolicIvpGeneratedBackendConfig,
pub symbolic_assembly_backend: IvpSymbolicAssemblyBackend,
}
impl BdfSolverOptions {
pub fn new(
eq_system: Vec<Expr>,
values: Vec<String>,
arg: String,
method: String,
t0: f64,
y0: DVector<f64>,
t_bound: f64,
max_step: f64,
rtol: f64,
atol: f64,
jac_sparsity: Option<DMatrix<f64>>,
vectorized: bool,
first_step: Option<f64>,
) -> Self {
Self {
eq_system,
values,
arg,
method,
t0,
y0,
t_bound,
max_step,
rtol,
atol,
jac_sparsity,
vectorized,
first_step,
max_bdf_order: 5,
equation_parameters: None,
equation_parameter_values: None,
generated_backend_config: SymbolicIvpGeneratedBackendConfig::defaults(),
symbolic_assembly_backend: IvpSymbolicAssemblyBackend::ExprLegacy,
}
}
pub fn with_generated_backend_config(
mut self,
config: SymbolicIvpGeneratedBackendConfig,
) -> Self {
self.generated_backend_config = config;
self
}
pub fn with_symbolic_assembly_backend(mut self, backend: IvpSymbolicAssemblyBackend) -> Self {
self.symbolic_assembly_backend = backend;
self
}
pub fn with_equation_parameters(mut self, parameters: Vec<String>) -> Self {
self.equation_parameters = Some(parameters);
self
}
pub fn with_equation_parameter_values(mut self, values: DVector<f64>) -> Self {
self.equation_parameter_values = Some(values);
self
}
pub fn with_max_bdf_order(mut self, max_bdf_order: usize) -> Self {
self.max_bdf_order = max_bdf_order;
self
}
pub fn with_dense_generated_backend_mode(mut self, mode: DenseIvpGeneratedBackendMode) -> Self {
let mut config = SymbolicIvpGeneratedBackendConfig::from_mode(mode);
config.resolver = self.generated_backend_config.resolver.clone();
config.aot_options = self.generated_backend_config.aot_options;
config.aot_codegen_backend = self.generated_backend_config.aot_codegen_backend;
config.aot_c_compiler = self.generated_backend_config.aot_c_compiler.clone();
config.output_parent_dir = self.generated_backend_config.output_parent_dir.clone();
config.crate_name_override = self.generated_backend_config.crate_name_override.clone();
config.module_name_override = self.generated_backend_config.module_name_override.clone();
self.generated_backend_config = config;
self
}
pub fn with_dense_generated_backend_c_tcc(self, output_parent_dir: impl Into<PathBuf>) -> Self {
self.with_generated_backend_config(
SymbolicIvpGeneratedBackendConfig::build_if_missing_release(output_parent_dir)
.with_c_tcc(),
)
}
pub fn with_dense_generated_backend_c_gcc(self, output_parent_dir: impl Into<PathBuf>) -> Self {
self.with_generated_backend_config(
SymbolicIvpGeneratedBackendConfig::build_if_missing_release(output_parent_dir)
.with_c_gcc(),
)
}
pub fn with_dense_generated_backend_zig(self, output_parent_dir: impl Into<PathBuf>) -> Self {
self.with_generated_backend_config(
SymbolicIvpGeneratedBackendConfig::build_if_missing_release(output_parent_dir)
.with_zig(),
)
}
pub fn with_dense_generated_backend_for_repeated_solves(
self,
output_parent_dir: impl Into<PathBuf>,
) -> Self {
self.with_generated_backend_config(
SymbolicIvpGeneratedBackendConfig::build_if_missing_release(output_parent_dir)
.for_repeated_solves(),
)
}
}
pub struct ODEsolver {
eq_system: Vec<Expr>,
values: Vec<String>,
arg: String,
method: String,
t0: f64,
y0: DVector<f64>,
t_bound: f64,
max_step: f64,
rtol: f64,
atol: f64,
#[allow(dead_code)]
jac_sparsity: Option<DMatrix<f64>>,
vectorized: bool,
first_step: Option<f64>,
max_bdf_order: usize,
status: String,
Solver_instance: BDF,
message: Option<String>,
t_result: DVector<f64>,
y_result: DMatrix<f64>,
stop_condition: Option<HashMap<String, f64>>,
equation_parameters: Option<Vec<String>>,
equation_parameter_values: Option<DVector<f64>>,
parameter_values_handle: Option<SharedIvpParameterValues>,
backend_prepared: bool,
generated_backend_config: SymbolicIvpGeneratedBackendConfig,
symbolic_assembly_backend: IvpSymbolicAssemblyBackend,
statistics: Arc<Mutex<IvpBackendStatistics>>,
bdf_linear_backend_factory: Option<Box<dyn Fn() -> Box<dyn BdfLinearBackend>>>,
bdf_native_jacobian_factory: Option<Box<BdfNativeJacobianFactory>>,
native_rhs: Option<BdfNativeRhs>,
native_jacobian: Option<BdfNativeDenseJac>,
}
impl ODEsolver {
pub fn new(
eq_system: Vec<Expr>,
values: Vec<String>,
arg: String,
method: String,
t0: f64,
y0: DVector<f64>,
t_bound: f64,
max_step: f64,
rtol: f64,
atol: f64,
jac_sparsity: Option<DMatrix<f64>>,
vectorized: bool,
first_step: Option<f64>,
) -> Self {
let New = BDF::new();
ODEsolver {
eq_system,
values,
arg,
method,
t0,
y0,
t_bound,
max_step,
rtol,
atol,
jac_sparsity,
vectorized,
first_step,
max_bdf_order: 5,
status: "running".to_string(),
Solver_instance: New,
message: None,
t_result: DVector::zeros(1),
y_result: DMatrix::zeros(1, 1),
stop_condition: None,
equation_parameters: None,
equation_parameter_values: None,
parameter_values_handle: None,
backend_prepared: false,
generated_backend_config: SymbolicIvpGeneratedBackendConfig::defaults(),
symbolic_assembly_backend: IvpSymbolicAssemblyBackend::ExprLegacy,
statistics: Arc::new(Mutex::new(IvpBackendStatistics::default())),
bdf_linear_backend_factory: None,
bdf_native_jacobian_factory: None,
native_rhs: None,
native_jacobian: None,
}
}
pub fn new_with_options(options: BdfSolverOptions) -> Self {
let mut solver = Self::new(
options.eq_system,
options.values,
options.arg,
options.method,
options.t0,
options.y0,
options.t_bound,
options.max_step,
options.rtol,
options.atol,
options.jac_sparsity,
options.vectorized,
options.first_step,
)
.with_generated_backend_config(options.generated_backend_config);
solver.max_bdf_order = options.max_bdf_order;
solver.equation_parameters = options.equation_parameters;
solver.equation_parameter_values = options.equation_parameter_values;
solver.symbolic_assembly_backend = options.symbolic_assembly_backend;
solver
}
pub fn set_generated_backend_config(&mut self, config: SymbolicIvpGeneratedBackendConfig) {
self.generated_backend_config = config;
self.backend_prepared = false;
}
pub fn generated_backend_config(&self) -> &SymbolicIvpGeneratedBackendConfig {
&self.generated_backend_config
}
pub fn symbolic_assembly_backend(&self) -> IvpSymbolicAssemblyBackend {
self.symbolic_assembly_backend
}
pub fn set_symbolic_assembly_backend(&mut self, backend: IvpSymbolicAssemblyBackend) {
self.symbolic_assembly_backend = backend;
self.backend_prepared = false;
}
pub fn get_statistics(&self) -> IvpBackendStatistics {
self.statistics
.lock()
.expect("IVP statistics lock poisoned")
.clone()
}
pub fn statistics_report(&self) -> String {
self.get_statistics().table_report()
}
pub fn bdf_max_order_cap(&self) -> usize {
self.Solver_instance.max_order_cap()
}
pub fn bdf_current_order(&self) -> usize {
self.Solver_instance.current_order()
}
pub fn bdf_equal_step_count(&self) -> usize {
self.Solver_instance.equal_step_count()
}
pub fn set_bdf_linear_backend_factory<F>(&mut self, factory: F)
where
F: Fn() -> Box<dyn BdfLinearBackend> + 'static,
{
self.bdf_linear_backend_factory = Some(Box::new(factory));
self.backend_prepared = false;
}
pub fn with_bdf_linear_backend_factory<F>(mut self, factory: F) -> Self
where
F: Fn() -> Box<dyn BdfLinearBackend> + 'static,
{
self.set_bdf_linear_backend_factory(factory);
self
}
pub fn set_bdf_native_jacobian_factory<F>(&mut self, factory: F)
where
F: Fn(
Option<SharedIvpParameterValues>,
) -> Box<dyn FnMut(f64, &DVector<f64>) -> BdfJacobian>
+ 'static,
{
self.bdf_native_jacobian_factory = Some(Box::new(factory));
self.backend_prepared = false;
}
pub fn with_bdf_native_jacobian_factory<F>(mut self, factory: F) -> Self
where
F: Fn(
Option<SharedIvpParameterValues>,
) -> Box<dyn FnMut(f64, &DVector<f64>) -> BdfJacobian>
+ 'static,
{
self.set_bdf_native_jacobian_factory(factory);
self
}
pub fn set_native_ode_callbacks<F, J>(&mut self, rhs: F, jac: Option<J>)
where
F: Fn(f64, &DVector<f64>) -> DVector<f64> + Send + Sync + 'static,
J: Fn(f64, &DVector<f64>) -> DMatrix<f64> + Send + Sync + 'static,
{
self.native_rhs = Some(Arc::new(rhs));
self.native_jacobian = jac.map(|j| Arc::new(j) as BdfNativeDenseJac);
self.backend_prepared = false;
}
pub fn with_generated_backend_config(
mut self,
config: SymbolicIvpGeneratedBackendConfig,
) -> Self {
self.set_generated_backend_config(config);
self
}
pub fn set_dense_generated_backend_mode(&mut self, mode: DenseIvpGeneratedBackendMode) {
let mut config = SymbolicIvpGeneratedBackendConfig::from_mode(mode);
config.resolver = self.generated_backend_config.resolver.clone();
config.aot_options = self.generated_backend_config.aot_options;
config.aot_codegen_backend = self.generated_backend_config.aot_codegen_backend;
config.aot_c_compiler = self.generated_backend_config.aot_c_compiler.clone();
config.output_parent_dir = self.generated_backend_config.output_parent_dir.clone();
config.crate_name_override = self.generated_backend_config.crate_name_override.clone();
config.module_name_override = self.generated_backend_config.module_name_override.clone();
self.set_generated_backend_config(config);
}
pub fn with_dense_generated_backend_mode(mut self, mode: DenseIvpGeneratedBackendMode) -> Self {
self.set_dense_generated_backend_mode(mode);
self
}
pub fn set_dense_generated_backend_c_tcc(&mut self, output_parent_dir: impl Into<PathBuf>) {
self.set_generated_backend_config(
SymbolicIvpGeneratedBackendConfig::build_if_missing_release(output_parent_dir)
.with_c_tcc(),
);
}
pub fn set_dense_generated_backend_c_gcc(&mut self, output_parent_dir: impl Into<PathBuf>) {
self.set_generated_backend_config(
SymbolicIvpGeneratedBackendConfig::build_if_missing_release(output_parent_dir)
.with_c_gcc(),
);
}
pub fn set_dense_generated_backend_zig(&mut self, output_parent_dir: impl Into<PathBuf>) {
self.set_generated_backend_config(
SymbolicIvpGeneratedBackendConfig::build_if_missing_release(output_parent_dir)
.with_zig(),
);
}
pub fn set_dense_generated_backend_for_repeated_solves(
&mut self,
output_parent_dir: impl Into<PathBuf>,
) {
self.set_generated_backend_config(
SymbolicIvpGeneratedBackendConfig::build_if_missing_release(output_parent_dir)
.for_repeated_solves(),
);
}
pub fn with_dense_generated_backend_c_tcc(
mut self,
output_parent_dir: impl Into<PathBuf>,
) -> Self {
self.set_dense_generated_backend_c_tcc(output_parent_dir);
self
}
pub fn with_dense_generated_backend_c_gcc(
mut self,
output_parent_dir: impl Into<PathBuf>,
) -> Self {
self.set_dense_generated_backend_c_gcc(output_parent_dir);
self
}
pub fn with_dense_generated_backend_zig(
mut self,
output_parent_dir: impl Into<PathBuf>,
) -> Self {
self.set_dense_generated_backend_zig(output_parent_dir);
self
}
pub fn with_dense_generated_backend_for_repeated_solves(
mut self,
output_parent_dir: impl Into<PathBuf>,
) -> Self {
self.set_dense_generated_backend_for_repeated_solves(output_parent_dir);
self
}
pub fn set_stop_condition(&mut self, stop_condition: HashMap<String, f64>) {
self.stop_condition = Some(stop_condition);
}
pub fn set_equation_parameters(&mut self, params: Option<&[&str]>) {
self.equation_parameters =
params.map(|params| params.iter().map(|p| (*p).to_string()).collect());
self.backend_prepared = false;
}
pub fn set_parameter_values(&mut self, values: DVector<f64>) -> Result<(), IvpBackendError> {
if let Some(parameters) = self.equation_parameters.as_ref() {
if parameters.len() != values.len() {
return Err(IvpBackendError::ParameterCountMismatch {
expected: parameters.len(),
actual: values.len(),
});
}
} else if !values.is_empty() {
return Err(IvpBackendError::ParameterCountMismatch {
expected: 0,
actual: values.len(),
});
}
if let Some(handle) = self.parameter_values_handle.as_ref() {
let mut slot = handle
.write()
.expect("shared IVP parameter state lock poisoned");
*slot = values.clone();
}
self.equation_parameter_values = Some(values);
Ok(())
}
fn check_stop_condition(&self, y: &DVector<f64>) -> bool {
if let Some(ref conditions) = self.stop_condition {
for (var_name, target_value) in conditions {
if let Some(var_index) = self.values.iter().position(|v| v == var_name) {
let current_value = y[var_index];
if (current_value - target_value).abs() <= self.atol {
return true;
}
}
}
}
false
}
pub fn try_generate(&mut self) -> Result<(), IvpBackendError> {
if self.native_rhs.is_some() {
return self.try_generate_native_numeric();
}
let start = Instant::now();
let mut options = SymbolicIvpProblemOptions::new();
if let Some(parameters) = self.equation_parameters.clone() {
options = options.with_equation_parameters(parameters);
}
if let Some(values) = self.equation_parameter_values.clone() {
options = options.with_equation_parameter_values(values);
}
options = options.with_symbolic_assembly_backend(self.symbolic_assembly_backend);
if self.bdf_native_jacobian_factory.is_some() {
return self.try_generate_with_native_jacobian(start, options);
}
let prepared = prepare_generated_symbolic_ivp_problem(
self.eq_system.clone(),
self.values.clone(),
self.arg.clone(),
options.with_aot_options(self.generated_backend_config.aot_options),
self.generated_backend_config.clone(),
)
.map_err(|err| IvpBackendError::GeneratedBackendFailure {
message: err.to_string(),
})?;
self.generated_backend_config.resolver = prepared.updated_resolver.clone();
let prepared_problem = prepared.into_problem();
let parameter_values_handle = prepared_problem.parameter_values_handle();
let fun = prepared_problem.residual;
let jac = prepared_problem.jacobian;
let stats_for_fun = Arc::clone(&self.statistics);
let wrapped_fun = Box::new(move |t: f64, y: &DVector<f64>| -> DVector<f64> {
let start = Instant::now();
let out = fun(t, y);
stats_for_fun
.lock()
.expect("IVP statistics lock poisoned")
.record_residual_duration(start.elapsed());
out
});
let stats_for_jac = Arc::clone(&self.statistics);
let wrapped_jac = Box::new(move |t: f64, y: &DVector<f64>| -> DMatrix<f64> {
let start = Instant::now();
let out = jac(t, y);
stats_for_jac
.lock()
.expect("IVP statistics lock poisoned")
.record_jacobian_duration(start.elapsed());
out
});
self.parameter_values_handle = parameter_values_handle.clone();
if self.method == "BDF" {
let mut Solver_instance = BDF::new();
Solver_instance.set_max_order_cap(self.max_bdf_order);
Solver_instance.set_initial(
wrapped_fun,
self.t0,
self.y0.clone(),
self.t_bound,
self.max_step,
NumberOrVec::Number(self.rtol),
NumberOrVec::Number(self.atol),
Some(wrapped_jac),
None,
self.vectorized,
self.first_step,
);
if let Some(factory) = self.bdf_native_jacobian_factory.as_ref() {
Solver_instance.set_native_jacobian(
self.timed_native_jacobian(factory(parameter_values_handle.clone())),
);
}
if let Some(factory) = self.bdf_linear_backend_factory.as_ref() {
Solver_instance.set_linear_backend(factory());
}
self.Solver_instance = Solver_instance;
}
self.backend_prepared = true;
self.statistics
.lock()
.expect("IVP statistics lock poisoned")
.record_backend_prepare_duration(start.elapsed());
Ok(())
}
fn try_generate_native_numeric(&mut self) -> Result<(), IvpBackendError> {
let start = Instant::now();
if self.method == "BDF" {
let rhs = self
.native_rhs
.clone()
.expect("native_rhs must exist in native numeric generation path");
let stats_for_fun = Arc::clone(&self.statistics);
let wrapped_fun = Box::new(move |t: f64, y: &DVector<f64>| -> DVector<f64> {
let start = Instant::now();
let out = rhs(t, y);
stats_for_fun
.lock()
.expect("IVP statistics lock poisoned")
.record_residual_duration(start.elapsed());
out
});
let wrapped_jac = self.native_jacobian.clone().map(|jac| {
let stats_for_jac = Arc::clone(&self.statistics);
Box::new(move |t: f64, y: &DVector<f64>| -> DMatrix<f64> {
let start = Instant::now();
let out = jac(t, y);
stats_for_jac
.lock()
.expect("IVP statistics lock poisoned")
.record_jacobian_duration(start.elapsed());
out
}) as Box<dyn Fn(f64, &DVector<f64>) -> DMatrix<f64>>
});
let mut solver_instance = BDF::new();
solver_instance.set_max_order_cap(self.max_bdf_order);
solver_instance.set_initial(
wrapped_fun,
self.t0,
self.y0.clone(),
self.t_bound,
self.max_step,
NumberOrVec::Number(self.rtol),
NumberOrVec::Number(self.atol),
wrapped_jac,
self.jac_sparsity.clone(),
self.vectorized,
self.first_step,
);
if let Some(factory) = self.bdf_linear_backend_factory.as_ref() {
solver_instance.set_linear_backend(factory());
}
self.Solver_instance = solver_instance;
}
self.backend_prepared = true;
self.statistics
.lock()
.expect("IVP statistics lock poisoned")
.record_backend_prepare_duration(start.elapsed());
Ok(())
}
fn try_generate_with_native_jacobian(
&mut self,
start: std::time::Instant,
options: SymbolicIvpProblemOptions,
) -> Result<(), IvpBackendError> {
let prepared = prepare_generated_symbolic_ivp_residual_problem(
self.eq_system.clone(),
self.values.clone(),
self.arg.clone(),
options.with_aot_options(self.generated_backend_config.aot_options),
self.generated_backend_config.clone(),
)
.map_err(|err| IvpBackendError::GeneratedBackendFailure {
message: err.to_string(),
})?;
self.generated_backend_config.resolver = prepared.updated_resolver.clone();
let prepared_problem = prepared.into_problem();
let parameter_values_handle = prepared_problem.parameter_values_handle();
let fun = prepared_problem.residual;
let stats_for_fun = Arc::clone(&self.statistics);
let wrapped_fun = Box::new(move |t: f64, y: &DVector<f64>| -> DVector<f64> {
let start = Instant::now();
let out = fun(t, y);
stats_for_fun
.lock()
.expect("IVP statistics lock poisoned")
.record_residual_duration(start.elapsed());
out
});
self.parameter_values_handle = parameter_values_handle.clone();
if self.method == "BDF" {
let mut solver_instance = BDF::new();
solver_instance.set_max_order_cap(self.max_bdf_order);
solver_instance.set_initial(
wrapped_fun,
self.t0,
self.y0.clone(),
self.t_bound,
self.max_step,
NumberOrVec::Number(self.rtol),
NumberOrVec::Number(self.atol),
None,
None,
self.vectorized,
self.first_step,
);
if let Some(factory) = self.bdf_native_jacobian_factory.as_ref() {
solver_instance.set_native_jacobian(
self.timed_native_jacobian(factory(parameter_values_handle.clone())),
);
}
if let Some(factory) = self.bdf_linear_backend_factory.as_ref() {
solver_instance.set_linear_backend(factory());
}
self.Solver_instance = solver_instance;
}
self.backend_prepared = true;
self.statistics
.lock()
.expect("IVP statistics lock poisoned")
.record_backend_prepare_duration(start.elapsed());
Ok(())
}
fn timed_native_jacobian(
&self,
mut jacobian: Box<dyn FnMut(f64, &DVector<f64>) -> BdfJacobian>,
) -> Box<dyn FnMut(f64, &DVector<f64>) -> BdfJacobian> {
let stats_for_jac = Arc::clone(&self.statistics);
Box::new(move |t: f64, y: &DVector<f64>| -> BdfJacobian {
let start = Instant::now();
let out = jacobian(t, y);
stats_for_jac
.lock()
.expect("IVP statistics lock poisoned")
.record_jacobian_duration(start.elapsed());
out
})
}
pub fn generate(&mut self) {
self.try_generate()
.expect("BDF symbolic IVP backend generation should succeed");
}
pub fn step(&mut self) {
let t = self.Solver_instance.t;
if t == self.t_bound {
self.Solver_instance.t_old = Some(t);
self.status = "finished".to_string();
} else {
let (success, message_) = self.Solver_instance._step_impl();
if let Some(message_str) = message_ {
self.message = Some(message_str.to_string());
} else {
self.message = None;
}
if success == false {
self.status = "failed".to_string();
} else {
self.Solver_instance.t_old = Some(t);
let _status: String = "running".to_string();
if self.Solver_instance.direction * (self.Solver_instance.t - self.t_bound) >= 0.0 {
self.status = "finished".to_string();
}
}
}
}
#[warn(unused_assignments)]
pub fn main_loop(&mut self) -> () {
let start = Instant::now();
let mut integr_status: Option<i8> = None;
let mut y: Vec<DVector<f64>> = Vec::new();
let mut t: Vec<f64> = Vec::new();
let mut _i: i64 = 0;
let (nfev_before, njev_before, nlu_before) = self.Solver_instance.counters();
while integr_status.is_none() {
self.step();
self.statistics
.lock()
.expect("IVP statistics lock poisoned")
.step_calls += 1;
let _status: i8 = 0;
_i += 1;
if self.status == "finished".to_string() {
integr_status = Some(0)
} else if self.status == "failed".to_string() {
integr_status = Some(-1);
break;
}
if self.check_stop_condition(&self.Solver_instance.y) {
self.status = "stopped_by_condition".to_string();
integr_status = Some(0);
}
t.push(self.Solver_instance.t);
y.push(self.Solver_instance.y.clone());
}
let rows = &y.len();
let cols = &y[0].len();
let mut flat_vec: Vec<f64> = Vec::new();
for vector in y.iter() {
flat_vec.extend(vector)
}
let y_res: DMatrix<f64> = DMatrix::from_vec(*cols, *rows, flat_vec).transpose();
let t_res = DVector::from_vec(t);
let duration = start.elapsed();
println!("Program took {} milliseconds to run", duration.as_millis());
let (nfev_after, njev_after, nlu_after) = self.Solver_instance.counters();
let mut stats = self
.statistics
.lock()
.expect("IVP statistics lock poisoned");
stats.record_solve_duration(duration);
stats.bdf_nfev_total += nfev_after.saturating_sub(nfev_before);
stats.bdf_njev_total += njev_after.saturating_sub(njev_before);
stats.bdf_nlu_total += nlu_after.saturating_sub(nlu_before);
self.t_result = t_res.clone();
self.y_result = y_res.clone();
}
pub fn solve(&mut self) -> () {
if !self.backend_prepared {
self.generate();
}
self.main_loop();
}
pub fn plot_result(&self) -> () {
plots(
self.arg.clone(),
self.values.clone(),
self.t_result.clone(),
self.y_result.clone(),
);
println!("result plotted");
}
pub fn get_result(&self) -> (DVector<f64>, DMatrix<f64>) {
(self.t_result.clone(), self.y_result.clone())
}
pub fn get_status(&self) -> &String {
&self.status
}
pub fn save_result(&self) -> Result<(), Box<dyn std::error::Error>> {
let path = format!(
"f:\\RUST\\RustProjects_\\RustedSciThe3\\src\\numerical\\results\\{}+{}.csv",
self.arg,
self.values.join("+")
);
let mut wtr = Writer::from_path(path)?;
wtr.write_record(&[&self.arg, "values"])?;
wtr.write_record(self.t_result.iter().map(|&x| x.to_string()))?;
for (i, col) in self.y_result.column_iter().enumerate() {
let col_name = format!("{}", &self.values[i]);
wtr.write_record(&[
&col_name,
&col.iter()
.map(|&x| x.to_string())
.collect::<Vec<_>>()
.join(","),
])?;
}
print!("result saved");
wtr.flush()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::symbolic::symbolic_engine::Expr;
use crate::symbolic::symbolic_ivp_generated::SymbolicIvpAotBuildPolicy;
use std::collections::HashMap;
#[test]
fn bdf_new_with_options_installs_generated_backend_mode() {
let solver = ODEsolver::new_with_options(
BdfSolverOptions::new(
vec![Expr::parse_expression("y")],
vec!["y".to_string()],
"t".to_string(),
"BDF".to_string(),
0.0,
DVector::from_vec(vec![1.0]),
1.0,
0.1,
1e-6,
1e-8,
None,
false,
None,
)
.with_dense_generated_backend_mode(DenseIvpGeneratedBackendMode::BuildIfMissingRelease),
);
assert_eq!(
solver.generated_backend_config().build_policy,
SymbolicIvpAotBuildPolicy::BuildIfMissing {
profile: crate::symbolic::codegen::rust_backend::codegen_aot_build::AotBuildProfile::Release
}
);
}
#[test]
fn generated_backend_surface_mode_updates_bdf_config() {
let solver = ODEsolver::new(
vec![Expr::parse_expression("y")],
vec!["y".to_string()],
"t".to_string(),
"BDF".to_string(),
0.0,
DVector::from_vec(vec![1.0]),
1.0,
0.1,
1e-6,
1e-8,
None,
false,
None,
)
.with_dense_generated_backend_mode(DenseIvpGeneratedBackendMode::RequirePrebuilt);
assert_eq!(
solver.generated_backend_config().build_policy,
SymbolicIvpAotBuildPolicy::RequirePrebuilt
);
}
#[test]
fn bdf_generated_backend_surface_keeps_selected_zig_backend() {
let solver = ODEsolver::new(
vec![Expr::parse_expression("y")],
vec!["y".to_string()],
"t".to_string(),
"BDF".to_string(),
0.0,
DVector::from_vec(vec![1.0]),
1.0,
0.1,
1e-6,
1e-8,
None,
false,
None,
)
.with_dense_generated_backend_zig("target/generated-ivp-tests")
.with_dense_generated_backend_mode(DenseIvpGeneratedBackendMode::BuildIfMissingRelease);
assert_eq!(
solver.generated_backend_config().aot_codegen_backend,
crate::symbolic::codegen::codegen_aot_driver::AotCodegenBackend::Zig
);
assert_eq!(solver.generated_backend_config().aot_c_compiler, None);
}
#[test]
fn bdf_generated_backend_repeated_solves_alias_prefers_c_gcc() {
let solver = ODEsolver::new(
vec![Expr::parse_expression("y")],
vec!["y".to_string()],
"t".to_string(),
"BDF".to_string(),
0.0,
DVector::from_vec(vec![1.0]),
1.0,
0.1,
1e-6,
1e-8,
None,
false,
None,
)
.with_dense_generated_backend_for_repeated_solves("target/generated-ivp-tests");
assert_eq!(
solver.generated_backend_config().aot_codegen_backend,
crate::symbolic::codegen::codegen_aot_driver::AotCodegenBackend::C
);
assert_eq!(
solver.generated_backend_config().aot_c_compiler.as_deref(),
Some("gcc")
);
}
#[test]
fn bdf_options_can_set_symbolic_assembly_backend() {
let solver = ODEsolver::new_with_options(
BdfSolverOptions::new(
vec![Expr::parse_expression("y")],
vec!["y".to_string()],
"t".to_string(),
"BDF".to_string(),
0.0,
DVector::from_vec(vec![1.0]),
1.0,
0.1,
1e-6,
1e-8,
None,
false,
None,
)
.with_symbolic_assembly_backend(IvpSymbolicAssemblyBackend::AtomView),
);
assert_eq!(
solver.symbolic_assembly_backend(),
IvpSymbolicAssemblyBackend::AtomView
);
}
#[test]
fn test_bdf_riccati_equation() {
let eq1 = Expr::parse_expression("y*y - t*t");
let eq_system = vec![eq1];
let values = vec!["y".to_string()];
let arg = "t".to_string();
let method = "BDF".to_string();
let t0 = 0.0;
let y0 = DVector::from_vec(vec![1.0]);
let t_bound = 0.5;
let max_step = 0.001;
let rtol = 1e-8;
let atol = 1e-10;
let mut solver = ODEsolver::new(
eq_system, values, arg, method, t0, y0, t_bound, max_step, rtol, atol, None, false,
None,
);
solver.solve();
assert_eq!(solver.get_status(), "finished");
let (t_result, y_result) = solver.get_result();
for i in 0..t_result.len() {
assert!(y_result[(i, 0)].is_finite());
assert!(y_result[(i, 0)] > 0.0); }
}
#[test]
fn test_bdf_van_der_pol_oscillator() {
let eq1 = Expr::parse_expression("y2");
let eq2 = Expr::parse_expression("5*(1-y1*y1)*y2 - y1");
let eq_system = vec![eq1, eq2];
let values = vec!["y1".to_string(), "y2".to_string()];
let arg = "t".to_string();
let method = "BDF".to_string();
let t0 = 0.0;
let y0 = DVector::from_vec(vec![2.0, 0.0]);
let t_bound = 5.0;
let max_step = 0.01;
let rtol = 1e-6;
let atol = 1e-8;
let mut solver = ODEsolver::new(
eq_system, values, arg, method, t0, y0, t_bound, max_step, rtol, atol, None, false,
None,
);
solver.solve();
assert_eq!(solver.get_status(), "finished");
let (_, y_result) = solver.get_result();
assert!(y_result[(y_result.nrows() - 1, 0)].abs() < 3.0); }
#[test]
fn test_bdf_bernoulli_equation() {
let eq1 = Expr::parse_expression("y*y*y - y");
let eq_system = vec![eq1];
let values = vec!["y".to_string()];
let arg = "t".to_string();
let method = "BDF".to_string();
let t0 = 0.0;
let y0 = DVector::from_vec(vec![0.5]);
let t_bound = 0.3;
let max_step = 0.001;
let rtol = 1e-8;
let atol = 1e-10;
let mut solver = ODEsolver::new(
eq_system, values, arg, method, t0, y0, t_bound, max_step, rtol, atol, None, false,
None,
);
solver.solve();
assert_eq!(solver.get_status(), "finished");
let (t_result, y_result) = solver.get_result();
let t_final = t_result[t_result.len() - 1];
let y_analytical = 1.0 / (3.0 * (2.0 * t_final).exp() + 1.0).sqrt();
let y_numerical = y_result[(y_result.nrows() - 1, 0)];
assert!(
(y_numerical - y_analytical).abs() < 1e-4,
"Numerical: {}, Analytical: {}, Error: {}",
y_numerical,
y_analytical,
(y_numerical - y_analytical).abs()
);
}
#[test]
fn test_bdf_logistic_equation() {
let eq1 = Expr::parse_expression("2*y*(1-y/10)");
let eq_system = vec![eq1];
let values = vec!["y".to_string()];
let arg = "t".to_string();
let method = "BDF".to_string();
let t0 = 0.0;
let y0 = DVector::from_vec(vec![1.0]);
let t_bound = 3.0;
let max_step = 0.01;
let rtol = 1e-8;
let atol = 1e-10;
let mut solver = ODEsolver::new(
eq_system, values, arg, method, t0, y0, t_bound, max_step, rtol, atol, None, false,
None,
);
solver.solve();
assert_eq!(solver.get_status(), "finished");
let (t_result, y_result) = solver.get_result();
let r = 2.0;
let k = 10.0;
let y0_val = 1.0;
for i in 0..t_result.len() {
let t = t_result[i];
let y_analytical = k * y0_val * (r * t).exp() / (k + y0_val * ((r * t).exp() - 1.0));
let y_numerical = y_result[(i, 0)];
assert!(
(y_numerical - y_analytical).abs() < 1e-5,
"At t={}: Numerical: {}, Analytical: {}, Error: {}",
t,
y_numerical,
y_analytical,
(y_numerical - y_analytical).abs()
);
}
}
#[test]
fn test_bdf_pendulum_equation() {
let eq1 = Expr::parse_expression("omega");
let eq2 = Expr::parse_expression("-sin(theta)");
let eq_system = vec![eq1, eq2];
let values = vec!["theta".to_string(), "omega".to_string()];
let arg = "t".to_string();
let method = "BDF".to_string();
let t0 = 0.0;
let y0 = DVector::from_vec(vec![1.0, 0.0]); let t_bound = 1.0;
let max_step = 0.001;
let rtol = 1e-6;
let atol = 1e-8;
let mut solver = ODEsolver::new(
eq_system, values, arg, method, t0, y0, t_bound, max_step, rtol, atol, None, false,
None,
);
solver.solve();
assert_eq!(solver.get_status(), "finished");
let (_, y_result) = solver.get_result();
println!(
"1st and last teta {}, {}",
y_result[(0, 0)],
y_result[(y_result.nrows() - 1, 0)]
);
println!(
"1st and last omega {}, {}",
y_result[(0, 1)],
y_result[(y_result.nrows() - 1, 1)]
);
let final_theta = y_result[(y_result.nrows() - 1, 0)];
let final_omega = y_result[(y_result.nrows() - 1, 1)];
let final_energy = 0.5 * final_omega.powi(2) - (1.0_f64.cos() - final_theta.cos());
assert!(
final_energy.abs() < 1e-3,
"Energy not conserved: Initial: {}",
final_energy
);
}
#[test]
fn test_bdf_lorenz_system() {
let eq1 = Expr::parse_expression("10*(y-x)");
let eq2 = Expr::parse_expression("x*(28-z)-y");
let eq3 = Expr::parse_expression("x*y-8*z/3");
let eq_system = vec![eq1, eq2, eq3];
let values = vec!["x".to_string(), "y".to_string(), "z".to_string()];
let arg = "t".to_string();
let method = "BDF".to_string();
let t0 = 0.0;
let y0 = DVector::from_vec(vec![1.0, 1.0, 1.0]);
let t_bound = 5.0;
let max_step = 0.001;
let rtol = 1e-8;
let atol = 1e-10;
let mut solver = ODEsolver::new(
eq_system, values, arg, method, t0, y0, t_bound, max_step, rtol, atol, None, false,
None,
);
solver.solve();
assert_eq!(solver.get_status(), "finished");
let (_, y_result) = solver.get_result();
for i in 0..y_result.nrows() {
assert!(y_result[(i, 0)].abs() < 50.0); assert!(y_result[(i, 1)].abs() < 50.0); assert!(y_result[(i, 2)] > 0.0 && y_result[(i, 2)] < 50.0); }
}
#[test]
fn test_bdf_stiff_chemical_reaction() {
let eq1 = Expr::parse_expression("-y1");
let eq2 = Expr::parse_expression("y1 - 1000*y2");
let eq3 = Expr::parse_expression("1000*y2");
let eq_system = vec![eq1, eq2, eq3];
let values = vec!["y1".to_string(), "y2".to_string(), "y3".to_string()];
let arg = "t".to_string();
let method = "BDF".to_string();
let t0 = 0.0;
let y0 = DVector::from_vec(vec![1.0, 0.0, 0.0]);
let t_bound = 2.0;
let max_step = 0.01;
let rtol = 1e-6;
let atol = 1e-8;
let mut solver = ODEsolver::new(
eq_system, values, arg, method, t0, y0, t_bound, max_step, rtol, atol, None, false,
None,
);
solver.solve();
assert_eq!(solver.get_status(), "finished");
let (t_result, y_result) = solver.get_result();
let final_sum = y_result[(y_result.nrows() - 1, 0)]
+ y_result[(y_result.nrows() - 1, 1)]
+ y_result[(y_result.nrows() - 1, 2)];
assert!(
(final_sum - 1.0).abs() < 1e-6,
"Mass not conserved: {}",
final_sum
);
let t_final = t_result[t_result.len() - 1];
let y1_analytical = (-t_final).exp();
let y1_numerical = y_result[(y_result.nrows() - 1, 0)];
assert!((y1_numerical - y1_analytical).abs() < 1e-4);
}
#[test]
fn test_bdf_stop_condition_single_variable() {
let eq1 = Expr::parse_expression("y");
let eq_system = vec![eq1];
let values = vec!["y".to_string()];
let arg = "t".to_string();
let method = "BDF".to_string();
let t0 = 0.0;
let y0 = DVector::from_vec(vec![1.0]);
let t_bound = 10.0;
let max_step = 0.01;
let rtol = 1e-6;
let atol = 1e-3;
let mut solver = ODEsolver::new(
eq_system, values, arg, method, t0, y0, t_bound, max_step, rtol, atol, None, false,
None,
);
let mut stop_condition = HashMap::new();
stop_condition.insert("y".to_string(), 2.0);
solver.set_stop_condition(stop_condition);
solver.solve();
assert_eq!(solver.get_status(), "stopped_by_condition");
let (_, y_result) = solver.get_result();
let final_y = y_result[(y_result.nrows() - 1, 0)];
assert!((final_y - 2.0).abs() <= atol);
}
#[test]
fn test_bdf_stop_condition_multiple_variables() {
let eq1 = Expr::parse_expression("y2");
let eq2 = Expr::parse_expression("-y1");
let eq_system = vec![eq1, eq2];
let values = vec!["y1".to_string(), "y2".to_string()];
let arg = "t".to_string();
let method = "BDF".to_string();
let t0 = 0.0;
let y0 = DVector::from_vec(vec![1.0, 0.0]);
let t_bound = 10.0;
let max_step = 0.01;
let rtol = 1e-6;
let atol = 1e-3;
let mut solver = ODEsolver::new(
eq_system, values, arg, method, t0, y0, t_bound, max_step, rtol, atol, None, false,
None,
);
let mut stop_condition = HashMap::new();
stop_condition.insert("y1".to_string(), 0.0);
solver.set_stop_condition(stop_condition);
solver.solve();
assert_eq!(solver.get_status(), "stopped_by_condition");
let (_, y_result) = solver.get_result();
let final_y1 = y_result[(y_result.nrows() - 1, 0)];
assert!(final_y1.abs() <= atol);
}
#[test]
fn test_bdf_no_stop_condition() {
let eq1 = Expr::parse_expression("-y");
let eq_system = vec![eq1];
let values = vec!["y".to_string()];
let arg = "t".to_string();
let method = "BDF".to_string();
let t0 = 0.0;
let y0 = DVector::from_vec(vec![1.0]);
let t_bound = 1.0;
let max_step = 0.1;
let rtol = 1e-6;
let atol = 1e-6;
let mut solver = ODEsolver::new(
eq_system, values, arg, method, t0, y0, t_bound, max_step, rtol, atol, None, false,
None,
);
solver.solve();
assert_eq!(solver.get_status(), "finished");
let (t_result, _) = solver.get_result();
let final_t = t_result[t_result.len() - 1];
assert!((final_t - t_bound).abs() <= max_step);
}
#[test]
fn test_bdf_stop_condition_nonlinear() {
let eq1 = Expr::parse_expression("y*y");
let eq_system = vec![eq1];
let values = vec!["y".to_string()];
let arg = "t".to_string();
let method = "BDF".to_string();
let t0 = 0.0;
let y0 = DVector::from_vec(vec![1.0]);
let t_bound = 10.0;
let max_step = 0.01;
let rtol = 1e-6;
let atol = 1e-3;
let mut solver = ODEsolver::new(
eq_system, values, arg, method, t0, y0, t_bound, max_step, rtol, atol, None, false,
None,
);
let mut stop_condition = HashMap::new();
stop_condition.insert("y".to_string(), 1.5);
solver.set_stop_condition(stop_condition);
solver.solve();
assert_eq!(solver.get_status(), "stopped_by_condition");
let (_, y_result) = solver.get_result();
let final_y = y_result[(y_result.nrows() - 1, 0)];
assert!((final_y - 1.5).abs() <= atol);
}
}
#[cfg(test)]
mod tests_generated_backend_heavy_dense_aot {
use super::*;
use crate::symbolic::codegen::codegen_runtime_api::{
recommended_dense_jacobian_chunking_for_parallelism,
recommended_residual_chunking_for_parallelism,
};
use crate::symbolic::codegen::rust_backend::codegen_aot_build::AotBuildProfile;
use crate::symbolic::symbolic_ivp::SymbolicIvpAotOptions;
use crate::symbolic::symbolic_ivp_generated::{
DenseIvpGeneratedBackendMode, SymbolicIvpAotBuildPolicy, SymbolicIvpGeneratedBackendConfig,
};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::path::PathBuf;
use std::process::Command;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
#[derive(Clone)]
struct BdfScenario {
label: &'static str,
equations: Vec<Expr>,
values: Vec<String>,
y0: DVector<f64>,
t0: f64,
t_bound: f64,
max_step: f64,
rtol: f64,
atol: f64,
}
#[derive(Clone, Copy)]
enum Toolchain {
Ctcc,
Cgcc,
Zig,
Rust,
}
impl Toolchain {
fn label(self) -> &'static str {
match self {
Self::Ctcc => "AOT-C-tcc",
Self::Cgcc => "AOT-C-gcc",
Self::Zig => "AOT-Zig",
Self::Rust => "AOT-Rust",
}
}
}
#[derive(Clone, Copy)]
enum ChunkingMode {
Whole,
Parallel2,
}
impl ChunkingMode {
fn label(self) -> &'static str {
match self {
Self::Whole => "whole",
Self::Parallel2 => "parallel(auto,x2)",
}
}
}
struct CompareRow {
scenario: &'static str,
route: String,
chunking: &'static str,
total: Duration,
prepare_ms: f64,
solve_ms: f64,
residual_calls: usize,
jacobian_calls: usize,
nlu: usize,
final_diff: f64,
status: String,
}
fn command_exists(cmd: &str, probe_arg: &str) -> bool {
Command::new(cmd).arg(probe_arg).output().is_ok()
}
fn tcc_available() -> bool {
if let Ok(explicit) = std::env::var("RUSTEDSCITHE_TCC") {
return std::path::Path::new(&explicit).is_file();
}
command_exists("tcc", "-v")
}
fn gcc_available() -> bool {
if let Ok(explicit) = std::env::var("RUSTEDSCITHE_GCC") {
return std::path::Path::new(&explicit).is_file();
}
command_exists("gcc", "--version")
}
fn zig_available() -> bool {
command_exists("zig", "version")
}
fn toolchain_available(toolchain: Toolchain) -> bool {
match toolchain {
Toolchain::Ctcc => tcc_available(),
Toolchain::Cgcc => gcc_available(),
Toolchain::Zig => zig_available(),
Toolchain::Rust => true,
}
}
fn robertson_3_scenario() -> BdfScenario {
BdfScenario {
label: "robertson-3",
equations: vec![
Expr::parse_expression("-0.04*y1 + 1.0e4*y2*y3"),
Expr::parse_expression("0.04*y1 - 1.0e4*y2*y3 - 3.0e7*y2^2"),
Expr::parse_expression("3.0e7*y2^2"),
],
values: vec!["y1".to_string(), "y2".to_string(), "y3".to_string()],
y0: DVector::from_vec(vec![1.0, 0.0, 0.0]),
t0: 0.0,
t_bound: 20.0,
max_step: 0.001,
rtol: 1e-9,
atol: 1e-12,
}
}
fn hires_8_scenario() -> BdfScenario {
BdfScenario {
label: "hires-8",
equations: vec![
Expr::parse_expression("-1.71*y1 + 0.43*y2 + 8.32*y3 + 0.0007"),
Expr::parse_expression("1.71*y1 - 8.75*y2"),
Expr::parse_expression("-10.03*y3 + 0.43*y4 + 0.035*y5"),
Expr::parse_expression("8.32*y2 + 1.71*y3 - 1.12*y4"),
Expr::parse_expression("-1.745*y5 + 0.43*y6 + 0.43*y7"),
Expr::parse_expression("-280.0*y6*y8 + 0.69*y4 + 1.71*y5 - 0.43*y6 + 0.69*y7"),
Expr::parse_expression("280.0*y6*y8 - 1.81*y7"),
Expr::parse_expression("-280.0*y6*y8 + 1.81*y7"),
],
values: vec![
"y1".to_string(),
"y2".to_string(),
"y3".to_string(),
"y4".to_string(),
"y5".to_string(),
"y6".to_string(),
"y7".to_string(),
"y8".to_string(),
],
y0: DVector::from_vec(vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0057]),
t0: 0.0,
t_bound: 20.0,
max_step: 0.002,
rtol: 1e-8,
atol: 1e-11,
}
}
fn max_abs_diff(a: &DVector<f64>, b: &DVector<f64>) -> f64 {
a.iter()
.zip(b.iter())
.fold(0.0_f64, |acc, (lhs, rhs)| acc.max((lhs - rhs).abs()))
}
fn unique_output_root(prefix: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
PathBuf::from(format!(
"target/generated-bdf-aot-story/{prefix}/pid{}_{}",
std::process::id(),
nanos
))
}
fn chunking_options(var_count: usize, mode: ChunkingMode) -> SymbolicIvpAotOptions {
match mode {
ChunkingMode::Whole => SymbolicIvpAotOptions::default(),
ChunkingMode::Parallel2 => SymbolicIvpAotOptions {
residual_strategy: recommended_residual_chunking_for_parallelism(var_count, 2),
jacobian_strategy: recommended_dense_jacobian_chunking_for_parallelism(
var_count, 2,
),
},
}
}
fn make_backend_config(
out_dir: PathBuf,
toolchain: Toolchain,
chunking: ChunkingMode,
var_count: usize,
) -> SymbolicIvpGeneratedBackendConfig {
let base = SymbolicIvpGeneratedBackendConfig::from_mode(
DenseIvpGeneratedBackendMode::BuildIfMissingRelease,
)
.with_output_parent_dir(Some(out_dir))
.with_build_policy(SymbolicIvpAotBuildPolicy::BuildIfMissing {
profile: AotBuildProfile::Release,
})
.with_aot_options(chunking_options(var_count, chunking));
match toolchain {
Toolchain::Ctcc => base.with_c_tcc(),
Toolchain::Cgcc => base.with_c_gcc(),
Toolchain::Zig => base.with_zig(),
Toolchain::Rust => base.with_rust(),
}
}
fn scenario_options(s: &BdfScenario) -> BdfSolverOptions {
BdfSolverOptions::new(
s.equations.clone(),
s.values.clone(),
"t".to_string(),
"BDF".to_string(),
s.t0,
s.y0.clone(),
s.t_bound,
s.max_step,
s.rtol,
s.atol,
None,
false,
None,
)
.with_max_bdf_order(5)
}
fn run_case(
scenario: &BdfScenario,
route_label: &str,
chunking: ChunkingMode,
options: BdfSolverOptions,
baseline_solution: Option<&DVector<f64>>,
) -> (CompareRow, DVector<f64>) {
let mut solver = ODEsolver::new_with_options(options);
let start = Instant::now();
let solve_result = catch_unwind(AssertUnwindSafe(|| {
solver.solve();
let stats = solver.get_statistics();
let (_, y) = solver.get_result();
let final_solution = if y.nrows() == 0 {
DVector::from_element(scenario.values.len(), f64::NAN)
} else {
y.row(y.nrows() - 1).transpose().into_owned()
};
(
solver.get_status().clone(),
stats.backend_prepare_ms_total,
stats.solve_ms_total,
stats.bdf_nfev_total,
stats.bdf_njev_total,
stats.bdf_nlu_total,
final_solution,
)
}));
let total = start.elapsed();
match solve_result {
Ok((status, prepare_ms, solve_ms, residual_calls, jacobian_calls, nlu, solution)) => {
let final_diff = baseline_solution
.map(|baseline| max_abs_diff(&solution, baseline))
.unwrap_or(0.0);
(
CompareRow {
scenario: scenario.label,
route: route_label.to_string(),
chunking: chunking.label(),
total,
prepare_ms,
solve_ms,
residual_calls,
jacobian_calls,
nlu,
final_diff,
status,
},
solution,
)
}
Err(_) => (
CompareRow {
scenario: scenario.label,
route: route_label.to_string(),
chunking: chunking.label(),
total,
prepare_ms: f64::NAN,
solve_ms: f64::NAN,
residual_calls: 0,
jacobian_calls: 0,
nlu: 0,
final_diff: f64::NAN,
status: "panic".to_string(),
},
DVector::from_element(scenario.values.len(), f64::NAN),
),
}
}
#[test]
#[ignore]
fn bdf_dense_aot_heavy_toolchain_chunking_matrix_story() {
let scenarios = vec![robertson_3_scenario(), hires_8_scenario()];
let mut rows = Vec::<CompareRow>::new();
for scenario in &scenarios {
let (baseline_row, baseline_solution) = run_case(
scenario,
"Lambdify",
ChunkingMode::Whole,
scenario_options(scenario),
None,
);
rows.push(baseline_row);
for toolchain in [
Toolchain::Ctcc,
Toolchain::Cgcc,
Toolchain::Zig,
Toolchain::Rust,
] {
if !toolchain_available(toolchain) {
println!(
"[BDF AOT heavy] skipping {} on scenario {}: compiler/runtime unavailable",
toolchain.label(),
scenario.label
);
continue;
}
for chunking in [ChunkingMode::Whole, ChunkingMode::Parallel2] {
let out_dir = unique_output_root(&format!(
"{}_{}_{}",
scenario.label,
toolchain.label(),
chunking.label()
));
let config = make_backend_config(
out_dir,
toolchain,
chunking,
scenario.values.len().max(1),
);
let options = scenario_options(scenario).with_generated_backend_config(config);
let (row, _) = run_case(
scenario,
toolchain.label(),
chunking,
options,
Some(&baseline_solution),
);
rows.push(row);
}
}
}
println!(
"[BDF AOT heavy] dense toolchain+chunking matrix; all time columns are milliseconds"
);
println!(
"scenario | route | chunking | total_ms | prepare_ms | solve_ms | final_diff_vs_lambdify | residual_calls | jacobian_calls | nlu | status"
);
println!(
"---------------------------------------------------------------------------------------------------------------------------------------------------------------"
);
for row in &rows {
println!(
"{:<11} | {:<12} | {:<16} | {:>8.3} | {:>10.3} | {:>8.3} | {:>22.3e} | {:>14} | {:>14} | {:>3} | {}",
row.scenario,
row.route,
row.chunking,
row.total.as_secs_f64() * 1_000.0,
row.prepare_ms,
row.solve_ms,
row.final_diff,
row.residual_calls,
row.jacobian_calls,
row.nlu,
row.status
);
}
let finished: Vec<&CompareRow> =
rows.iter().filter(|row| row.status == "finished").collect();
assert!(
!finished.is_empty(),
"at least one dense BDF heavy AOT route should finish"
);
for row in rows.iter().filter(|row| row.route != "Lambdify") {
assert_eq!(
row.status, "finished",
"dense BDF AOT route failed: scenario={} route={} chunking={}",
row.scenario, row.route, row.chunking
);
assert!(
row.final_diff <= 1e-6,
"dense BDF AOT parity drift is too large: scenario={} route={} chunking={} diff={:e}",
row.scenario,
row.route,
row.chunking,
row.final_diff
);
}
}
}