use crate::Utils::postprocessing::{PostprocessDataset, PostprocessPlan};
use crate::command_interpreter::task_parser::{DocumentMap, DocumentParser, Value};
use crate::command_interpreter::task_parser_common::{
SharedEquationParseError, parse_symbolic_equation_system,
};
use crate::numerical::LSODE2::{
Lsode2AotProfile, Lsode2AotToolchain, Lsode2ControllerConfig, Lsode2JacobianBackend,
Lsode2LinearSolverChoice, Lsode2LinearSolverPolicy, Lsode2LinearSystemStructure,
Lsode2NativeExecutionConfig, Lsode2ProblemConfig, Lsode2ResidualJacobianSource,
Lsode2StopComparator, Lsode2StopCondition, Lsode2SymbolicAssemblyBackend,
Lsode2SymbolicExecutionMode,
};
use crate::numerical::ODE_api2::{SolverType, UniversalODESolver};
use crate::numerical::Radau::Radau_main::RadauOrder;
use crate::symbolic::symbolic_engine::Expr;
use nalgebra::{DMatrix, DVector};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TaskKindSpec {
Ivp,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IvpMethodSpec {
NonStiff(String),
Radau3,
Radau5,
Bdf,
BackwardEuler,
Lsode2,
}
impl IvpMethodSpec {
fn to_solver_type(&self) -> SolverType {
match self {
Self::NonStiff(name) => SolverType::NonStiff(name.clone()),
Self::Radau3 => SolverType::Radau(RadauOrder::Order3),
Self::Radau5 => SolverType::Radau(RadauOrder::Order5),
Self::Bdf => SolverType::BDF,
Self::BackwardEuler => SolverType::BackwardEuler,
Self::Lsode2 => SolverType::LSODE2,
}
}
fn from_str(raw: &str) -> Result<Self, IvpTaskError> {
match raw.trim().to_ascii_lowercase().as_str() {
"rk45" => Ok(Self::NonStiff("RK45".to_string())),
"rk4" => Ok(Self::NonStiff("RK4".to_string())),
"euler" => Ok(Self::NonStiff("euler".to_string())),
"ab4" => Ok(Self::NonStiff("AB4".to_string())),
"radau3" | "radau-3" | "radau_iia_3" => Ok(Self::Radau3),
"radau5" | "radau-5" | "radau_iia_5" => Ok(Self::Radau5),
"bdf" => Ok(Self::Bdf),
"backwardeuler" | "backward_euler" | "implicit_euler" => Ok(Self::BackwardEuler),
"lsode2" | "lsode" | "lsoda" => Ok(Self::Lsode2),
other => Err(IvpTaskError::UnknownMethod(other.to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SolverSelectionSpec {
pub task_kind: TaskKindSpec,
pub method: IvpMethodSpec,
}
#[derive(Debug, Clone, PartialEq)]
pub struct EquationSpec {
pub arg: String,
pub unknowns: Vec<String>,
pub rhs: Vec<Expr>,
pub parameter_names: Vec<String>,
pub parameter_values: HashMap<String, f64>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct InitialConditionSpec {
pub t0: f64,
pub t_end: f64,
pub y0: Vec<f64>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct IvpSolverOptionsSpec {
pub step_size: Option<f64>,
pub tolerance: Option<f64>,
pub max_iterations: Option<usize>,
pub rtol: Option<f64>,
pub atol: Option<f64>,
pub max_step: Option<f64>,
pub first_step: Option<f64>,
pub vectorized: Option<bool>,
pub parallel: Option<bool>,
pub neighborhood_check: Option<f64>,
pub lsode2: Option<Lsode2TaskOptionsSpec>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Lsode2TaskOptionsSpec {
pub controller: Option<Lsode2ControllerConfig>,
pub symbolic_assembly: Option<Lsode2SymbolicAssemblyBackend>,
pub symbolic_execution: Option<Lsode2TaskExecutionSpec>,
pub linear_system_structure: Option<Lsode2LinearSystemStructure>,
pub linear_solver_policy: Option<Lsode2LinearSolverPolicy>,
pub native_execution: Option<Lsode2NativeExecutionConfig>,
pub stop_conditions: Vec<Lsode2StopCondition>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Lsode2TaskExecutionSpec {
LambdifyExpr,
Aot {
toolchain: Lsode2AotToolchain,
profile: Lsode2AotProfile,
output_parent_dir: Option<PathBuf>,
},
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct PostprocessingSpec {
pub save_csv: bool,
pub csv_path: Option<String>,
pub save_txt: bool,
pub txt_path: Option<String>,
pub write_report: bool,
pub report_path: Option<String>,
pub plotters_png: bool,
pub plotters_dir: Option<String>,
pub gnuplot_png: bool,
pub gnuplot_dir: Option<String>,
pub terminal_plot: bool,
pub plot: bool,
}
impl PostprocessingSpec {
fn to_plan(&self, default_csv_path: &str) -> PostprocessPlan {
let mut plan = PostprocessPlan::new();
if self.save_csv {
plan = plan.save_csv(
self.csv_path
.clone()
.unwrap_or_else(|| default_csv_path.to_string()),
);
}
if self.save_txt {
plan = plan.save_txt(
self.txt_path
.clone()
.unwrap_or_else(|| "ivp_result.txt".to_string()),
);
}
if self.write_report {
plan = plan.write_report(
self.report_path
.clone()
.unwrap_or_else(|| "ivp_report.md".to_string()),
);
}
if self.plotters_png {
plan = plan.plotters_png(
self.plotters_dir
.clone()
.unwrap_or_else(|| "ivp_plotters".to_string()),
);
}
if self.gnuplot_png {
plan = plan.gnuplot_png(
self.gnuplot_dir
.clone()
.unwrap_or_else(|| "ivp_gnuplot".to_string()),
);
}
if self.terminal_plot {
plan = plan.terminal_plot();
}
plan
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct IvpTaskSpec {
pub solver: SolverSelectionSpec,
pub equations: EquationSpec,
pub initial_conditions: InitialConditionSpec,
pub solver_options: IvpSolverOptionsSpec,
pub postprocessing: PostprocessingSpec,
}
impl IvpTaskSpec {
pub fn problem_spec(&self) -> IvpProblemSpec {
IvpProblemSpec {
equations: self.equations.clone(),
initial_conditions: self.initial_conditions.clone(),
}
}
pub fn solver_settings_spec(&self) -> IvpSolverSettingsSpec {
IvpSolverSettingsSpec {
solver: self.solver.clone(),
solver_options: self.solver_options.clone(),
}
}
pub fn postprocessing_spec(&self) -> PostprocessingSpec {
self.postprocessing.clone()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct IvpProblemSpec {
pub equations: EquationSpec,
pub initial_conditions: InitialConditionSpec,
}
#[derive(Debug, Clone, PartialEq)]
pub struct IvpSolverSettingsSpec {
pub solver: SolverSelectionSpec,
pub solver_options: IvpSolverOptionsSpec,
}
#[derive(Debug)]
pub struct IvpTaskRunResult {
pub specification: IvpTaskSpec,
pub t_result: Option<DVector<f64>>,
pub y_result: Option<DMatrix<f64>>,
pub status: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum IvpTaskError {
Parser(String),
MissingSection(&'static str),
MissingField {
section: String,
field: String,
},
InvalidField {
section: String,
field: String,
message: String,
},
InconsistentEquationCounts {
unknowns: usize,
rhs: usize,
},
UnknownMethod(String),
Semantic(String),
Solver(String),
}
impl std::fmt::Display for IvpTaskError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Parser(msg) => write!(f, "parser error: {msg}"),
Self::MissingSection(section) => write!(f, "missing section `{section}`"),
Self::MissingField { section, field } => {
write!(f, "missing field `{field}` in section `{section}`")
}
Self::InvalidField {
section,
field,
message,
} => write!(
f,
"invalid field `{field}` in section `{section}`: {message}"
),
Self::InconsistentEquationCounts { unknowns, rhs } => write!(
f,
"number of unknowns ({unknowns}) does not match number of rhs expressions ({rhs})"
),
Self::UnknownMethod(method) => write!(f, "unknown IVP method `{method}`"),
Self::Semantic(message) => write!(f, "{message}"),
Self::Solver(message) => write!(f, "{message}"),
}
}
}
impl std::error::Error for IvpTaskError {}
type GenericSectionMap = HashMap<String, Option<Vec<Value>>>;
pub fn parse_ivp_task_from_str(input: &str) -> Result<IvpTaskSpec, IvpTaskError> {
let mut parser = DocumentParser::new(input.to_string());
let pseudonyms = default_ivp_pseudonyms();
parser.with_pseudonims(Some(pseudonyms.0), Some(pseudonyms.1));
parser.parse_document().map_err(IvpTaskError::Parser)?;
parser.keys_to_lower_case(Some(vec![
"equations".to_string(),
"parameters".to_string(),
"where".to_string(),
"substitute".to_string(),
]));
let document = parser
.get_result()
.ok_or_else(|| IvpTaskError::Parser("document parser returned no result".to_string()))?;
parse_ivp_task_from_document(document)
}
pub fn parse_ivp_task_from_file(path: Option<PathBuf>) -> Result<IvpTaskSpec, IvpTaskError> {
let mut parser = DocumentParser::new(String::new());
parser
.setting_from_file(path)
.map_err(IvpTaskError::Parser)?;
parser.parse_document().map_err(IvpTaskError::Parser)?;
parser.keys_to_lower_case(Some(vec![
"equations".to_string(),
"parameters".to_string(),
"where".to_string(),
"substitute".to_string(),
]));
let document = parser
.get_result()
.ok_or_else(|| IvpTaskError::Parser("document parser returned no result".to_string()))?;
parse_ivp_task_from_document(document)
}
pub fn try_parse_ivp_problem_from_document(
document: &DocumentMap,
) -> Result<IvpProblemSpec, IvpTaskError> {
parse_ivp_problem_from_document(document)
}
pub fn try_parse_ivp_solver_settings_from_document(
document: &DocumentMap,
) -> Result<IvpSolverSettingsSpec, IvpTaskError> {
parse_ivp_solver_settings_from_document(document)
}
pub fn try_parse_ivp_task_from_document(
document: &DocumentMap,
) -> Result<IvpTaskSpec, IvpTaskError> {
parse_ivp_task_from_document(document)
}
pub fn try_parse_ivp_task_from_str(input: &str) -> Result<IvpTaskSpec, IvpTaskError> {
parse_ivp_task_from_str(input)
}
pub fn try_parse_ivp_task_from_file(path: Option<PathBuf>) -> Result<IvpTaskSpec, IvpTaskError> {
parse_ivp_task_from_file(path)
}
pub fn parse_ivp_problem_from_document(
document: &DocumentMap,
) -> Result<IvpProblemSpec, IvpTaskError> {
let equations = parse_equations(document)?;
let initial_conditions = parse_initial_conditions(document, equations.unknowns.len())?;
Ok(IvpProblemSpec {
equations,
initial_conditions,
})
}
pub fn parse_ivp_solver_settings_from_document(
document: &DocumentMap,
) -> Result<IvpSolverSettingsSpec, IvpTaskError> {
Ok(IvpSolverSettingsSpec {
solver: parse_solver_selection(document)?,
solver_options: parse_solver_options(document)?,
})
}
pub fn parse_ivp_task_from_document(document: &DocumentMap) -> Result<IvpTaskSpec, IvpTaskError> {
let problem = parse_ivp_problem_from_document(document)?;
let solver_settings = parse_ivp_solver_settings_from_document(document)?;
let postprocessing = parse_postprocessing(document)?;
Ok(IvpTaskSpec {
solver: solver_settings.solver,
equations: problem.equations,
initial_conditions: problem.initial_conditions,
solver_options: solver_settings.solver_options,
postprocessing,
})
}
pub fn build_ivp_solver_from_spec(spec: &IvpTaskSpec) -> Result<UniversalODESolver, IvpTaskError> {
build_ivp_solver_from_problem_and_settings(&spec.problem_spec(), &spec.solver_settings_spec())
}
pub fn build_ivp_solver_from_problem_and_settings(
problem: &IvpProblemSpec,
settings: &IvpSolverSettingsSpec,
) -> Result<UniversalODESolver, IvpTaskError> {
let spec = IvpTaskSpec {
solver: settings.solver.clone(),
equations: problem.equations.clone(),
initial_conditions: problem.initial_conditions.clone(),
solver_options: settings.solver_options.clone(),
postprocessing: PostprocessingSpec::default(),
};
build_ivp_solver_from_spec_impl(&spec)
}
fn build_ivp_solver_from_spec_impl(spec: &IvpTaskSpec) -> Result<UniversalODESolver, IvpTaskError> {
if spec.equations.unknowns.len() != spec.initial_conditions.y0.len() {
return Err(IvpTaskError::Semantic(format!(
"initial condition vector length {} does not match number of unknowns {}",
spec.initial_conditions.y0.len(),
spec.equations.unknowns.len()
)));
}
let mut solver = if matches!(spec.solver.method, IvpMethodSpec::Lsode2) {
let config = build_lsode2_problem_config_from_spec(spec)?;
UniversalODESolver::lsode2_with_problem_config(config)
} else {
UniversalODESolver::new(
spec.equations.rhs.clone(),
spec.equations.unknowns.clone(),
spec.equations.arg.clone(),
spec.solver.method.to_solver_type(),
spec.initial_conditions.t0,
DVector::from_vec(spec.initial_conditions.y0.clone()),
spec.initial_conditions.t_end,
)
};
if let Some(value) = spec.solver_options.step_size {
solver.set_step_size(value);
}
if let Some(value) = spec.solver_options.tolerance {
solver.set_tolerance(value);
}
if let Some(value) = spec.solver_options.max_iterations {
solver.set_max_iterations(value);
}
if let Some(value) = spec.solver_options.rtol {
solver.set_rtol(value);
}
if let Some(value) = spec.solver_options.atol {
solver.set_atol(value);
}
if let Some(value) = spec.solver_options.max_step {
solver.set_max_step(value);
}
solver.set_first_step(spec.solver_options.first_step);
if let Some(value) = spec.solver_options.vectorized {
solver.set_vectorized(value);
}
if let Some(value) = spec.solver_options.parallel {
solver.set_parallel(value);
}
if let Some(value) = spec.solver_options.neighborhood_check {
solver.set_neighborhood_check(value);
}
Ok(solver)
}
fn build_lsode2_problem_config_from_spec(
spec: &IvpTaskSpec,
) -> Result<Lsode2ProblemConfig, IvpTaskError> {
let max_step = spec.solver_options.max_step.unwrap_or(1e-3);
let rtol = spec.solver_options.rtol.unwrap_or(1e-5);
let atol = spec.solver_options.atol.unwrap_or(1e-8);
let mut config = Lsode2ProblemConfig::new(
spec.equations.rhs.clone(),
spec.equations.unknowns.clone(),
spec.equations.arg.clone(),
spec.initial_conditions.t0,
DVector::from_vec(spec.initial_conditions.y0.clone()),
spec.initial_conditions.t_end,
max_step,
rtol,
atol,
)
.with_first_step(spec.solver_options.first_step)
.with_vectorized(spec.solver_options.vectorized.unwrap_or(false))
.with_faithful_bdf_solve(200_000, 200_000);
if !spec.equations.parameter_names.is_empty() {
let mut parameter_values = Vec::with_capacity(spec.equations.parameter_names.len());
for name in &spec.equations.parameter_names {
let value = spec
.equations
.parameter_values
.get(name)
.copied()
.ok_or_else(|| IvpTaskError::MissingField {
section: "equations".to_string(),
field: format!("parameter_values[{name}]"),
})?;
parameter_values.push(value);
}
config = config
.with_equation_parameters(spec.equations.parameter_names.clone())
.with_equation_parameter_values(DVector::from_vec(parameter_values));
}
if let Some(options) = spec.solver_options.lsode2.as_ref() {
if let Some(controller) = options.controller {
config = config.with_controller(controller);
}
let symbolic_assembly = options
.symbolic_assembly
.unwrap_or(Lsode2SymbolicAssemblyBackend::ExprLegacy);
let symbolic_execution = options
.symbolic_execution
.clone()
.unwrap_or(Lsode2TaskExecutionSpec::LambdifyExpr);
match symbolic_execution {
Lsode2TaskExecutionSpec::LambdifyExpr => {
config =
config.with_residual_jacobian_source(Lsode2ResidualJacobianSource::Symbolic {
assembly: symbolic_assembly,
execution: Lsode2SymbolicExecutionMode::LambdifyExpr,
});
}
Lsode2TaskExecutionSpec::Aot {
toolchain,
profile,
output_parent_dir,
} => {
config =
config.with_residual_jacobian_source(Lsode2ResidualJacobianSource::Symbolic {
assembly: symbolic_assembly,
execution: Lsode2SymbolicExecutionMode::Aot { toolchain, profile },
});
if let Some(output_dir) = output_parent_dir {
let mut backend = config.backend.clone();
backend.generated_backend.output_parent_dir = Some(output_dir);
config = config.with_backend(backend);
}
}
}
if let Some(structure) = options.linear_system_structure {
config = config.with_linear_system_structure(structure);
}
if let Some(policy) = options.linear_solver_policy {
config = config.with_linear_solver_policy(policy);
}
if let Some(native_execution) = options.native_execution {
config = config.with_native_execution(native_execution);
}
for condition in &options.stop_conditions {
config = match condition.comparator {
Lsode2StopComparator::GreaterEqual => {
config.with_stop_condition_ge(condition.variable.clone(), condition.target)
}
Lsode2StopComparator::LessEqual => {
config.with_stop_condition_le(condition.variable.clone(), condition.target)
}
Lsode2StopComparator::AbsDistance => config.with_stop_condition_abs(
condition.variable.clone(),
condition.target,
condition.tolerance,
),
};
}
}
let source = config.residual_jacobian_source;
config = config.with_residual_jacobian_source(match source {
Lsode2ResidualJacobianSource::Symbolic {
assembly,
execution,
} => Lsode2ResidualJacobianSource::Symbolic {
assembly,
execution,
},
Lsode2ResidualJacobianSource::Analytical => Lsode2ResidualJacobianSource::Symbolic {
assembly: Lsode2SymbolicAssemblyBackend::ExprLegacy,
execution: Lsode2SymbolicExecutionMode::LambdifyExpr,
},
});
config.backend.jacobian_backend = Lsode2JacobianBackend::SymbolicGenerated;
Ok(config)
}
pub fn run_ivp_task_from_str(input: &str) -> Result<IvpTaskRunResult, IvpTaskError> {
let spec = parse_ivp_task_from_str(input)?;
run_ivp_task(spec)
}
pub fn run_ivp_task(spec: IvpTaskSpec) -> Result<IvpTaskRunResult, IvpTaskError> {
let mut solver = build_ivp_solver_from_spec(&spec)?;
solver
.try_solve()
.map_err(|err| IvpTaskError::Solver(err.to_string()))?;
let (t_result, y_result) = solver.get_result();
let status = solver.get_status();
let plan = spec.postprocessing.to_plan("ivp_result.csv");
if !plan.actions.is_empty() {
let dataset = PostprocessDataset::new(
spec.equations.arg.clone(),
spec.equations.unknowns.clone(),
t_result.clone().ok_or_else(|| {
IvpTaskError::Solver("cannot write CSV because t_result is missing".to_string())
})?,
y_result.clone().ok_or_else(|| {
IvpTaskError::Solver("cannot write CSV because y_result is missing".to_string())
})?,
)
.map_err(|err| IvpTaskError::Solver(err.to_string()))?;
plan.execute(&dataset)
.map_err(|err| IvpTaskError::Solver(err.to_string()))?;
}
Ok(IvpTaskRunResult {
specification: spec,
t_result,
y_result,
status,
})
}
pub fn create_ivp_template_file(path: Option<PathBuf>) {
use std::env;
use std::fs::File;
use std::io::Write;
let template = r#"
task
solver: IVP
method: BDF
equations
arg: t
parameters: a
parameter_values: 1.0
y: -a*y
initial_conditions
t0: 0.0
t_end: 1.0
y0: 1.0
solver_options
rtol: 1e-6
atol: 1e-8
max_step: 0.1
first_step: Some(1e-4)
parallel: false
postprocessing
save_csv: false
csv_path: ivp_result.csv
save_txt: false
txt_path: ivp_result.txt
write_report: false
report_path: ivp_report.md
plotters_png: false
plotters_dir: ivp_plotters
gnuplot_png: false
gnuplot_dir: ivp_gnuplot
terminal_plot: false
plot: false
"#;
let file_path = path.unwrap_or_else(|| {
let mut default_path = env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
default_path.push("ivp_task_template.txt");
default_path
});
match File::create(&file_path) {
Ok(mut file) => {
if let Err(err) = file.write_all(template.as_bytes()) {
eprintln!("Failed to write IVP template file: {err}");
}
}
Err(err) => eprintln!("Failed to create IVP template file: {err}"),
}
}
fn parse_solver_selection(document: &DocumentMap) -> Result<SolverSelectionSpec, IvpTaskError> {
let task_section = get_required_section(document, "task")?;
let solver_name = get_required_string(task_section, "task", "solver")?;
if !solver_name.eq_ignore_ascii_case("ivp") {
return Err(IvpTaskError::InvalidField {
section: "task".to_string(),
field: "solver".to_string(),
message: format!("expected `IVP`, got `{solver_name}`"),
});
}
let method = IvpMethodSpec::from_str(&get_required_string(task_section, "task", "method")?)?;
Ok(SolverSelectionSpec {
task_kind: TaskKindSpec::Ivp,
method,
})
}
fn parse_equations(document: &DocumentMap) -> Result<EquationSpec, IvpTaskError> {
let parsed = parse_symbolic_equation_system(document, "t").map_err(map_equation_error)?;
Ok(EquationSpec {
arg: parsed.arg,
unknowns: parsed.unknowns,
rhs: parsed.rhs,
parameter_names: parsed.parameter_names,
parameter_values: parsed.parameter_values,
})
}
fn map_equation_error(error: SharedEquationParseError) -> IvpTaskError {
match error {
SharedEquationParseError::MissingSection(section) => IvpTaskError::MissingSection(section),
SharedEquationParseError::MissingField { section, field } => {
IvpTaskError::MissingField { section, field }
}
SharedEquationParseError::InvalidField {
section,
field,
message,
} => IvpTaskError::InvalidField {
section,
field,
message,
},
SharedEquationParseError::InconsistentEquationCounts { unknowns, rhs } => {
IvpTaskError::InconsistentEquationCounts { unknowns, rhs }
}
SharedEquationParseError::Semantic(message) => IvpTaskError::Semantic(message),
}
}
fn parse_initial_conditions(
document: &DocumentMap,
expected_dimension: usize,
) -> Result<InitialConditionSpec, IvpTaskError> {
let section = get_required_section(document, "initial_conditions")?;
let t0 = get_required_float(section, "initial_conditions", "t0")?;
let t_end = get_required_float(section, "initial_conditions", "t_end")?;
let y0 = get_required_float_list(section, "initial_conditions", "y0")?;
if y0.len() != expected_dimension {
return Err(IvpTaskError::InvalidField {
section: "initial_conditions".to_string(),
field: "y0".to_string(),
message: format!(
"expected {expected_dimension} initial values, got {}",
y0.len()
),
});
}
Ok(InitialConditionSpec { t0, t_end, y0 })
}
fn parse_solver_options(document: &DocumentMap) -> Result<IvpSolverOptionsSpec, IvpTaskError> {
let section = match document.get("solver_options") {
Some(section) => section,
None => return Ok(IvpSolverOptionsSpec::default()),
};
let lsode2 = parse_lsode2_options(section)?;
Ok(IvpSolverOptionsSpec {
step_size: get_optional_float(section, "step_size")?,
tolerance: get_optional_float(section, "tolerance")?,
max_iterations: get_optional_usize(section, "max_iterations")?,
rtol: get_optional_float(section, "rtol")?,
atol: get_optional_float(section, "atol")?,
max_step: get_optional_float(section, "max_step")?,
first_step: get_optional_float_or_option(section, "first_step")?,
vectorized: get_optional_bool(section, "vectorized")?,
parallel: get_optional_bool(section, "parallel")?,
neighborhood_check: get_optional_float(section, "neighborhood_check")?,
lsode2,
})
}
fn parse_lsode2_options(
section: &GenericSectionMap,
) -> Result<Option<Lsode2TaskOptionsSpec>, IvpTaskError> {
let controller = parse_lsode2_controller(section)?;
let assembly = match get_optional_string(section, "lsode2_symbolic_assembly", "solver_options")?
{
Some(raw) => Some(parse_lsode2_symbolic_assembly(&raw)?),
None => None,
};
let execution = parse_lsode2_execution(section)?;
let linear_structure = parse_lsode2_linear_structure(section)?;
let linear_policy = parse_lsode2_linear_solver_policy(section)?;
let native_execution = parse_lsode2_native_execution(section)?;
let stop_conditions = parse_lsode2_stop_conditions(section)?;
let has_any = assembly.is_some()
|| execution.is_some()
|| controller.is_some()
|| linear_structure.is_some()
|| linear_policy.is_some()
|| native_execution.is_some()
|| !stop_conditions.is_empty();
if !has_any {
return Ok(None);
}
Ok(Some(Lsode2TaskOptionsSpec {
controller,
symbolic_assembly: assembly,
symbolic_execution: execution,
linear_system_structure: linear_structure,
linear_solver_policy: linear_policy,
native_execution,
stop_conditions,
}))
}
fn parse_postprocessing(document: &DocumentMap) -> Result<PostprocessingSpec, IvpTaskError> {
let section = match document.get("postprocessing") {
Some(section) => section,
None => return Ok(PostprocessingSpec::default()),
};
Ok(PostprocessingSpec {
save_csv: get_optional_bool(section, "save_csv")?.unwrap_or(false),
csv_path: get_optional_string(section, "csv_path", "postprocessing")?,
save_txt: get_optional_bool(section, "save_txt")?.unwrap_or(false),
txt_path: get_optional_string(section, "txt_path", "postprocessing")?,
write_report: get_optional_bool(section, "write_report")?.unwrap_or(false),
report_path: get_optional_string(section, "report_path", "postprocessing")?,
plotters_png: get_optional_bool(section, "plotters_png")?.unwrap_or(false),
plotters_dir: get_optional_string(section, "plotters_dir", "postprocessing")?,
gnuplot_png: get_optional_bool(section, "gnuplot_png")?.unwrap_or(false),
gnuplot_dir: get_optional_string(section, "gnuplot_dir", "postprocessing")?,
terminal_plot: get_optional_bool(section, "terminal_plot")?.unwrap_or(false),
plot: get_optional_bool(section, "plot")?.unwrap_or(false),
})
}
fn default_ivp_pseudonyms() -> (HashMap<String, Vec<String>>, HashMap<String, Vec<String>>) {
let headers = HashMap::from([
(
"task".to_string(),
vec!["problem".to_string(), "solver_selection".to_string()],
),
(
"equations".to_string(),
vec!["system".to_string(), "ode_system".to_string()],
),
(
"where".to_string(),
vec!["substitute".to_string(), "aliases".to_string()],
),
(
"initial_conditions".to_string(),
vec!["initial".to_string(), "iv".to_string()],
),
(
"solver_options".to_string(),
vec!["solver_settings".to_string(), "options".to_string()],
),
]);
let fields = HashMap::from([
(
"method".to_string(),
vec!["ivp_method".to_string(), "solver_method".to_string()],
),
(
"parameters".to_string(),
vec!["params".to_string(), "parameter_names".to_string()],
),
(
"parameter_values".to_string(),
vec!["params_values".to_string(), "param_values".to_string()],
),
(
"t_end".to_string(),
vec!["tbound".to_string(), "t_bound".to_string()],
),
]);
(headers, fields)
}
fn parse_lsode2_symbolic_assembly(
raw: &str,
) -> Result<Lsode2SymbolicAssemblyBackend, IvpTaskError> {
match raw.trim().to_ascii_lowercase().as_str() {
"exprlegacy" | "expr_legacy" => Ok(Lsode2SymbolicAssemblyBackend::ExprLegacy),
"atomview" | "atom_view" => Ok(Lsode2SymbolicAssemblyBackend::AtomView),
other => Err(IvpTaskError::InvalidField {
section: "solver_options".to_string(),
field: "lsode2_symbolic_assembly".to_string(),
message: format!(
"unknown LSODE2 symbolic assembly `{other}` (use ExprLegacy or AtomView)"
),
}),
}
}
fn parse_lsode2_controller(
section: &GenericSectionMap,
) -> Result<Option<Lsode2ControllerConfig>, IvpTaskError> {
let raw = match get_optional_string(section, "lsode2_method_family", "solver_options")? {
Some(value) => value,
None => match get_optional_string(section, "lsode2_controller", "solver_options")? {
Some(value) => value,
None => return Ok(None),
},
};
let value = raw.trim().to_ascii_lowercase().replace(['-', ' '], "_");
let controller = match value.as_str() {
"auto" | "automatic" | "automatic_adams_bdf" | "lsoda" | "adams_bdf" => {
Lsode2ControllerConfig::automatic_adams_bdf()
}
"adams" | "adams_only" | "lsode_adams" => Lsode2ControllerConfig::adams_only(),
"bdf" | "bdf_only" | "lsode_bdf" | "lsode" => Lsode2ControllerConfig::bdf_only(),
other => {
return Err(IvpTaskError::InvalidField {
section: "solver_options".to_string(),
field: "lsode2_method_family".to_string(),
message: format!("unknown LSODE2 method family `{other}` (use auto, adams or bdf)"),
});
}
};
Ok(Some(controller))
}
fn parse_lsode2_execution(
section: &GenericSectionMap,
) -> Result<Option<Lsode2TaskExecutionSpec>, IvpTaskError> {
let raw = match get_optional_string(section, "lsode2_symbolic_execution", "solver_options")? {
Some(value) => value,
None => return Ok(None),
};
let key = raw.trim().to_ascii_lowercase();
if matches!(key.as_str(), "lambdify" | "lambdifyexpr" | "lambdify_expr") {
return Ok(Some(Lsode2TaskExecutionSpec::LambdifyExpr));
}
if !matches!(key.as_str(), "aot") {
return Err(IvpTaskError::InvalidField {
section: "solver_options".to_string(),
field: "lsode2_symbolic_execution".to_string(),
message: format!("unknown LSODE2 symbolic execution `{raw}` (use LambdifyExpr or AOT)"),
});
}
let toolchain_raw = get_optional_string(section, "lsode2_aot_toolchain", "solver_options")?
.unwrap_or_else(|| "c_gcc".to_string());
let profile_raw = get_optional_string(section, "lsode2_aot_profile", "solver_options")?
.unwrap_or_else(|| "release".to_string());
let output_parent_dir =
get_optional_string(section, "lsode2_aot_output_dir", "solver_options")?.map(PathBuf::from);
let toolchain = match toolchain_raw.trim().to_ascii_lowercase().as_str() {
"c_tcc" | "ctcc" => Lsode2AotToolchain::CTcc,
"c_gcc" | "cgcc" | "gcc" => Lsode2AotToolchain::CGcc,
"zig" => Lsode2AotToolchain::Zig,
"rust" => Lsode2AotToolchain::Rust,
other => {
return Err(IvpTaskError::InvalidField {
section: "solver_options".to_string(),
field: "lsode2_aot_toolchain".to_string(),
message: format!("unknown LSODE2 AOT toolchain `{other}`"),
});
}
};
let profile = match profile_raw.trim().to_ascii_lowercase().as_str() {
"debug" => Lsode2AotProfile::Debug,
"release" => Lsode2AotProfile::Release,
other => {
return Err(IvpTaskError::InvalidField {
section: "solver_options".to_string(),
field: "lsode2_aot_profile".to_string(),
message: format!("unknown LSODE2 AOT profile `{other}`"),
});
}
};
Ok(Some(Lsode2TaskExecutionSpec::Aot {
toolchain,
profile,
output_parent_dir,
}))
}
fn parse_lsode2_linear_structure(
section: &GenericSectionMap,
) -> Result<Option<Lsode2LinearSystemStructure>, IvpTaskError> {
let raw = match get_optional_string(section, "lsode2_linear_structure", "solver_options")? {
Some(value) => value,
None => return Ok(None),
};
let value = raw.trim().to_ascii_lowercase();
match value.as_str() {
"dense" => Ok(Some(Lsode2LinearSystemStructure::Dense)),
"sparse" => Ok(Some(Lsode2LinearSystemStructure::Sparse)),
"banded" => {
let kl = get_optional_usize(section, "lsode2_banded_kl")?.unwrap_or(0);
let ku = get_optional_usize(section, "lsode2_banded_ku")?.unwrap_or(0);
Ok(Some(Lsode2LinearSystemStructure::Banded { kl, ku }))
}
other => Err(IvpTaskError::InvalidField {
section: "solver_options".to_string(),
field: "lsode2_linear_structure".to_string(),
message: format!(
"unknown LSODE2 linear structure `{other}` (use dense, sparse or banded)"
),
}),
}
}
fn parse_lsode2_linear_solver_policy(
section: &GenericSectionMap,
) -> Result<Option<Lsode2LinearSolverPolicy>, IvpTaskError> {
let raw = match get_optional_string(section, "lsode2_linear_solver_policy", "solver_options")? {
Some(value) => value,
None => return Ok(None),
};
let value = raw.trim().to_ascii_lowercase();
let policy = match value.as_str() {
"auto" => Lsode2LinearSolverPolicy::Auto,
"dense_lu" | "denselu" => {
Lsode2LinearSolverPolicy::Force(Lsode2LinearSolverChoice::DenseLu)
}
"faer_sparse_lu" | "faersparselu" => {
Lsode2LinearSolverPolicy::Force(Lsode2LinearSolverChoice::FaerSparseLu)
}
"lapack_faithful_banded_lu" | "lapackfaithfulbandedlu" => {
Lsode2LinearSolverPolicy::Force(Lsode2LinearSolverChoice::LapackFaithfulBandedLu)
}
other => {
return Err(IvpTaskError::InvalidField {
section: "solver_options".to_string(),
field: "lsode2_linear_solver_policy".to_string(),
message: format!("unknown LSODE2 linear solver policy `{other}`"),
});
}
};
Ok(Some(policy))
}
fn parse_lsode2_native_execution(
section: &GenericSectionMap,
) -> Result<Option<Lsode2NativeExecutionConfig>, IvpTaskError> {
let raw = match get_optional_string(section, "lsode2_native_execution", "solver_options")? {
Some(value) => value,
None => return Ok(None),
};
let max_step_attempts =
get_optional_usize(section, "lsode2_native_max_step_attempts")?.unwrap_or(200_000);
let max_accepted_steps =
get_optional_usize(section, "lsode2_native_max_accepted_steps")?.unwrap_or(200_000);
let value = raw.trim().to_ascii_lowercase();
let mode = match value.as_str() {
"faithful_bdf_solve" | "native_solve" => {
Lsode2NativeExecutionConfig::faithful_bdf_solve(max_step_attempts, max_accepted_steps)
}
"probe_before_bridge" | "native_probe_before_bridge" => {
Lsode2NativeExecutionConfig::probe_before_bridge(max_step_attempts, max_accepted_steps)
}
"bridge_solve" => Lsode2NativeExecutionConfig::bridge_solve(),
other => {
return Err(IvpTaskError::InvalidField {
section: "solver_options".to_string(),
field: "lsode2_native_execution".to_string(),
message: format!("unknown LSODE2 native execution mode `{other}`"),
});
}
};
Ok(Some(mode))
}
fn parse_lsode2_stop_conditions(
section: &GenericSectionMap,
) -> Result<Vec<Lsode2StopCondition>, IvpTaskError> {
const FIELDS: [&str; 4] = [
"lsode2_stop_variable",
"lsode2_stop_comparator",
"lsode2_stop_target",
"lsode2_stop_tolerance",
];
let variable = get_optional_string(section, "lsode2_stop_variable", "solver_options")?;
let Some(variable) = variable else {
if FIELDS
.iter()
.skip(1)
.any(|field| section.contains_key(*field))
{
return Err(IvpTaskError::MissingField {
section: "solver_options".to_string(),
field: "lsode2_stop_variable".to_string(),
});
}
return Ok(Vec::new());
};
let target = get_optional_float(section, "lsode2_stop_target")?.ok_or_else(|| {
IvpTaskError::MissingField {
section: "solver_options".to_string(),
field: "lsode2_stop_target".to_string(),
}
})?;
let comparator_raw = get_optional_string(section, "lsode2_stop_comparator", "solver_options")?
.unwrap_or_else(|| "ge".to_string());
let comparator = match comparator_raw.trim().to_ascii_lowercase().as_str() {
"ge" | ">=" | "greater_equal" | "greater_or_equal" => Lsode2StopComparator::GreaterEqual,
"le" | "<=" | "less_equal" | "less_or_equal" => Lsode2StopComparator::LessEqual,
"abs" | "abs_distance" | "distance" => Lsode2StopComparator::AbsDistance,
other => {
return Err(IvpTaskError::InvalidField {
section: "solver_options".to_string(),
field: "lsode2_stop_comparator".to_string(),
message: format!(
"unknown LSODE2 stop comparator `{other}` (use ge, le, or abs_distance)"
),
});
}
};
let tolerance = get_optional_float(section, "lsode2_stop_tolerance")?.unwrap_or(0.0);
Ok(vec![Lsode2StopCondition {
variable,
target,
comparator,
tolerance: tolerance.abs(),
}])
}
fn get_required_section<'a>(
document: &'a DocumentMap,
section: &'static str,
) -> Result<&'a GenericSectionMap, IvpTaskError> {
document
.get(section)
.ok_or(IvpTaskError::MissingSection(section))
}
fn get_required_values<'a>(
section: &'a GenericSectionMap,
section_name: &str,
field: &str,
) -> Result<&'a Vec<Value>, IvpTaskError> {
section
.get(field)
.ok_or_else(|| IvpTaskError::MissingField {
section: section_name.to_string(),
field: field.to_string(),
})?
.as_ref()
.ok_or_else(|| IvpTaskError::MissingField {
section: section_name.to_string(),
field: field.to_string(),
})
}
fn get_required_string(
section: &GenericSectionMap,
section_name: &str,
field: &str,
) -> Result<String, IvpTaskError> {
let values = get_required_values(section, section_name, field)?;
if values.len() != 1 {
return Err(IvpTaskError::InvalidField {
section: section_name.to_string(),
field: field.to_string(),
message: "expected a single string value".to_string(),
});
}
value_to_string(&values[0], section_name, field)
}
fn get_optional_string(
section: &GenericSectionMap,
field: &str,
section_name: &str,
) -> Result<Option<String>, IvpTaskError> {
match section.get(field) {
Some(Some(values)) if !values.is_empty() => {
if values.len() != 1 {
return Err(IvpTaskError::InvalidField {
section: section_name.to_string(),
field: field.to_string(),
message: "expected a single string value".to_string(),
});
}
Ok(Some(value_to_string(&values[0], section_name, field)?))
}
_ => Ok(None),
}
}
fn get_required_float(
section: &GenericSectionMap,
section_name: &str,
field: &str,
) -> Result<f64, IvpTaskError> {
let values = get_required_values(section, section_name, field)?;
if values.len() != 1 {
return Err(IvpTaskError::InvalidField {
section: section_name.to_string(),
field: field.to_string(),
message: "expected a single numeric value".to_string(),
});
}
value_to_float(&values[0], section_name, field)
}
fn get_optional_float(
section: &GenericSectionMap,
field: &str,
) -> Result<Option<f64>, IvpTaskError> {
match section.get(field) {
Some(Some(values)) if !values.is_empty() => {
if values.len() != 1 {
return Err(IvpTaskError::InvalidField {
section: "solver_options".to_string(),
field: field.to_string(),
message: "expected a single numeric value".to_string(),
});
}
Ok(Some(value_to_float(&values[0], "solver_options", field)?))
}
_ => Ok(None),
}
}
fn get_optional_float_or_option(
section: &GenericSectionMap,
field: &str,
) -> Result<Option<f64>, IvpTaskError> {
match section.get(field) {
Some(Some(values)) if !values.is_empty() => {
if values.len() != 1 {
return Err(IvpTaskError::InvalidField {
section: "solver_options".to_string(),
field: field.to_string(),
message: "expected a single numeric or optional numeric value".to_string(),
});
}
match &values[0] {
Value::Optional(None) => Ok(None),
Value::Optional(Some(_)) => Ok(values[0].as_option_float()),
_ => Ok(Some(value_to_float(&values[0], "solver_options", field)?)),
}
}
_ => Ok(None),
}
}
fn get_optional_usize(
section: &GenericSectionMap,
field: &str,
) -> Result<Option<usize>, IvpTaskError> {
match section.get(field) {
Some(Some(values)) if !values.is_empty() => {
if values.len() != 1 {
return Err(IvpTaskError::InvalidField {
section: "solver_options".to_string(),
field: field.to_string(),
message: "expected a single integer value".to_string(),
});
}
values[0]
.as_usize()
.ok_or_else(|| IvpTaskError::InvalidField {
section: "solver_options".to_string(),
field: field.to_string(),
message: "expected usize".to_string(),
})
.map(Some)
}
_ => Ok(None),
}
}
fn get_optional_bool(
section: &GenericSectionMap,
field: &str,
) -> Result<Option<bool>, IvpTaskError> {
match section.get(field) {
Some(Some(values)) if !values.is_empty() => {
if values.len() != 1 {
return Err(IvpTaskError::InvalidField {
section: "solver_options".to_string(),
field: field.to_string(),
message: "expected a single boolean value".to_string(),
});
}
values[0]
.as_boolean()
.ok_or_else(|| IvpTaskError::InvalidField {
section: "solver_options".to_string(),
field: field.to_string(),
message: "expected bool".to_string(),
})
.map(Some)
}
_ => Ok(None),
}
}
fn get_required_float_list(
section: &GenericSectionMap,
section_name: &str,
field: &str,
) -> Result<Vec<f64>, IvpTaskError> {
let values = get_required_values(section, section_name, field)?;
values_to_float_list(values, section_name, field)
}
fn values_to_float_list(
values: &[Value],
section_name: &str,
field: &str,
) -> Result<Vec<f64>, IvpTaskError> {
if values.len() == 1 {
if let Some(vector) = values[0].as_vector() {
return Ok(vector.clone());
}
}
values
.iter()
.map(|value| value_to_float(value, section_name, field))
.collect()
}
fn value_to_string(value: &Value, section_name: &str, field: &str) -> Result<String, IvpTaskError> {
if let Some(text) = value.as_string() {
Ok(text.clone())
} else {
Err(IvpTaskError::InvalidField {
section: section_name.to_string(),
field: field.to_string(),
message: "expected string".to_string(),
})
}
}
fn value_to_float(value: &Value, section_name: &str, field: &str) -> Result<f64, IvpTaskError> {
if let Some(number) = value.as_float() {
Ok(number)
} else if let Some(integer) = value.as_usize() {
Ok(integer as f64)
} else {
Err(IvpTaskError::InvalidField {
section: section_name.to_string(),
field: field.to_string(),
message: "expected numeric value".to_string(),
})
}
}
#[cfg(test)]
#[path = "task_parser_ivp_tests.rs"]
mod task_parser_ivp_tests;