use super::*;
use crate::command_interpreter::task_parser::ParseErrorKind;
use std::error::Error;
use std::fs;
use tempfile::tempdir;
#[test]
fn bvp_adapter_keeps_typed_document_errors() {
let error =
parse_bvp_task_from_str("task\nsolver BVP").expect_err("malformed task header must fail");
match error {
BvpTaskError::Document(error) => assert_eq!(error.kind, ParseErrorKind::InvalidSection),
other => panic!("expected typed document error, got {other:?}"),
}
}
#[test]
fn bvp_document_semantic_errors_keep_source_location() {
let input = r#"
task
solver: BVP
method: Dense
equations
arg: x
unknowns: y
rhs: -y
boundary_conditions
z_left: 0.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 4
initial_guess
y: 0.0
"#;
let error = parse_bvp_task_from_str(input)
.expect_err("an undeclared boundary state must be rejected");
match error {
BvpTaskError::SymbolDiagnostic {
message,
token,
line,
column,
} => {
assert!(message.contains("z_left"));
assert_eq!(token, "z_left");
assert!(line.is_some());
assert!(column.is_some());
}
other => panic!("expected a located BVP diagnostic, got {other:?}"),
}
}
fn parse_document_for_bvp(input: &str) -> DocumentMap {
let mut parser = DocumentParser::new(input.to_string());
let pseudonyms = default_bvp_pseudonyms();
parser.with_pseudonims(Some(pseudonyms.0), Some(pseudonyms.1));
parser.parse_document().expect("BVP document should parse");
parser.keys_to_lower_case(Some(vec![
"equations".to_string(),
"parameters".to_string(),
"boundary_conditions".to_string(),
"initial_guess".to_string(),
"where".to_string(),
"substitute".to_string(),
]));
parser
.get_result()
.expect("BVP document map should exist")
.clone()
}
#[test]
fn bvp_task_parser_supports_file_roundtrip() {
let dir = tempdir().expect("temp dir should be created");
let path = dir.path().join("bvp_task.txt");
fs::write(
&path,
r#"
task
solver: BVP
strategy: Damped
scheme: forward
method: Sparse
equations
arg: x
unknowns: y
rhs: -y
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 20
initial_guess
y: 0.0
"#,
)
.expect("test task file should be written");
let spec = parse_bvp_task_from_file(Some(path)).expect("BVP file task should parse");
assert_eq!(spec.solver.strategy, BvpStrategySpec::Damped);
assert_eq!(spec.equations.unknowns, vec!["y".to_string()]);
assert_eq!(spec.mesh.n_steps, 20);
}
#[test]
fn bvp_task_parser_rejects_invalid_mesh_before_solver_construction() {
let input = r#"
task
solver: BVP
strategy: Damped
equations
arg: x
unknowns: y
rhs: -y
boundary_conditions
y_left: 1.0
mesh
t0: 1.0
t_end: 1.0
n_steps: 1
initial_guess
y: 0.0
"#;
let error = parse_bvp_task_from_str(input).expect_err("invalid mesh must be rejected");
match error {
BvpTaskError::InvalidConfiguration { field, message } => {
assert_eq!(field, "mesh.t_end");
assert!(message.contains("greater than"));
}
other => panic!("expected typed mesh configuration error, got {other:?}"),
}
}
#[test]
fn bvp_task_parser_rejects_invalid_tolerance_before_solver_construction() {
let input = r#"
task
solver: BVP
strategy: Damped
equations
arg: x
unknowns: y
rhs: -y
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 0.0
solver_options
tolerance: 0.0
"#;
let error = parse_bvp_task_from_str(input).expect_err("invalid tolerance must be rejected");
match error {
BvpTaskError::InvalidConfiguration { field, .. } => {
assert_eq!(field, "solver_options.tolerance");
}
other => panic!("expected typed tolerance configuration error, got {other:?}"),
}
}
#[test]
fn bvp_task_file_errors_keep_io_source_and_category() {
let path = tempdir()
.expect("temp dir should be created")
.path()
.join("missing-bvp-task.txt");
let error = parse_bvp_task_from_file(Some(path)).expect_err("missing task must fail");
assert_eq!(error.category(), "io");
assert!(error.source().is_some());
assert!(matches!(error, BvpTaskError::Io { .. }));
}
#[test]
fn bvp_sci_task_route_executes_through_the_new_lambdify_api() {
let input = r#"
task
solver: BVP_sci
frontend: ExprLegacy
execution: Lambdify
method: Dense
equations
arg: x
unknowns: y
rhs: -y
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 0.0
solver_options
tolerance: 1e-2
max_iterations: 20
"#;
let run = run_bvp_task_from_str(input).expect("BVP_sci task should execute");
assert_eq!(run.specification.solver.family, BvpSolverFamilySpec::Sci);
assert_eq!(
run.specification.solver.frontend,
Some(BvpFrontendSpec::ExprLegacy)
);
assert_eq!(
run.specification.solver.execution,
Some(BvpExecutionSpec::Lambdify)
);
assert_eq!(run.continuation_segments.len(), 1);
let result = run.result.expect("BVP_sci should publish a matrix result");
assert_eq!(result.nrows(), 1);
assert_eq!(result.ncols(), 8);
assert!(result.iter().all(|value| value.is_finite()));
let aot_input = input.replace("execution: Lambdify", "execution: AOT");
let error = run_bvp_task_from_str(&aot_input).expect_err("AOT without output dir must fail");
match error {
BvpTaskError::BvpSci(error) => {
assert!(error.to_string().contains("output directory"));
}
other => panic!("expected typed BVP AOT preparation error, got {other:?}"),
}
let invalid_frontend = input.replace("frontend: ExprLegacy", "frontend: Lambdify");
let error = parse_bvp_task_from_str(&invalid_frontend)
.expect_err("execution route must not be accepted as a frontend");
match error {
BvpTaskError::UnsupportedRoute(message) => {
assert!(message.contains("unsupported BVP frontend"));
}
other => panic!("expected typed frontend error, got {other:?}"),
}
}
#[test]
fn bvp_sci_task_route_executes_through_atom_native_frontend() {
let input = r#"
task
solver: BVP_sci
frontend: AtomViewNative
method: Dense
equations
arg: x
unknowns: y
rhs: -y
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 0.0
solver_options
tolerance: 1e-2
max_iterations: 20
"#;
let run = run_bvp_task_from_str(input).expect("BVP_sci AtomView task should execute");
assert_eq!(run.specification.solver.family, BvpSolverFamilySpec::Sci);
assert_eq!(
run.specification.solver.frontend,
Some(BvpFrontendSpec::AtomViewNative)
);
assert_eq!(run.specification.solver.execution, None);
assert_eq!(run.continuation_segments.len(), 1);
let result = run.result.expect("BVP_sci should publish a matrix result");
assert_eq!(result.nrows(), 1);
assert_eq!(result.ncols(), 8);
assert!(result.iter().all(|value| value.is_finite()));
}
#[test]
fn bvp_sci_task_runner_uses_typed_postprocessing_dataset() {
let dir = tempdir().expect("temp dir should be created");
let report_path = dir.path().join("bvp_sci_report.md");
let report_path = report_path.to_string_lossy().replace('\\', "/");
let input = format!(
r#"
task
solver: BVP_sci
frontend: ExprLegacy
execution: Lambdify
method: Dense
equations
arg: x
unknowns: y
rhs: -y
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 0.0
solver_options
tolerance: 1e-2
max_iterations: 20
postprocessing
write_report: true
report_path: {report_path}
"#
);
let run = run_bvp_task_from_str(&input).expect("BVP_sci postprocessing should execute");
assert!(run.result.is_some());
let report = fs::read_to_string(&report_path).expect("BVP_sci report should be written");
assert!(report.contains("axis: x"));
assert!(report.contains("variables: y"));
}
#[test]
fn bvp_sci_task_runner_reuses_prepared_model_for_warm_continuation() {
let input = r#"
task
solver: BVP_sci
frontend: AtomViewNative
execution: Lambdify
method: Dense
equations
arg: x
unknowns: y
rhs: -p*y
parameters: p
parameter_values: 0.5
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 1.0
solver_options
tolerance: 1e-2
max_iterations: 20
continuation
parameter: p
values: 0.5, 1.0
mode: warm
"#;
let run = run_bvp_task_from_str(input).expect("warm BVP_sci continuation should execute");
assert_eq!(run.continuation_segments.len(), 2);
assert!(run
.continuation_segments
.iter()
.all(|segment| segment.result.iter().all(|value| value.is_finite())));
}
#[test]
fn bvp_damp_fresh_continuation_executes_every_segment() {
let input = r#"
task
solver: BVP
strategy: Damped
method: Dense
equations
arg: x
unknowns: y
rhs: -rate*y
parameters: rate
parameter_values: 1.0
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 0.0
continuation
parameter: rate
values: 0.5, 1.0
mode: fresh
"#;
let run = run_bvp_task_from_str(input).expect("BVP_Damp continuation should execute");
assert_eq!(run.continuation_segments.len(), 2);
assert!(run
.continuation_segments
.iter()
.all(|segment| segment.result.iter().all(|value| value.is_finite())));
}
#[test]
fn bvp_damp_warm_continuation_reuses_prepared_callbacks() {
let input = r#"
task
solver: BVP
strategy: Damped
method: Dense
equations
arg: x
unknowns: y
rhs: -rate*y
parameters: rate
parameter_values: 1.0
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 0.0
continuation
parameter: rate
values: 0.5, 1.0
mode: warm
"#;
let run = run_bvp_task_from_str(input).expect("BVP_Damp warm continuation should execute");
assert_eq!(run.continuation_segments.len(), 2);
assert_eq!(run.continuation_telemetry.fresh_preparations, 1);
assert_eq!(run.continuation_telemetry.prepared_reuses, 1);
assert!(run
.continuation_segments
.iter()
.all(|segment| segment.result.iter().all(|value| value.is_finite())));
}
#[test]
fn bvp_damp_long_prepared_series_keeps_logical_workspace_bounded() {
let values = (0..16)
.map(|index| format!("{:.3}", 0.5 + index as f64 * 0.1))
.collect::<Vec<_>>()
.join(", ");
let input = format!(
r#"
task
solver: BVP
strategy: Damped
method: Dense
equations
arg: x
unknowns: y
rhs: -rate*y
parameters: rate
parameter_values: 0.5
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 0.0
continuation
parameter: rate
values: {values}
mode: prepared
restart_each: false
"#
);
let run = run_bvp_task_from_str(&input).expect("long BVP_Damp continuation should execute");
let telemetry = &run.continuation_telemetry;
assert_eq!(telemetry.segments, 16);
assert_eq!(telemetry.fresh_preparations, 1);
assert_eq!(telemetry.prepared_reuses, 15);
assert_eq!(telemetry.mesh_restarts, 0);
assert_eq!(telemetry.initial_guess_restarts, 15);
let result = run
.result
.expect("long continuation should publish a result");
assert!(result.iter().all(|value| value.is_finite()));
assert_eq!(telemetry.peak_result_elements, result.len());
assert!(run
.continuation_segments
.iter()
.all(|segment| segment.result.len() == telemetry.peak_result_elements));
}
#[test]
fn bvp_task_parser_accepts_typed_output_policy() {
let input = r#"
task
solver: BVP_sci
method: Dense
equations
arg: x
unknowns: y
rhs: -y
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 4
initial_guess
y: 1.0
postprocessing
output_policy: terminal
"#;
let spec = parse_bvp_task_from_str(input).expect("typed output policy should parse");
assert_eq!(spec.postprocessing.output_policy, BvpOutputPolicy::Terminal);
}
#[test]
fn bvp_task_parser_preserves_continuation_template() {
let input = r#"
task
solver: BVP
strategy: Damped
method: Dense
equations
arg: x
unknowns: y
rhs: -rate*y
parameters: rate
parameter_values: 1.0
boundary_conditions
y_left: 1.0
y_right: 0.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 0.0
continuation
parameter: rate
values: 0.5, 1.0
mode: warm
restart_each: true
"#;
let spec = parse_bvp_task_from_str(input).expect("BVP continuation task should parse");
let continuation = spec
.continuation
.expect("BVP continuation should be present");
assert_eq!(continuation.parameter, "rate");
assert_eq!(continuation.values, vec![0.5, 1.0]);
assert_eq!(continuation.mode, ContinuationMode::Warm);
assert!(continuation.restart_each);
assert!(continuation.symbolic_rhs[0].to_string().contains("rate"));
}
#[test]
fn bvp_task_parser_supports_pair_style_equations() {
let input = r#"
task
solver: BVP
strategy: Damped
scheme: forward
method: Sparse
equations
arg: x
y: -2.0*y
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 20
initial_guess
y: 0.0
"#;
let spec = parse_bvp_task_from_str(input).expect("pair-style BVP task should parse");
assert_eq!(spec.equations.unknowns, vec!["y".to_string()]);
assert_eq!(spec.mesh.n_steps, 20);
assert_eq!(
spec.boundary_conditions.conditions["y"],
vec![(0usize, 1.0f64)]
);
}
#[test]
fn bvp_task_parser_preserves_readable_parameter_section_symbol_case() {
let input = r#"
task
solver: BVP
strategy: Damped
scheme: forward
method: Sparse
equations
arg: x
unknowns: T
rhs: -R*T
parameters
R: 2.0
boundary_conditions
T_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 20
initial_guess
T: 1.0
"#;
let spec = parse_bvp_task_from_str(input)
.expect("BVP task with a readable parameter section should parse");
assert_eq!(spec.equations.parameter_names, vec!["R"]);
assert_eq!(spec.equations.parameter_values["R"], 2.0);
let rhs = spec.equations.rhs[0].lambdify_borrowed_thread_safe(&["x", "T"]);
assert!((rhs(&[0.0, 3.0]) + 6.0).abs() < 1e-12);
}
#[test]
fn bvp_task_parser_can_split_problem_and_solver_settings() {
let input = r#"
task
solver: BVP
strategy: Damped
scheme: forward
method: Banded
equations
arg: x
unknowns: y
rhs: -y
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 12
initial_guess
y: 0.0
solver_options
tolerance: 1e-6
max_iterations: 24
generated_backend: banded_aot_tcc
aot_build_policy: build_if_missing
aot_build_profile: debug
aot_execution_policy: sequential
banded_linear_solver: faithful
refinement_steps: 2
"#;
let document = parse_document_for_bvp(input);
let problem = parse_bvp_problem_from_document(&document)
.expect("problem section should be parsed independently");
let settings = parse_bvp_solver_settings_from_document(&document)
.expect("solver settings should be parsed independently");
assert_eq!(problem.equations.unknowns, vec!["y".to_string()]);
assert_eq!(problem.mesh.n_steps, 12);
assert_eq!(settings.solver.strategy, BvpStrategySpec::Damped);
assert_eq!(settings.solver.backend, BvpLinearBackendSpec::Banded);
assert_eq!(settings.solver_options.tolerance, Some(1e-6));
assert_eq!(settings.solver_options.max_iterations, Some(24));
assert_eq!(
settings.solver_options.generated_backend.preset.as_deref(),
Some("banded_aot_tcc")
);
let solver = build_bvp_solver_from_problem_and_settings(&problem, &settings)
.expect("split problem/settings should build a solver");
drop(solver);
}
#[test]
fn bvp_task_parser_reads_solver_settings_without_problem_sections() {
let input = r#"
task
solver: BVP
strategy: Frozen
scheme: forward
method: Sparse
solver_options
generated_backend: sparse_lambdify
tolerance: 1e-4
max_iterations: 10
"#;
let document = parse_document_for_bvp(input);
let settings = parse_bvp_solver_settings_from_document(&document)
.expect("solver-only settings should parse without equations");
assert_eq!(settings.solver.strategy, BvpStrategySpec::Frozen);
assert_eq!(settings.solver.backend, BvpLinearBackendSpec::Sparse);
assert_eq!(
settings.solver_options.generated_backend.preset.as_deref(),
Some("sparse_lambdify")
);
assert_eq!(settings.solver_options.tolerance, Some(1e-4));
assert_eq!(settings.solver_options.max_iterations, Some(10));
}
#[test]
fn bvp_task_parser_supports_full_end_to_end_document_with_aot_settings() {
let input = r#"
task
solver: BVP
strategy: Damped
scheme: forward
method: Banded
equations
arg: x
unknowns: z, y
rhs: y-z, -z^3
boundary_conditions
z_left: 1.0
y_right: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 20
initial_guess
guess: 0.0, 0.0
solver_options
tolerance: 1e-5
max_iterations: 20
generated_backend: banded_aot_tcc
aot_build_policy: build_if_missing
aot_build_profile: debug
aot_execution_policy: sequential
banded_linear_solver: faithful
refinement_steps: 1
postprocessing
save_csv: false
save_txt: false
write_report: false
plot: false
"#;
let spec = parse_bvp_task_from_str(input)
.expect("full end-to-end BVP document with AOT settings should parse");
assert_eq!(spec.solver.backend, BvpLinearBackendSpec::Banded);
assert_eq!(
spec.equations.unknowns,
vec!["z".to_string(), "y".to_string()]
);
assert_eq!(spec.mesh.n_steps, 20);
assert_eq!(
spec.solver_options.generated_backend.preset.as_deref(),
Some("banded_aot_tcc")
);
let solver = build_bvp_solver_from_spec(&spec)
.expect("full end-to-end BVP document with AOT settings should build");
drop(solver);
}
#[test]
fn bvp_task_parser_can_build_solver_from_rust_problem_and_task_doc_settings() {
let problem = BvpProblemSpec {
equations: BvpEquationSpec {
arg: "x".to_string(),
unknowns: vec!["y".to_string()],
rhs: vec![Expr::parse_expression("-y")],
parameter_names: vec![],
parameter_values: HashMap::new(),
},
boundary_conditions: BoundaryConditionSpec {
conditions: HashMap::from([("y".to_string(), vec![(0usize, 1.0f64)])]),
},
mesh: BvpMeshSpec {
t0: 0.0,
t_end: 1.0,
n_steps: 16,
},
initial_guess: BvpInitialGuessSpec { values: vec![0.0] },
};
let settings_doc = r#"
task
solver: BVP
strategy: Frozen
scheme: forward
method: Sparse
solver_options
generated_backend: sparse_aot_tcc
aot_build_policy: require_prebuilt
aot_build_profile: release
aot_execution_policy: sequential
tolerance: 1e-4
max_iterations: 12
"#;
let document = parse_document_for_bvp(settings_doc);
let settings = parse_bvp_solver_settings_from_document(&document)
.expect("solver settings should parse independently of problem sections");
assert_eq!(settings.solver.strategy, BvpStrategySpec::Frozen);
assert_eq!(settings.solver.backend, BvpLinearBackendSpec::Sparse);
assert_eq!(
settings.solver_options.generated_backend.preset.as_deref(),
Some("sparse_aot_tcc")
);
let solver = build_bvp_solver_from_problem_and_settings(&problem, &settings)
.expect("Rust problem plus task-doc settings should build");
drop(solver);
}
#[test]
fn bvp_task_parser_supports_where_symbolic_substitutions() {
let input = r#"
task
solver: BVP
strategy: Damped
scheme: forward
method: Sparse
equations
arg: x
y: flux - y
where
flux: 3.0*x
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 0.0
"#;
let spec = parse_bvp_task_from_str(input).expect("BVP with where substitutions should parse");
let expr = spec.equations.rhs[0].clone();
let f = expr.lambdify_borrowed_thread_safe(&["x", "y"]);
let value = f(&[2.0, 1.0]);
assert!((value - 5.0).abs() < 1e-12);
}
#[test]
fn bvp_task_parser_resolves_multilevel_where_with_numeric_parameters() {
let input = r#"
task
solver: BVP
strategy: Damped
scheme: forward
method: Sparse
equations
arg: x
parameters: gain
parameter_values: 2.0
y: source - y
where
base: gain * x
shifted: base + 1.0
source: 2.0 * shifted
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 0.0
"#;
let spec = parse_bvp_task_from_str(input)
.expect("BVP parser should resolve a multilevel symbolic definition chain");
let rhs = spec.equations.rhs[0].lambdify_borrowed_thread_safe(&["x", "y"]);
assert!((rhs(&[3.0, 1.0]) - 13.0).abs() < 1e-12);
}
#[test]
fn bvp_task_parser_accepts_params_alias_and_where_substitution() {
let input = r#"
task
solver: BVP
strategy: Damped
scheme: forward
method: Sparse
equations
arg: x
params: a
parameter_values: 2.0
y: heat - a*y
where
heat: 1.0 + x
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 0.0
"#;
let spec = parse_bvp_task_from_str(input).expect("BVP should accept IVP-style params alias");
assert_eq!(spec.equations.parameter_names, vec!["a".to_string()]);
assert_eq!(spec.equations.parameter_values["a"], 2.0);
let expr = spec.equations.rhs[0].clone();
let f = expr.lambdify_borrowed_thread_safe(&["x", "y"]);
let value = f(&[0.5, 1.0]);
assert!((value + 0.5).abs() < 1e-12);
}
#[test]
fn bvp_task_parser_reports_bad_where_expression() {
let input = r#"
task
solver: BVP
strategy: Damped
scheme: forward
method: Sparse
equations
arg: x
y: heat - y
where
heat: sin(
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 0.0
"#;
let err = parse_bvp_task_from_str(input)
.expect_err("bad where expression should be reported as a BVP parser error");
let message = err.to_string();
assert!(message.contains("where/substitute"));
assert!(message.contains("failed to parse symbolic expression"));
}
#[test]
fn bvp_task_parser_maps_banded_lambdify_generated_backend_config() {
let input = r#"
task
solver: BVP
strategy: Damped
scheme: forward
method: Banded
equations
arg: x
unknowns: y
rhs: -y
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 0.0
solver_options
generated_backend: banded_lambdify
banded_linear_solver: lapack
refinement_steps: 0
"#;
let spec = parse_bvp_task_from_str(input).expect("banded BVP task should parse");
assert_eq!(spec.solver.backend, BvpLinearBackendSpec::Banded);
assert_eq!(
spec.solver_options.generated_backend.preset.as_deref(),
Some("banded_lambdify")
);
let config = generated_backend_config_from_spec(
&spec.solver_options.generated_backend,
&spec.solver.backend,
)
.expect("banded lambdify generated backend config should be valid");
assert_eq!(config.matrix_backend_override, Some(MatrixBackend::Banded));
assert_eq!(
config.backend_policy_override,
Some(BackendSelectionPolicy::LambdifyOnly)
);
assert_eq!(
config.symbolic_assembly_backend,
BvpSymbolicAssemblyBackend::AtomView
);
assert_eq!(
config.banded_linear_solver_config.policy,
LinearSolverPolicy::ForceBanded
);
assert_eq!(
config
.banded_linear_solver_config
.iterative_refinement_steps,
0
);
}
#[test]
fn bvp_task_parser_maps_banded_aot_tcc_generated_backend_config() {
let input = r#"
task
solver: BVP
strategy: Frozen
scheme: forward
method: Banded
equations
arg: x
unknowns: y
rhs: -y
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 0.0
solver_options
generated_backend: banded_aot_tcc
aot_build_policy: rebuild
aot_build_profile: release
aot_compile_preset: dev_fastest
aot_execution_policy: sequential
symbolic_backend: atomview
banded_linear_solver: faithful
refinement_steps: 1
"#;
let spec = parse_bvp_task_from_str(input).expect("banded AOT BVP task should parse");
let config = generated_backend_config_from_spec(
&spec.solver_options.generated_backend,
&spec.solver.backend,
)
.expect("banded AOT/tcc generated backend config should be valid");
assert_eq!(config.matrix_backend_override, Some(MatrixBackend::Banded));
assert_eq!(
config.backend_policy_override,
Some(BackendSelectionPolicy::PreferAotThenLambdify)
);
assert_eq!(
config.symbolic_assembly_backend,
BvpSymbolicAssemblyBackend::AtomView
);
assert_eq!(config.aot_codegen_backend, AotCodegenBackend::C);
assert_eq!(config.aot_c_compiler.as_deref(), Some("tcc"));
assert_eq!(
config.aot_execution_policy,
AotExecutionPolicy::SequentialOnly
);
assert!(matches!(
config.aot_build_policy,
AotBuildPolicy::RebuildAlways {
profile: AotBuildProfile::Release
}
));
assert_eq!(
config.banded_linear_solver_config.policy,
LinearSolverPolicy::ForceBanded
);
assert_eq!(
config
.banded_linear_solver_config
.iterative_refinement_steps,
1
);
}
#[test]
fn bvp_task_docs_reject_numeric_backend_policies_because_they_cannot_carry_closures() {
for policy in [
"numeric_only",
"prefer_aot_then_numeric",
"prefer_lambdify_then_numeric",
] {
let input = format!(
r#"
task
solver: BVP
strategy: Damped
scheme: forward
method: Sparse
equations
arg: x
unknowns: y
rhs: -y
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 0.5
solver_options
backend_policy: {policy}
"#
);
let spec = parse_bvp_task_from_str(&input).expect("syntax is valid");
let err = match build_bvp_solver_from_spec(&spec) {
Ok(_) => panic!("task docs cannot request pure numeric closure route: {policy}"),
Err(err) => err,
};
match err {
BvpTaskError::Semantic(message) => {
assert!(
message.contains("cannot provide Rust numeric_rhs closures"),
"unexpected semantic error for {policy}: {message}"
);
}
other => panic!("expected semantic error for {policy}, got {other:?}"),
}
}
}
#[test]
fn bvp_task_docs_accept_symbolic_backend_policies_after_numeric_guard() {
for policy in ["lambdify_only", "prefer_aot_then_lambdify", "aot_only"] {
let input = format!(
r#"
task
solver: BVP
strategy: Damped
scheme: forward
method: Sparse
equations
arg: x
unknowns: y
rhs: -y
boundary_conditions
y_left: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 8
initial_guess
y: 0.5
solver_options
backend_policy: {policy}
"#
);
let spec = parse_bvp_task_from_str(&input).expect("syntax is valid");
build_bvp_solver_from_spec(&spec)
.unwrap_or_else(|err| panic!("symbolic policy {policy} should be accepted: {err}"));
}
}
#[test]
fn bvp_task_runner_solves_reference_problem() {
let input = r#"
task
solver: BVP
strategy: Damped
scheme: forward
method: Sparse
equations
arg: x
unknowns: z, y
rhs: y-z, -z^3
boundary_conditions
z_left: 1.0
y_right: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 20
initial_guess
guess: 0.0, 0.0
solver_options
tolerance: 1e-5
max_iterations: 20
loglevel: off
"#;
let result = run_bvp_task_from_str(input).expect("BVP task should solve");
let matrix = result
.result
.expect("solver should produce a result matrix");
assert!(matrix.nrows() > 0);
assert!(matrix.ncols() > 0);
}
#[test]
fn bvp_task_runner_can_save_csv() {
let dir = tempdir().expect("tempdir should be created");
let csv_path = dir.path().join("bvp_task_output.csv");
let input = format!(
r#"
task
solver: BVP
strategy: Damped
scheme: forward
method: Sparse
equations
arg: x
unknowns: z, y
rhs: y-z, -z^3
boundary_conditions
z_left: 1.0
y_right: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 20
initial_guess
z: 0.0
y: 0.0
solver_options
tolerance: 1e-5
max_iterations: 20
loglevel: off
postprocessing
save_csv: true
csv_path: {}
plot: false
"#,
csv_path.display()
);
let result = run_bvp_task_from_str(&input).expect("BVP task should solve and save CSV");
assert!(result.result.is_some());
assert!(csv_path.exists());
}
#[test]
fn bvp_task_runner_can_execute_modern_postprocessing_plan() {
let dir = tempdir().expect("tempdir should be created");
let txt_path = dir.path().join("bvp_task_output.txt");
let report_path = dir.path().join("bvp_task_report.md");
let input = format!(
r#"
task
solver: BVP
strategy: Damped
scheme: forward
method: Sparse
equations
arg: x
unknowns: z, y
rhs: y-z, -z^3
boundary_conditions
z_left: 1.0
y_right: 1.0
mesh
t0: 0.0
t_end: 1.0
n_steps: 20
initial_guess
z: 0.0
y: 0.0
solver_options
tolerance: 1e-5
max_iterations: 20
loglevel: off
postprocessing
save_txt: true
txt_path: {}
write_report: true
report_path: {}
plot: false
"#,
txt_path.display(),
report_path.display()
);
let result = run_bvp_task_from_str(&input).expect("BVP task should solve and postprocess");
assert!(result.result.is_some());
assert!(txt_path.exists());
let report = std::fs::read_to_string(&report_path)
.expect("report should be readable after postprocessing");
assert!(report.contains("Solver Result Report"));
assert!(report.contains("axis: x"));
}