use crate::numerical::BVP_Damp::BVP_traits::{Fun, Jac};
use crate::somelinalg::banded::LinearSolverConfig;
use crate::symbolic::codegen::c_backend::codegen_c_aot_build::{
CAotBuildProfile, CAotBuildRequest, CAotCompileConfig,
};
use crate::symbolic::codegen::c_backend::codegen_c_aot_registry::register_c_build_in_registry;
use crate::symbolic::codegen::c_backend::codegen_c_aot_runtime_link::{
register_generated_c_banded_backend, register_generated_c_sparse_backend,
};
use crate::symbolic::codegen::codegen_aot_driver::AotCodegenBackend;
use crate::symbolic::codegen::codegen_aot_registry::AotRegistry;
use crate::symbolic::codegen::codegen_aot_resolution::AotResolver;
use crate::symbolic::codegen::codegen_aot_runtime_link::{
register_generated_banded_cdylib_backend, register_generated_sparse_cdylib_backend,
resolve_linked_sparse_backend, unregister_linked_sparse_backend,
};
use crate::symbolic::codegen::codegen_backend_selection::{
BackendSelectionPolicy, SelectedBackendKind,
};
use crate::symbolic::codegen::codegen_manifest::PreparedProblemManifest;
use crate::symbolic::codegen::codegen_orchestrator::ParallelExecutorConfig;
use crate::symbolic::codegen::codegen_provider_api::MatrixBackend;
use crate::symbolic::codegen::codegen_runtime_api::ResidualChunkingStrategy;
use crate::symbolic::codegen::codegen_tasks::SparseChunkingStrategy;
use crate::symbolic::codegen::rust_backend::codegen_aot_build::{
AotBuildProfile as LifecycleBuildProfile, AotBuildRequest,
AotCompileConfig as LifecycleAotCompileConfig,
};
use crate::symbolic::codegen::zig_backend::codegen_zig_aot_build::{
ZigAotBuildProfile, ZigAotBuildRequest,
};
use crate::symbolic::codegen::zig_backend::codegen_zig_aot_registry::register_zig_build_in_registry;
use crate::symbolic::codegen::zig_backend::codegen_zig_aot_runtime_link::{
register_generated_zig_banded_backend, register_generated_zig_sparse_backend,
};
use crate::symbolic::symbolic_engine::Expr;
use crate::symbolic::symbolic_functions_BVP::{
BvpBackendIntegrationError, BvpLegacySolverBundle, BvpSparseSolverBundle,
BvpSymbolicAssemblyBackend, Jacobian,
};
use log::{error, info};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
pub struct DampedGeneratedSolverState {
pub fun: Box<dyn Fun>,
pub jac: Option<Box<dyn Jac>>,
pub bounds_vec: Vec<(f64, f64)>,
pub rel_tolerance_vec: Vec<f64>,
pub variable_string: Vec<String>,
pub bandwidth: (usize, usize),
pub bc_position_and_value: Vec<(usize, usize, f64)>,
pub updated_resolver: Option<AotResolver>,
}
pub struct FrozenGeneratedSolverState {
pub fun: Box<dyn Fun>,
pub jac: Option<Box<dyn Jac>>,
pub variable_string: Vec<String>,
pub bandwidth: (usize, usize),
pub updated_resolver: Option<AotResolver>,
}
pub trait ApplyDampedGeneratedSolverState {
fn apply_generated_solver_state(&mut self, state: DampedGeneratedSolverState);
}
pub trait ApplyFrozenGeneratedSolverState {
fn apply_generated_solver_state(&mut self, state: FrozenGeneratedSolverState);
}
pub trait BuildDampedSolverRequest {
fn build_solver_request(
&mut self,
mesh: Option<Vec<f64>>,
bandwidth: Option<(usize, usize)>,
) -> DampedSolverBuildRequest;
}
pub trait BuildFrozenSolverRequest {
fn build_solver_request(&self) -> FrozenSolverBuildRequest;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum AotBuildProfile {
#[default]
Release,
Debug,
}
pub type AotCompileConfig = LifecycleAotCompileConfig;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum AotBuildPolicy {
#[default]
UseIfAvailable,
BuildIfMissing {
profile: AotBuildProfile,
},
RequirePrebuilt,
RebuildAlways {
profile: AotBuildProfile,
},
}
impl AotBuildPolicy {
pub fn as_str(&self) -> &'static str {
match self {
Self::UseIfAvailable => "UseIfAvailable",
Self::BuildIfMissing { .. } => "BuildIfMissing",
Self::RequirePrebuilt => "RequirePrebuilt",
Self::RebuildAlways { .. } => "RebuildAlways",
}
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub enum AotExecutionPolicy {
#[default]
Auto,
SequentialOnly,
Parallel(ParallelExecutorConfig),
}
impl AotExecutionPolicy {
pub fn as_str(&self) -> &'static str {
match self {
Self::Auto => "Auto",
Self::SequentialOnly => "SequentialOnly",
Self::Parallel(_) => "Parallel",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct AotChunkingPolicy {
pub residual: Option<ResidualChunkingStrategy>,
pub sparse_jacobian: Option<SparseChunkingStrategy>,
}
impl AotChunkingPolicy {
pub fn new() -> Self {
Self::default()
}
pub fn with_parts(
residual: Option<ResidualChunkingStrategy>,
sparse_jacobian: Option<SparseChunkingStrategy>,
) -> Self {
Self {
residual,
sparse_jacobian,
}
}
}
#[derive(Clone, Default)]
pub struct GeneratedBackendConfig {
pub backend_policy_override: Option<BackendSelectionPolicy>,
pub resolver: Option<AotResolver>,
pub aot_execution_policy: AotExecutionPolicy,
pub aot_build_policy: AotBuildPolicy,
pub aot_compile_config: AotCompileConfig,
pub aot_codegen_backend: AotCodegenBackend,
pub aot_c_compiler: Option<String>,
pub aot_chunking_policy: AotChunkingPolicy,
pub symbolic_assembly_backend: BvpSymbolicAssemblyBackend,
pub matrix_backend_override: Option<MatrixBackend>,
pub banded_linear_solver_config: LinearSolverConfig,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum SparseGeneratedBackendMode {
#[default]
Defaults,
RequirePrebuilt,
BuildIfMissingRelease,
}
impl SparseGeneratedBackendMode {
pub fn generated_backend_config(self) -> GeneratedBackendConfig {
match self {
Self::Defaults => GeneratedBackendConfig::sparse_defaults(),
Self::RequirePrebuilt => GeneratedBackendConfig::sparse_require_prebuilt(),
Self::BuildIfMissingRelease => {
GeneratedBackendConfig::sparse_build_if_missing_release()
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum BandedGeneratedBackendMode {
#[default]
Defaults,
Lambdify,
BuildIfMissingRelease,
}
impl BandedGeneratedBackendMode {
pub fn generated_backend_config(self) -> GeneratedBackendConfig {
match self {
Self::Defaults => GeneratedBackendConfig::banded_defaults(),
Self::Lambdify => GeneratedBackendConfig::banded_lambdify_defaults(),
Self::BuildIfMissingRelease => {
GeneratedBackendConfig::banded_build_if_missing_release()
}
}
}
}
impl GeneratedBackendConfig {
pub fn new() -> Self {
Self::default()
}
pub fn sparse_defaults() -> Self {
Self::new()
.with_backend_policy_override(Some(BackendSelectionPolicy::PreferAotThenLambdify))
}
pub fn sparse_require_prebuilt() -> Self {
Self::sparse_defaults().with_aot_build_policy(AotBuildPolicy::RequirePrebuilt)
}
pub fn sparse_build_if_missing_release() -> Self {
Self::sparse_defaults()
.with_aot_build_policy(AotBuildPolicy::BuildIfMissing {
profile: AotBuildProfile::Release,
})
.with_aot_compile_dev_fastest()
}
pub fn from_sparse_mode(mode: SparseGeneratedBackendMode) -> Self {
mode.generated_backend_config()
}
pub fn banded_defaults() -> Self {
Self::sparse_defaults()
.with_matrix_backend_override(MatrixBackend::Banded)
.with_banded_linear_solver_config(LinearSolverConfig::faithful_banded())
}
pub fn banded_lambdify_defaults() -> Self {
Self::banded_defaults()
.with_backend_policy_override(Some(BackendSelectionPolicy::LambdifyOnly))
}
pub fn banded_build_if_missing_release() -> Self {
Self::banded_defaults()
.with_aot_build_policy(AotBuildPolicy::BuildIfMissing {
profile: AotBuildProfile::Release,
})
.with_aot_compile_dev_fastest()
}
pub fn from_banded_mode(mode: BandedGeneratedBackendMode) -> Self {
mode.generated_backend_config()
}
pub fn with_parts(
backend_policy_override: Option<BackendSelectionPolicy>,
resolver: Option<AotResolver>,
) -> Self {
Self {
backend_policy_override,
resolver,
aot_execution_policy: AotExecutionPolicy::Auto,
aot_build_policy: AotBuildPolicy::UseIfAvailable,
aot_compile_config: AotCompileConfig::default(),
aot_codegen_backend: AotCodegenBackend::Rust,
aot_c_compiler: None,
aot_chunking_policy: AotChunkingPolicy::default(),
symbolic_assembly_backend: BvpSymbolicAssemblyBackend::ExprLegacy,
matrix_backend_override: None,
banded_linear_solver_config: crate::somelinalg::banded::LinearSolverConfig::default(),
}
}
pub fn with_backend_policy_override(
mut self,
backend_policy_override: Option<BackendSelectionPolicy>,
) -> Self {
self.backend_policy_override = backend_policy_override;
self
}
pub fn with_resolver(mut self, resolver: Option<AotResolver>) -> Self {
self.resolver = resolver;
self
}
pub fn with_aot_execution_policy(mut self, policy: AotExecutionPolicy) -> Self {
self.aot_execution_policy = policy;
self
}
pub fn with_aot_build_policy(mut self, policy: AotBuildPolicy) -> Self {
self.aot_build_policy = policy;
self
}
pub fn with_aot_compile_config(mut self, config: AotCompileConfig) -> Self {
self.aot_compile_config = config;
self
}
pub fn with_aot_codegen_backend(mut self, backend: AotCodegenBackend) -> Self {
self.aot_codegen_backend = backend;
self
}
pub fn with_aot_c_compiler(mut self, compiler: impl Into<String>) -> Self {
self.aot_c_compiler = Some(compiler.into());
self
}
pub fn sparse_atomview_build_if_missing_release_gcc() -> Self {
Self::sparse_build_if_missing_release()
.with_symbolic_assembly_backend(BvpSymbolicAssemblyBackend::AtomView)
.with_aot_codegen_backend(AotCodegenBackend::C)
.with_aot_c_compiler("gcc")
}
pub fn sparse_atomview_build_if_missing_release_tcc() -> Self {
Self::sparse_build_if_missing_release()
.with_symbolic_assembly_backend(BvpSymbolicAssemblyBackend::AtomView)
.with_aot_codegen_backend(AotCodegenBackend::C)
.with_aot_c_compiler("tcc")
}
pub fn sparse_atomview_build_if_missing_release_zig() -> Self {
Self::sparse_build_if_missing_release()
.with_symbolic_assembly_backend(BvpSymbolicAssemblyBackend::AtomView)
.with_aot_codegen_backend(AotCodegenBackend::Zig)
}
pub fn sparse_atomview_for_repeated_solves() -> Self {
Self::sparse_atomview_build_if_missing_release_tcc()
}
pub fn banded_atomview_build_if_missing_release_gcc() -> Self {
Self::banded_build_if_missing_release()
.with_symbolic_assembly_backend(BvpSymbolicAssemblyBackend::AtomView)
.with_aot_codegen_backend(AotCodegenBackend::C)
.with_aot_c_compiler("gcc")
}
pub fn banded_atomview_build_if_missing_release_tcc() -> Self {
Self::banded_build_if_missing_release()
.with_symbolic_assembly_backend(BvpSymbolicAssemblyBackend::AtomView)
.with_aot_codegen_backend(AotCodegenBackend::C)
.with_aot_c_compiler("tcc")
}
pub fn banded_atomview_build_if_missing_release_zig() -> Self {
Self::banded_build_if_missing_release()
.with_symbolic_assembly_backend(BvpSymbolicAssemblyBackend::AtomView)
.with_aot_codegen_backend(AotCodegenBackend::Zig)
}
pub fn banded_atomview_for_repeated_solves() -> Self {
Self::banded_atomview_build_if_missing_release_tcc()
}
pub fn with_aot_compile_production(self) -> Self {
self.with_aot_compile_config(AotCompileConfig::production())
}
pub fn with_aot_compile_fast_build(self) -> Self {
self.with_aot_compile_config(AotCompileConfig::fast_build())
}
pub fn with_aot_compile_dev_fastest(self) -> Self {
self.with_aot_compile_config(AotCompileConfig::dev_fastest())
}
pub fn with_aot_chunking_policy(mut self, policy: AotChunkingPolicy) -> Self {
self.aot_chunking_policy = policy;
self
}
pub fn with_symbolic_assembly_backend(mut self, backend: BvpSymbolicAssemblyBackend) -> Self {
self.symbolic_assembly_backend = backend;
self
}
pub fn with_matrix_backend_override(mut self, backend: MatrixBackend) -> Self {
self.matrix_backend_override = Some(backend);
self
}
pub fn with_banded_linear_solver_config(mut self, config: LinearSolverConfig) -> Self {
self.banded_linear_solver_config = config;
self
}
pub fn effective_method(&self, fallback_method: &str) -> String {
self.matrix_backend_override
.map(|backend| match backend {
MatrixBackend::Banded => "Banded",
MatrixBackend::Dense => "Dense",
MatrixBackend::CsMat => "Sparse_1",
MatrixBackend::CsMatrix => "Sparse_2",
MatrixBackend::SparseCol | MatrixBackend::ValuesOnly => "Sparse",
})
.unwrap_or(fallback_method)
.to_string()
}
pub fn effective_backend_policy(&self, method: &str) -> BackendSelectionPolicy {
self.backend_policy_override
.unwrap_or_else(|| backend_policy_for_method(method))
}
}
pub struct DampedSolverBuildRequest {
pub eq_system: Vec<Expr>,
pub values: Vec<String>,
pub param_names: Option<Vec<String>>,
pub param_values: Option<Vec<f64>>,
pub arg: String,
pub t0: f64,
pub n_steps: Option<usize>,
pub h: Option<f64>,
pub mesh: Option<Vec<f64>>,
pub border_conditions: HashMap<String, Vec<(usize, f64)>>,
pub bounds: Option<HashMap<String, (f64, f64)>>,
pub rel_tolerance: Option<HashMap<String, f64>>,
pub scheme: String,
pub method: String,
pub bandwidth: Option<(usize, usize)>,
pub backend_policy: BackendSelectionPolicy,
pub resolver: Option<AotResolver>,
pub aot_execution_policy: AotExecutionPolicy,
pub aot_build_policy: AotBuildPolicy,
pub aot_compile_config: AotCompileConfig,
pub aot_codegen_backend: AotCodegenBackend,
pub aot_c_compiler: Option<String>,
pub aot_chunking_policy: AotChunkingPolicy,
pub symbolic_assembly_backend: BvpSymbolicAssemblyBackend,
pub matrix_backend_override: Option<MatrixBackend>,
pub banded_linear_solver_config: LinearSolverConfig,
}
impl DampedSolverBuildRequest {
pub fn generate(self) -> Result<DampedGeneratedSolverState, BvpBackendIntegrationError> {
generate_damped_solver_state(
self.eq_system,
self.values,
self.param_names,
self.param_values,
self.arg,
self.t0,
self.n_steps,
self.h,
self.mesh,
self.border_conditions,
self.bounds,
self.rel_tolerance,
self.scheme,
self.method,
self.bandwidth,
self.backend_policy,
self.resolver.as_ref(),
self.aot_execution_policy,
self.aot_build_policy,
self.aot_compile_config,
self.aot_codegen_backend,
self.aot_c_compiler,
self.aot_chunking_policy,
self.symbolic_assembly_backend,
self.matrix_backend_override,
self.banded_linear_solver_config,
)
}
}
pub fn try_build_and_apply_damped_solver_state<T: ApplyDampedGeneratedSolverState>(
target: &mut T,
request: DampedSolverBuildRequest,
context: &str,
) -> Result<(), BvpBackendIntegrationError> {
info!("{context}: generating damped solver state");
match request.generate() {
Ok(state) => {
target.apply_generated_solver_state(state);
info!("{context}: damped solver state applied");
Ok(())
}
Err(err) => {
error!("{context}: {err:?}");
Err(err)
}
}
}
pub fn build_and_apply_damped_solver_state<T: ApplyDampedGeneratedSolverState>(
target: &mut T,
request: DampedSolverBuildRequest,
context: &str,
) {
try_build_and_apply_damped_solver_state(target, request, context)
.unwrap_or_else(|err| panic!("{context}: {err:?}"));
}
pub fn try_generate_and_apply_damped_solver_state<
T: BuildDampedSolverRequest + ApplyDampedGeneratedSolverState,
>(
target: &mut T,
mesh: Option<Vec<f64>>,
bandwidth: Option<(usize, usize)>,
context: &str,
) -> Result<(), BvpBackendIntegrationError> {
let request = target.build_solver_request(mesh, bandwidth);
try_build_and_apply_damped_solver_state(target, request, context)
}
pub fn generate_and_apply_damped_solver_state<
T: BuildDampedSolverRequest + ApplyDampedGeneratedSolverState,
>(
target: &mut T,
mesh: Option<Vec<f64>>,
bandwidth: Option<(usize, usize)>,
context: &str,
) {
try_generate_and_apply_damped_solver_state(target, mesh, bandwidth, context)
.unwrap_or_else(|err| panic!("{context}: {err:?}"));
}
pub struct FrozenSolverBuildRequest {
pub eq_system: Vec<Expr>,
pub values: Vec<String>,
pub arg: String,
pub param_names: Option<Vec<String>>,
pub param_values: Option<Vec<f64>>,
pub t0: f64,
pub n_steps: Option<usize>,
pub h: Option<f64>,
pub mesh: Option<Vec<f64>>,
pub border_conditions: HashMap<String, Vec<(usize, f64)>>,
pub scheme: String,
pub method: String,
pub bandwidth: Option<(usize, usize)>,
pub backend_policy: BackendSelectionPolicy,
pub resolver: Option<AotResolver>,
pub aot_execution_policy: AotExecutionPolicy,
pub aot_build_policy: AotBuildPolicy,
pub aot_compile_config: AotCompileConfig,
pub aot_codegen_backend: AotCodegenBackend,
pub aot_c_compiler: Option<String>,
pub aot_chunking_policy: AotChunkingPolicy,
pub symbolic_assembly_backend: BvpSymbolicAssemblyBackend,
pub matrix_backend_override: Option<MatrixBackend>,
pub banded_linear_solver_config: LinearSolverConfig,
}
impl FrozenSolverBuildRequest {
pub fn generate(self) -> Result<FrozenGeneratedSolverState, BvpBackendIntegrationError> {
generate_frozen_solver_state(
self.eq_system,
self.values,
self.arg,
self.param_names,
self.param_values,
self.t0,
self.n_steps,
self.h,
self.mesh,
self.border_conditions,
self.scheme,
self.method,
self.bandwidth,
self.backend_policy,
self.resolver.as_ref(),
self.aot_execution_policy,
self.aot_build_policy,
self.aot_compile_config,
self.aot_codegen_backend,
self.aot_c_compiler,
self.aot_chunking_policy,
self.symbolic_assembly_backend,
self.matrix_backend_override,
self.banded_linear_solver_config,
)
}
}
pub fn try_build_and_apply_frozen_solver_state<T: ApplyFrozenGeneratedSolverState>(
target: &mut T,
request: FrozenSolverBuildRequest,
context: &str,
) -> Result<(), BvpBackendIntegrationError> {
info!("{context}: generating frozen solver state");
match request.generate() {
Ok(state) => {
target.apply_generated_solver_state(state);
info!("{context}: frozen solver state applied");
Ok(())
}
Err(err) => {
error!("{context}: {err:?}");
Err(err)
}
}
}
pub fn build_and_apply_frozen_solver_state<T: ApplyFrozenGeneratedSolverState>(
target: &mut T,
request: FrozenSolverBuildRequest,
context: &str,
) {
try_build_and_apply_frozen_solver_state(target, request, context)
.unwrap_or_else(|err| panic!("{context}: {err:?}"));
}
pub fn try_generate_and_apply_frozen_solver_state<
T: BuildFrozenSolverRequest + ApplyFrozenGeneratedSolverState,
>(
target: &mut T,
context: &str,
) -> Result<(), BvpBackendIntegrationError> {
let request = target.build_solver_request();
try_build_and_apply_frozen_solver_state(target, request, context)
}
pub fn generate_and_apply_frozen_solver_state<
T: BuildFrozenSolverRequest + ApplyFrozenGeneratedSolverState,
>(
target: &mut T,
context: &str,
) {
try_generate_and_apply_frozen_solver_state(target, context)
.unwrap_or_else(|err| panic!("{context}: {err:?}"));
}
#[allow(clippy::too_many_arguments)]
pub fn generate_damped_solver_state(
eq_system: Vec<Expr>,
values: Vec<String>,
param_names: Option<Vec<String>>,
param_values: Option<Vec<f64>>,
arg: String,
t0: f64,
n_steps: Option<usize>,
h: Option<f64>,
mesh: Option<Vec<f64>>,
border_conditions: HashMap<String, Vec<(usize, f64)>>,
bounds: Option<HashMap<String, (f64, f64)>>,
rel_tolerance: Option<HashMap<String, f64>>,
scheme: String,
method: String,
bandwidth: Option<(usize, usize)>,
backend_policy: BackendSelectionPolicy,
resolver: Option<&AotResolver>,
aot_execution_policy: AotExecutionPolicy,
aot_build_policy: AotBuildPolicy,
aot_compile_config: AotCompileConfig,
aot_codegen_backend: AotCodegenBackend,
aot_c_compiler: Option<String>,
aot_chunking_policy: AotChunkingPolicy,
symbolic_assembly_backend: BvpSymbolicAssemblyBackend,
matrix_backend_override: Option<MatrixBackend>,
banded_linear_solver_config: LinearSolverConfig,
) -> Result<DampedGeneratedSolverState, BvpBackendIntegrationError> {
let param_name_refs = parameter_name_refs(param_names.as_ref());
if matches!(method.as_str(), "Sparse" | "Banded")
|| matches!(matrix_backend_override, Some(MatrixBackend::Banded))
{
let jacobian_instance = prepared_bvp_jacobian(
symbolic_assembly_backend,
param_name_refs.as_deref(),
param_values.clone(),
banded_linear_solver_config,
);
let backend_policy = effective_backend_policy_for_build(backend_policy, aot_build_policy);
let bundle = try_generate_sparse_bundle(
jacobian_instance,
eq_system,
values,
arg,
param_name_refs.as_deref(),
t0,
n_steps,
h,
mesh,
border_conditions,
bounds,
rel_tolerance,
scheme,
method,
bandwidth,
backend_policy,
resolver,
aot_chunking_policy,
)?;
let (bundle, updated_resolver) = enforce_build_policy_on_sparse_bundle(
bundle,
resolver,
aot_build_policy,
aot_compile_config,
aot_codegen_backend,
aot_c_compiler,
)?;
let bundle = apply_execution_policy_to_sparse_bundle(bundle, aot_execution_policy)?;
Ok(damped_state_from_sparse_solver_bundle(
bundle,
updated_resolver,
))
} else {
let jacobian_instance = prepared_bvp_jacobian(
symbolic_assembly_backend,
param_name_refs.as_deref(),
param_values,
banded_linear_solver_config,
);
let legacy_bundle = jacobian_instance.generate_legacy_solver_bundle_with_params(
eq_system,
values,
arg,
param_name_refs.as_deref(),
t0,
None,
n_steps,
h,
mesh,
border_conditions,
bounds,
rel_tolerance,
scheme,
method,
bandwidth,
);
Ok(damped_state_from_legacy_solver_bundle(legacy_bundle))
}
}
#[allow(clippy::too_many_arguments)]
pub fn generate_frozen_solver_state(
eq_system: Vec<Expr>,
values: Vec<String>,
arg: String,
param_names: Option<Vec<String>>,
param_values: Option<Vec<f64>>,
t0: f64,
n_steps: Option<usize>,
h: Option<f64>,
mesh: Option<Vec<f64>>,
border_conditions: HashMap<String, Vec<(usize, f64)>>,
scheme: String,
method: String,
bandwidth: Option<(usize, usize)>,
backend_policy: BackendSelectionPolicy,
resolver: Option<&AotResolver>,
aot_execution_policy: AotExecutionPolicy,
aot_build_policy: AotBuildPolicy,
aot_compile_config: AotCompileConfig,
aot_codegen_backend: AotCodegenBackend,
aot_c_compiler: Option<String>,
aot_chunking_policy: AotChunkingPolicy,
symbolic_assembly_backend: BvpSymbolicAssemblyBackend,
matrix_backend_override: Option<MatrixBackend>,
banded_linear_solver_config: LinearSolverConfig,
) -> Result<FrozenGeneratedSolverState, BvpBackendIntegrationError> {
let param_name_refs = parameter_name_refs(param_names.as_ref());
if matches!(method.as_str(), "Sparse" | "Banded")
|| matches!(matrix_backend_override, Some(MatrixBackend::Banded))
{
let jacobian_instance = prepared_bvp_jacobian(
symbolic_assembly_backend,
param_name_refs.as_deref(),
param_values.clone(),
banded_linear_solver_config,
);
let backend_policy = effective_backend_policy_for_build(backend_policy, aot_build_policy);
let bundle = try_generate_sparse_bundle(
jacobian_instance,
eq_system,
values,
arg,
param_name_refs.as_deref(),
t0,
n_steps,
h,
mesh,
border_conditions,
None,
None,
scheme,
method,
bandwidth,
backend_policy,
resolver,
aot_chunking_policy,
)?;
let (bundle, updated_resolver) = enforce_build_policy_on_sparse_bundle(
bundle,
resolver,
aot_build_policy,
aot_compile_config,
aot_codegen_backend,
aot_c_compiler,
)?;
let bundle = apply_execution_policy_to_sparse_bundle(bundle, aot_execution_policy)?;
Ok(frozen_state_from_sparse_solver_bundle(
bundle,
updated_resolver,
))
} else {
let jacobian_instance = prepared_bvp_jacobian(
symbolic_assembly_backend,
param_name_refs.as_deref(),
param_values,
banded_linear_solver_config,
);
let legacy_bundle = jacobian_instance.generate_legacy_solver_bundle_with_params(
eq_system,
values,
arg,
param_name_refs.as_deref(),
t0,
None,
n_steps,
h,
mesh,
border_conditions,
None,
None,
scheme,
method,
bandwidth,
);
Ok(frozen_state_from_legacy_solver_bundle(legacy_bundle))
}
}
pub fn backend_policy_for_method(method: &str) -> BackendSelectionPolicy {
if method == "Sparse" {
BackendSelectionPolicy::PreferAotThenLambdify
} else {
BackendSelectionPolicy::LambdifyOnly
}
}
fn backend_policy_targets_aot(policy: BackendSelectionPolicy) -> bool {
matches!(
policy,
BackendSelectionPolicy::AotOnly
| BackendSelectionPolicy::PreferAotThenLambdify
| BackendSelectionPolicy::PreferAotThenNumeric
)
}
fn effective_backend_policy_for_build(
backend_policy: BackendSelectionPolicy,
build_policy: AotBuildPolicy,
) -> BackendSelectionPolicy {
match build_policy {
AotBuildPolicy::UseIfAvailable
| AotBuildPolicy::BuildIfMissing { .. }
| AotBuildPolicy::RebuildAlways { .. } => backend_policy,
AotBuildPolicy::RequirePrebuilt => {
if backend_policy_targets_aot(backend_policy) {
BackendSelectionPolicy::AotOnly
} else {
backend_policy
}
}
}
}
fn parameter_name_refs(param_names: Option<&Vec<String>>) -> Option<Vec<&str>> {
param_names.map(|names| names.iter().map(|name| name.as_str()).collect())
}
fn prepared_bvp_jacobian(
symbolic_assembly_backend: BvpSymbolicAssemblyBackend,
param_name_refs: Option<&[&str]>,
param_values: Option<Vec<f64>>,
banded_linear_solver_config: LinearSolverConfig,
) -> Jacobian {
let mut jacobian_instance = Jacobian::new();
jacobian_instance.set_symbolic_assembly_backend(symbolic_assembly_backend);
jacobian_instance.set_params(param_name_refs);
jacobian_instance.set_param_values(param_values);
jacobian_instance.set_banded_linear_solver_config(banded_linear_solver_config);
jacobian_instance
}
#[allow(clippy::too_many_arguments)]
fn try_generate_sparse_bundle(
jacobian_instance: Jacobian,
eq_system: Vec<Expr>,
values: Vec<String>,
arg: String,
param_name_refs: Option<&[&str]>,
t0: f64,
n_steps: Option<usize>,
h: Option<f64>,
mesh: Option<Vec<f64>>,
border_conditions: HashMap<String, Vec<(usize, f64)>>,
bounds: Option<HashMap<String, (f64, f64)>>,
rel_tolerance: Option<HashMap<String, f64>>,
scheme: String,
method: String,
bandwidth: Option<(usize, usize)>,
backend_policy: BackendSelectionPolicy,
resolver: Option<&AotResolver>,
aot_chunking_policy: AotChunkingPolicy,
) -> Result<BvpSparseSolverBundle, BvpBackendIntegrationError> {
match (
aot_chunking_policy.residual,
aot_chunking_policy.sparse_jacobian,
) {
(None, None) => jacobian_instance.try_generate_sparse_solver_bundle_with_backend_selection(
eq_system,
values,
arg,
param_name_refs,
t0,
None,
n_steps,
h,
mesh,
border_conditions,
bounds,
rel_tolerance,
scheme,
method,
bandwidth,
backend_policy,
resolver,
),
(residual, sparse_jacobian) => jacobian_instance
.try_generate_sparse_solver_bundle_with_backend_selection_and_chunking(
eq_system,
values,
arg,
param_name_refs,
t0,
None,
n_steps,
h,
mesh,
border_conditions,
bounds,
rel_tolerance,
scheme,
method,
bandwidth,
backend_policy,
resolver,
residual.unwrap_or(ResidualChunkingStrategy::Whole),
sparse_jacobian.unwrap_or(SparseChunkingStrategy::Whole),
),
}
}
fn sanitize_generated_name(input: &str) -> String {
input
.chars()
.map(|ch| match ch {
'a'..='z' | 'A'..='Z' | '0'..='9' => ch.to_ascii_lowercase(),
_ => '_',
})
.collect::<String>()
}
fn build_output_parent_for_problem(problem_key: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("target")
.join("generated-aot")
.join(sanitize_generated_name(problem_key))
}
fn unique_build_output_parent_for_problem(problem_key: &str) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock should be after unix epoch")
.as_nanos();
build_output_parent_for_problem(problem_key)
.join(format!("build-{}-{nonce}", std::process::id()))
}
fn to_lifecycle_build_profile(profile: AotBuildProfile) -> LifecycleBuildProfile {
match profile {
AotBuildProfile::Release => LifecycleBuildProfile::Release,
AotBuildProfile::Debug => LifecycleBuildProfile::Debug,
}
}
fn to_c_build_profile(profile: AotBuildProfile) -> CAotBuildProfile {
match profile {
AotBuildProfile::Release => CAotBuildProfile::Release,
AotBuildProfile::Debug => CAotBuildProfile::Debug,
}
}
fn to_zig_build_profile(profile: AotBuildProfile) -> ZigAotBuildProfile {
match profile {
AotBuildProfile::Release => ZigAotBuildProfile::ReleaseFast,
AotBuildProfile::Debug => ZigAotBuildProfile::Debug,
}
}
fn to_c_compile_config(compile_config: &AotCompileConfig) -> CAotCompileConfig {
if *compile_config == AotCompileConfig::dev_fastest() {
CAotCompileConfig::dev_fastest()
} else if *compile_config == AotCompileConfig::fast_build() {
CAotCompileConfig::fast_build()
} else {
CAotCompileConfig::production()
}
}
fn register_sparse_runtime_from_registered_artifact(
artifact: &crate::symbolic::codegen::codegen_aot_registry::RegisteredAotArtifact,
matrix_backend: MatrixBackend,
backend: AotCodegenBackend,
) -> Result<(), String> {
match backend {
AotCodegenBackend::Rust => match matrix_backend {
MatrixBackend::Banded => register_generated_banded_cdylib_backend(artifact).map(|_| ()),
_ => register_generated_sparse_cdylib_backend(artifact).map(|_| ()),
},
AotCodegenBackend::C => match matrix_backend {
MatrixBackend::Banded => register_generated_c_banded_backend(artifact).map(|_| ()),
_ => register_generated_c_sparse_backend(artifact).map(|_| ()),
},
AotCodegenBackend::Zig => match matrix_backend {
MatrixBackend::Banded => register_generated_zig_banded_backend(artifact).map(|_| ()),
_ => register_generated_zig_sparse_backend(artifact).map(|_| ()),
},
}
}
fn rust_sparse_aot_build_request(
bundle: &BvpSparseSolverBundle,
problem_key: &str,
profile: AotBuildProfile,
compile_config: AotCompileConfig,
) -> AotBuildRequest {
let selected = bundle.execution.selected();
let backend_label = match selected.matrix_backend {
MatrixBackend::Banded => "banded",
_ => "sparse",
};
let crate_name = format!(
"generated_bvp_{}_{}",
backend_label,
sanitize_generated_name(problem_key)
);
let module_name = format!(
"generated_bvp_module_{}_{}",
backend_label,
sanitize_generated_name(problem_key)
);
let output_parent_dir = unique_build_output_parent_for_problem(problem_key);
AotBuildRequest::new(
selected
.prepared_problem
.generated_aot_artifact_with_breakdown_for_matrix_backend(
&crate_name,
&module_name,
AotCodegenBackend::Rust,
selected.matrix_backend,
)
.0
.into_rust_crate()
.expect("Rust backend must emit GeneratedAotCrate"),
output_parent_dir,
to_lifecycle_build_profile(profile),
)
.with_compile_config(compile_config)
}
fn c_sparse_aot_build_request(
bundle: &BvpSparseSolverBundle,
problem_key: &str,
profile: AotBuildProfile,
compile_config: CAotCompileConfig,
) -> CAotBuildRequest {
let selected = bundle.execution.selected();
let backend_label = match selected.matrix_backend {
MatrixBackend::Banded => "banded",
_ => "sparse",
};
let library_name = format!(
"generated_bvp_{}_{}",
backend_label,
sanitize_generated_name(problem_key)
);
let module_name = format!(
"generated_bvp_module_{}_{}",
backend_label,
sanitize_generated_name(problem_key)
);
let output_parent_dir = unique_build_output_parent_for_problem(problem_key);
let library_spec = match selected
.prepared_problem
.generated_aot_artifact_with_breakdown_for_matrix_backend(
&library_name,
&module_name,
AotCodegenBackend::C,
selected.matrix_backend,
)
.0
{
crate::symbolic::codegen::codegen_aot_driver::GeneratedAotArtifact::C(library) => library,
_ => unreachable!("C backend must emit GeneratedCAotLibrary"),
};
CAotBuildRequest::new(library_spec, output_parent_dir, to_c_build_profile(profile))
.with_compile_config(compile_config)
}
fn zig_sparse_aot_build_request(
bundle: &BvpSparseSolverBundle,
problem_key: &str,
profile: AotBuildProfile,
) -> ZigAotBuildRequest {
let selected = bundle.execution.selected();
let backend_label = match selected.matrix_backend {
MatrixBackend::Banded => "banded",
_ => "sparse",
};
let library_name = format!(
"generated_bvp_{}_{}",
backend_label,
sanitize_generated_name(problem_key)
);
let module_name = format!(
"generated_bvp_module_{}_{}",
backend_label,
sanitize_generated_name(problem_key)
);
let output_parent_dir = unique_build_output_parent_for_problem(problem_key);
let library_spec = match selected
.prepared_problem
.generated_aot_artifact_with_breakdown_for_matrix_backend(
&library_name,
&module_name,
AotCodegenBackend::Zig,
selected.matrix_backend,
)
.0
{
crate::symbolic::codegen::codegen_aot_driver::GeneratedAotArtifact::Zig(library) => library,
_ => unreachable!("Zig backend must emit GeneratedZigAotLibrary"),
};
ZigAotBuildRequest::new(
library_spec,
output_parent_dir,
to_zig_build_profile(profile),
)
}
fn try_materialize_and_build_sparse_aot_bundle(
bundle: &BvpSparseSolverBundle,
resolver: Option<&AotResolver>,
profile: AotBuildProfile,
compile_config: AotCompileConfig,
aot_codegen_backend: AotCodegenBackend,
aot_c_compiler: Option<String>,
) -> Result<AotResolver, BvpBackendIntegrationError> {
let selected = bundle.execution.selected();
let problem_key = selected.problem_key();
let manifest = PreparedProblemManifest::from(
&selected
.prepared_problem
.as_prepared_problem_for_matrix_backend(selected.matrix_backend),
);
let mut registry = resolver
.map(|existing| existing.registry().clone())
.unwrap_or_else(AotRegistry::new);
match aot_codegen_backend {
AotCodegenBackend::Rust => {
let request =
rust_sparse_aot_build_request(bundle, &problem_key, profile, compile_config);
info!(
"materializing sparse Rust AOT crate for problem_key={} with build_profile={:?}",
problem_key, profile
);
let build = request.materialize().map_err(|err| {
BvpBackendIntegrationError::AutomaticAotBuildFailed {
problem_key: problem_key.clone(),
message: err.to_string(),
}
})?;
info!(
"executing sparse Rust AOT build for problem_key={} in crate_dir={}",
problem_key,
build.written.crate_dir.display()
);
let executed = build.execute().map_err(|err| {
BvpBackendIntegrationError::AutomaticAotBuildFailed {
problem_key: problem_key.clone(),
message: err.to_string(),
}
})?;
if !executed.succeeded() {
error!(
"sparse Rust AOT build failed for problem_key={} with status={:?}",
problem_key, executed.status_code
);
return Err(BvpBackendIntegrationError::AutomaticAotBuildFailed {
problem_key,
message: format!(
"cargo build failed with status {:?}\nstdout:\n{}\nstderr:\n{}",
executed.status_code, executed.stdout, executed.stderr
),
});
}
let registered = registry
.register_materialized_build(manifest, &build)
.clone();
let runtime_registration = match selected.matrix_backend {
MatrixBackend::Banded => register_generated_banded_cdylib_backend(®istered),
_ => register_generated_sparse_cdylib_backend(®istered),
};
if let Err(err) = runtime_registration {
error!(
"{:?} Rust AOT build succeeded for problem_key={} but runtime cdylib registration failed: {}",
selected.matrix_backend, problem_key, err
);
}
}
AotCodegenBackend::C => {
let mut c_compile = match profile {
AotBuildProfile::Debug => CAotCompileConfig::dev_fastest(),
AotBuildProfile::Release => to_c_compile_config(&compile_config),
};
if let Some(compiler) = aot_c_compiler {
c_compile = c_compile.with_compiler(compiler);
}
let request = c_sparse_aot_build_request(bundle, &problem_key, profile, c_compile);
info!(
"materializing sparse C AOT library for problem_key={} with build_profile={:?}",
problem_key, profile
);
let build = request.materialize().map_err(|err| {
BvpBackendIntegrationError::AutomaticAotBuildFailed {
problem_key: problem_key.clone(),
message: err.to_string(),
}
})?;
info!(
"executing sparse C AOT build for problem_key={} in library_dir={}",
problem_key,
build.written.library_dir.display()
);
let executed = build.execute().map_err(|err| {
BvpBackendIntegrationError::AutomaticAotBuildFailed {
problem_key: problem_key.clone(),
message: err.to_string(),
}
})?;
if !executed.succeeded() {
error!(
"sparse C AOT build failed for problem_key={} with status={:?}",
problem_key, executed.status_code
);
return Err(BvpBackendIntegrationError::AutomaticAotBuildFailed {
problem_key,
message: format!(
"C build failed with status {:?}\nstdout:\n{}\nstderr:\n{}",
executed.status_code, executed.stdout, executed.stderr
),
});
}
let registered = register_c_build_in_registry(&mut registry, manifest, &build).clone();
let runtime_registration = match selected.matrix_backend {
MatrixBackend::Banded => register_generated_c_banded_backend(®istered),
_ => register_generated_c_sparse_backend(®istered),
};
if let Err(err) = runtime_registration {
error!(
"{:?} C AOT build succeeded for problem_key={} but runtime registration failed: {}",
selected.matrix_backend, problem_key, err
);
}
}
AotCodegenBackend::Zig => {
let request = zig_sparse_aot_build_request(bundle, &problem_key, profile);
info!(
"materializing sparse Zig AOT library for problem_key={} with build_profile={:?}",
problem_key, profile
);
let build = request.materialize().map_err(|err| {
BvpBackendIntegrationError::AutomaticAotBuildFailed {
problem_key: problem_key.clone(),
message: err.to_string(),
}
})?;
info!(
"executing sparse Zig AOT build for problem_key={} in library_dir={}",
problem_key,
build.written.library_dir.display()
);
let executed = build.execute().map_err(|err| {
BvpBackendIntegrationError::AutomaticAotBuildFailed {
problem_key: problem_key.clone(),
message: err.to_string(),
}
})?;
if !executed.succeeded() {
error!(
"sparse Zig AOT build failed for problem_key={} with status={:?}",
problem_key, executed.status_code
);
return Err(BvpBackendIntegrationError::AutomaticAotBuildFailed {
problem_key,
message: format!(
"Zig build failed with status {:?}\nstdout:\n{}\nstderr:\n{}",
executed.status_code, executed.stdout, executed.stderr
),
});
}
let registered =
register_zig_build_in_registry(&mut registry, manifest, &build).clone();
let runtime_registration = match selected.matrix_backend {
MatrixBackend::Banded => register_generated_zig_banded_backend(®istered),
_ => register_generated_zig_sparse_backend(®istered),
};
if let Err(err) = runtime_registration {
error!(
"{:?} Zig AOT build succeeded for problem_key={} but runtime registration failed: {}",
selected.matrix_backend, problem_key, err
);
}
}
}
info!(
"sparse {:?} AOT build succeeded for problem_key={} with profile={:?}",
aot_codegen_backend, problem_key, profile
);
Ok(AotResolver::new(registry))
}
fn try_link_sparse_runtime_from_resolution(
bundle: &BvpSparseSolverBundle,
resolver: Option<&AotResolver>,
aot_codegen_backend: AotCodegenBackend,
) -> bool {
let problem_key = bundle.execution.selected().problem_key();
if resolve_linked_sparse_backend(problem_key.as_str()).is_some() {
return true;
}
let resolved = bundle
.resolved_aot_artifact()
.cloned()
.or_else(|| resolver.map(|value| value.resolve_by_problem_key(problem_key.as_str())));
let Some(resolved) = resolved else {
return false;
};
if !resolved.is_compiled() {
return false;
}
match register_sparse_runtime_from_registered_artifact(
&resolved.registered,
bundle.execution.selected().matrix_backend,
aot_codegen_backend,
) {
Ok(_) => true,
Err(err) => {
error!(
"failed to register sparse generated {:?} runtime for problem_key={}: {}",
aot_codegen_backend, problem_key, err
);
false
}
}
}
fn enforce_build_policy_on_sparse_bundle(
bundle: BvpSparseSolverBundle,
resolver: Option<&AotResolver>,
build_policy: AotBuildPolicy,
compile_config: AotCompileConfig,
aot_codegen_backend: AotCodegenBackend,
aot_c_compiler: Option<String>,
) -> Result<(BvpSparseSolverBundle, Option<AotResolver>), BvpBackendIntegrationError> {
let problem_key = bundle.execution.selected().problem_key();
let effective_backend = bundle.effective_backend();
info!(
"enforcing sparse AOT build policy {} for problem_key={} with effective_backend={:?}",
build_policy.as_str(),
problem_key,
effective_backend
);
match build_policy {
AotBuildPolicy::UseIfAvailable => {
let runtime_available = bundle.is_runtime_callable()
|| (matches!(effective_backend, SelectedBackendKind::AotCompiled)
&& try_link_sparse_runtime_from_resolution(
&bundle,
resolver,
aot_codegen_backend,
));
if matches!(effective_backend, SelectedBackendKind::AotCompiled) && !runtime_available {
error!(
"compiled sparse AOT backend resolved for problem_key={} but runtime callbacks are unavailable",
problem_key
);
Err(BvpBackendIntegrationError::CompiledAotRuntimeUnavailable { problem_key })
} else {
info!(
"reusing available sparse backend for problem_key={} under UseIfAvailable",
problem_key
);
Ok((bundle, None))
}
}
AotBuildPolicy::RequirePrebuilt => {
let runtime_available = bundle.is_runtime_callable()
|| (matches!(effective_backend, SelectedBackendKind::AotCompiled)
&& try_link_sparse_runtime_from_resolution(
&bundle,
resolver,
aot_codegen_backend,
));
if !matches!(effective_backend, SelectedBackendKind::AotCompiled) {
error!(
"RequirePrebuilt requested for problem_key={} but compiled backend is not available: {:?}",
problem_key, effective_backend
);
Err(
BvpBackendIntegrationError::CompiledAotRequiredButUnavailable {
problem_key,
effective_backend,
},
)
} else if !runtime_available {
error!(
"RequirePrebuilt succeeded in resolution for problem_key={} but runtime callbacks are unavailable",
problem_key
);
Err(BvpBackendIntegrationError::CompiledAotRuntimeUnavailable { problem_key })
} else {
info!(
"using prebuilt sparse AOT backend for problem_key={}",
problem_key
);
Ok((bundle, None))
}
}
AotBuildPolicy::BuildIfMissing { .. } => {
let runtime_available = bundle.is_runtime_callable()
|| (matches!(effective_backend, SelectedBackendKind::AotCompiled)
&& try_link_sparse_runtime_from_resolution(
&bundle,
resolver,
aot_codegen_backend,
));
if matches!(effective_backend, SelectedBackendKind::AotCompiled) && runtime_available {
info!(
"compiled sparse AOT backend already callable for problem_key={}, skipping build",
problem_key
);
Ok((bundle, None))
} else {
let profile = match build_policy {
AotBuildPolicy::BuildIfMissing { profile } => profile,
_ => unreachable!(),
};
let _ = unregister_linked_sparse_backend(problem_key.as_str());
info!(
"compiled sparse AOT backend missing or not callable for problem_key={}, building with profile={:?}",
problem_key, profile
);
let updated_resolver = try_materialize_and_build_sparse_aot_bundle(
&bundle,
resolver,
profile,
compile_config.clone(),
aot_codegen_backend,
aot_c_compiler.clone(),
)?;
if bundle.is_runtime_callable()
|| try_link_sparse_runtime_from_resolution(
&bundle,
Some(&updated_resolver),
aot_codegen_backend,
)
{
info!(
"compiled sparse AOT backend became callable after BuildIfMissing for problem_key={}",
problem_key
);
Ok((bundle, Some(updated_resolver)))
} else {
error!(
"BuildIfMissing completed for problem_key={} but runtime callbacks are still unavailable",
problem_key
);
Err(BvpBackendIntegrationError::AutomaticAotBuildRequested { problem_key })
}
}
}
AotBuildPolicy::RebuildAlways { .. } => {
let profile = match build_policy {
AotBuildPolicy::RebuildAlways { profile } => profile,
_ => unreachable!(),
};
let _ = unregister_linked_sparse_backend(problem_key.as_str());
info!(
"forcing sparse AOT rebuild for problem_key={} with profile={:?}",
problem_key, profile
);
let updated_resolver = try_materialize_and_build_sparse_aot_bundle(
&bundle,
resolver,
profile,
compile_config,
aot_codegen_backend,
aot_c_compiler,
)?;
if bundle.is_runtime_callable()
|| try_link_sparse_runtime_from_resolution(
&bundle,
Some(&updated_resolver),
aot_codegen_backend,
)
{
info!(
"compiled sparse AOT backend remains callable after forced rebuild for problem_key={}",
problem_key
);
Ok((bundle, Some(updated_resolver)))
} else {
error!(
"forced sparse AOT rebuild completed for problem_key={} but runtime callbacks are unavailable",
problem_key
);
Err(BvpBackendIntegrationError::AutomaticAotRebuildRequested { problem_key })
}
}
}
}
fn apply_execution_policy_to_sparse_bundle(
mut bundle: BvpSparseSolverBundle,
policy: AotExecutionPolicy,
) -> Result<BvpSparseSolverBundle, BvpBackendIntegrationError> {
let problem_key = bundle.execution.selected().problem_key();
info!(
"applying sparse AOT execution policy {} for problem_key={} with effective_backend={:?}",
policy.as_str(),
problem_key,
bundle.effective_backend()
);
match policy {
AotExecutionPolicy::Auto => {
if matches!(bundle.effective_backend(), SelectedBackendKind::AotCompiled) {
let auto_plan = bundle
.execution
.selected()
.prepared_problem
.auto_parallel_plan();
if let Some(config) = auto_plan.executor_config {
if !bundle.rebind_linked_runtime_callbacks(None, Some(config)) {
return Err(BvpBackendIntegrationError::CompiledAotRuntimeUnavailable {
problem_key,
});
}
info!(
"auto-selected sparse parallel runtime binding for problem_key={} with residual_jobs={:?}, sparse_jobs={:?}, residual_chunking={:?}, sparse_chunking={:?}, min_work_per_job={}, workers={}",
problem_key,
config.max_residual_jobs,
config.max_sparse_jobs,
auto_plan.residual_chunking,
auto_plan.sparse_chunking,
auto_plan.min_work_per_job,
auto_plan.workers
);
} else if !bundle.rebind_linked_runtime_callbacks(None, None) {
return Err(BvpBackendIntegrationError::CompiledAotRuntimeUnavailable {
problem_key,
});
} else {
info!(
"auto-selected sequential sparse runtime binding for problem_key={} with min_work_per_job={} and workers={}",
problem_key, auto_plan.min_work_per_job, auto_plan.workers
);
}
} else {
info!(
"keeping default sparse runtime callback binding for problem_key={}",
problem_key
);
}
}
AotExecutionPolicy::SequentialOnly => {
if matches!(bundle.effective_backend(), SelectedBackendKind::AotCompiled)
&& !bundle.rebind_linked_runtime_callbacks(None, None)
{
return Err(BvpBackendIntegrationError::CompiledAotRuntimeUnavailable {
problem_key,
});
}
info!(
"bound sparse runtime callbacks sequentially for problem_key={}",
problem_key
);
}
AotExecutionPolicy::Parallel(config) => {
if matches!(bundle.effective_backend(), SelectedBackendKind::AotCompiled)
&& !bundle.rebind_linked_runtime_callbacks(None, Some(config))
{
return Err(BvpBackendIntegrationError::CompiledAotRuntimeUnavailable {
problem_key,
});
}
info!(
"bound sparse runtime callbacks in parallel for problem_key={}",
problem_key
);
}
}
Ok(bundle)
}
pub fn damped_state_from_sparse_solver_bundle(
bundle: BvpSparseSolverBundle,
updated_resolver: Option<AotResolver>,
) -> DampedGeneratedSolverState {
let bounds_vec = bundle.bounds_vec.clone().unwrap_or_default();
let rel_tolerance_vec = bundle.rel_tolerance_vec.clone().unwrap_or_default();
let variable_string = bundle.variable_string.clone();
let bandwidth = bundle.bandwidth.unwrap_or((0, 0));
let bc_position_and_value = bundle.bc_position_and_value.clone();
let (fun, jac) = bundle
.into_runtime_callbacks()
.unwrap_or_else(|| panic!("BVP sparse solver bundle did not provide runtime callbacks"));
DampedGeneratedSolverState {
fun,
jac: Some(jac),
bounds_vec,
rel_tolerance_vec,
variable_string,
bandwidth,
bc_position_and_value,
updated_resolver,
}
}
pub fn damped_state_from_legacy_jacobian(
jacobian_instance: Jacobian,
) -> DampedGeneratedSolverState {
damped_state_from_legacy_solver_bundle(jacobian_instance.into_legacy_solver_bundle())
}
pub fn damped_state_from_legacy_solver_bundle(
bundle: BvpLegacySolverBundle,
) -> DampedGeneratedSolverState {
DampedGeneratedSolverState {
fun: bundle.residual_function,
jac: bundle.jacobian_function,
bounds_vec: bundle.bounds_vec.unwrap_or_default(),
rel_tolerance_vec: bundle.rel_tolerance_vec.unwrap_or_default(),
variable_string: bundle.variable_string,
bandwidth: bundle.bandwidth.unwrap_or((0, 0)),
bc_position_and_value: bundle.bc_position_and_value,
updated_resolver: None,
}
}
pub fn frozen_state_from_sparse_solver_bundle(
bundle: BvpSparseSolverBundle,
updated_resolver: Option<AotResolver>,
) -> FrozenGeneratedSolverState {
let variable_string = bundle.variable_string.clone();
let bandwidth = bundle.bandwidth.unwrap_or((0, 0));
let (fun, jac) = bundle.into_runtime_callbacks().unwrap_or_else(|| {
panic!("Frozen BVP sparse solver bundle did not provide runtime callbacks")
});
FrozenGeneratedSolverState {
fun,
jac: Some(jac),
variable_string,
bandwidth,
updated_resolver,
}
}
pub fn frozen_state_from_legacy_jacobian(
jacobian_instance: Jacobian,
) -> FrozenGeneratedSolverState {
frozen_state_from_legacy_solver_bundle(jacobian_instance.into_legacy_solver_bundle())
}
pub fn frozen_state_from_legacy_solver_bundle(
bundle: BvpLegacySolverBundle,
) -> FrozenGeneratedSolverState {
FrozenGeneratedSolverState {
fun: bundle.residual_function,
jac: bundle.jacobian_function,
variable_string: bundle.variable_string,
bandwidth: bundle.bandwidth.unwrap_or((0, 0)),
updated_resolver: None,
}
}
#[cfg(test)]
mod tests {
use super::{
AotBuildPolicy, AotChunkingPolicy, AotCompileConfig, AotExecutionPolicy,
BandedGeneratedBackendMode, DampedSolverBuildRequest, FrozenSolverBuildRequest,
GeneratedBackendConfig,
};
use crate::symbolic::codegen::codegen_aot_driver::AotCodegenBackend;
use crate::symbolic::codegen::codegen_aot_registry::AotRegistry;
use crate::symbolic::codegen::codegen_aot_resolution::AotResolver;
use crate::symbolic::codegen::codegen_aot_runtime_link::{
LinkedResidualChunk, LinkedSparseAotBackend, LinkedSparseJacobianChunk,
register_linked_sparse_backend, unregister_linked_sparse_backend,
};
use crate::symbolic::codegen::codegen_backend_selection::{
BackendSelectionPolicy, SelectedBackendKind,
};
use crate::symbolic::codegen::codegen_manifest::PreparedProblemManifest;
use crate::symbolic::codegen::codegen_orchestrator::{
ParallelExecutorConfig, ParallelFallbackPolicy,
};
use crate::symbolic::codegen::codegen_provider_api::PreparedProblem;
use crate::symbolic::codegen::rust_backend::codegen_aot_build::{
AotBuildProfile, AotBuildRequest,
};
use crate::symbolic::symbolic_engine::Expr;
use crate::symbolic::symbolic_functions_BVP::{
BvpBackendIntegrationError, BvpSymbolicAssemblyBackend, Jacobian,
};
use faer::Col;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
fn real_bvp_inputs() -> (
Vec<Expr>,
Vec<String>,
String,
HashMap<String, Vec<(usize, f64)>>,
) {
let eq_system = vec![Expr::parse_expression("y-z"), Expr::parse_expression("z^3")];
let values = vec!["y".to_string(), "z".to_string()];
let arg = "x".to_string();
let mut border_conditions = HashMap::new();
border_conditions.insert("y".to_string(), vec![(0, 0.0), (1, 0.0)]);
border_conditions.insert("z".to_string(), vec![(0, 1.0), (1, 1.0)]);
(eq_system, values, arg, border_conditions)
}
fn parameterized_bvp_inputs() -> (
Vec<Expr>,
Vec<String>,
String,
Vec<String>,
HashMap<String, Vec<(usize, f64)>>,
) {
let eq_system = vec![
Expr::parse_expression("a*(y-z)"),
Expr::parse_expression("a*z^3"),
];
let values = vec!["y".to_string(), "z".to_string()];
let arg = "x".to_string();
let params = vec!["a".to_string()];
let mut border_conditions = HashMap::new();
border_conditions.insert("y".to_string(), vec![(0, 0.0), (1, 0.0)]);
border_conditions.insert("z".to_string(), vec![(0, 1.0), (1, 1.0)]);
(eq_system, values, arg, params, border_conditions)
}
#[test]
fn sparse_build_if_missing_release_defaults_to_dev_fastest_compile_preset() {
let config = super::GeneratedBackendConfig::sparse_build_if_missing_release();
assert_eq!(
config.aot_build_policy,
super::AotBuildPolicy::BuildIfMissing {
profile: super::AotBuildProfile::Release,
}
);
assert_eq!(config.aot_compile_config, AotCompileConfig::dev_fastest());
assert_eq!(config.aot_codegen_backend, AotCodegenBackend::Rust);
}
fn baseline_sparse_residual(args: &Col<f64>) -> Vec<f64> {
let (eq_system, values, arg, border_conditions) = real_bvp_inputs();
let mut staged = Jacobian::new();
staged.discretization_system_BVP_par(
eq_system,
values,
arg,
0.0,
Some(6),
None,
None,
border_conditions,
None,
None,
"forward".to_string(),
);
staged.calc_jacobian_parallel_smart_optimized();
let prepared_bridge = staged.prepare_sparse_aot_problem(
"eval_bvp_residual",
"eval_bvp_sparse_values",
crate::symbolic::codegen::codegen_runtime_api::recommended_residual_chunking_for_parallelism(
staged.vector_of_functions.len(),
4,
),
crate::symbolic::codegen::codegen_runtime_api::recommended_row_chunking_for_parallelism(
staged.vector_of_functions.len(),
4,
),
);
let input_names = prepared_bridge
.variable_names
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>();
let args_vec = args.iter().copied().collect::<Vec<_>>();
prepared_bridge
.residuals
.iter()
.map(|expr| expr.lambdify_borrowed_thread_safe(&input_names)(&args_vec))
.collect()
}
fn unique_test_artifact_dir(problem_key: &str) -> PathBuf {
let sanitized = problem_key
.chars()
.map(|ch| match ch {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => ch,
_ => '_',
})
.collect::<String>();
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock should be after unix epoch")
.as_nanos();
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("target")
.join("test-artifacts")
.join("generated-solver-handoff")
.join(format!("{}-{}-{}", sanitized, std::process::id(), nonce))
}
fn register_offset_linked_backend(
offset_residual: f64,
offset_jacobian: f64,
) -> (AotResolver, String) {
register_offset_linked_backend_with_chunk_offsets(
offset_residual,
offset_jacobian,
offset_residual,
offset_jacobian,
)
}
fn register_offset_linked_backend_with_chunk_offsets(
offset_residual: f64,
offset_jacobian: f64,
chunk_offset_residual: f64,
chunk_offset_jacobian: f64,
) -> (AotResolver, String) {
let (eq_system, values, arg, border_conditions) = real_bvp_inputs();
let mut staged = Jacobian::new();
staged.discretization_system_BVP_par(
eq_system,
values,
arg,
0.0,
Some(6),
None,
None,
border_conditions,
None,
None,
"forward".to_string(),
);
staged.calc_jacobian_parallel_smart_optimized();
let prepared_bridge = staged.prepare_sparse_aot_problem(
"eval_bvp_residual",
"eval_bvp_sparse_values",
crate::symbolic::codegen::codegen_runtime_api::recommended_residual_chunking_for_parallelism(
staged.vector_of_functions.len(),
4,
),
crate::symbolic::codegen::codegen_runtime_api::recommended_row_chunking_for_parallelism(
staged.vector_of_functions.len(),
4,
),
);
let problem_key = prepared_bridge.problem_key();
let input_name_strings = prepared_bridge
.variable_names
.iter()
.cloned()
.collect::<Vec<_>>();
let residual_input_name_strings = input_name_strings.clone();
let jacobian_input_name_strings = input_name_strings.clone();
let residual_exprs = prepared_bridge.residuals.clone();
let sparse_exprs = prepared_bridge.sparse_entries.clone();
let residual_len = prepared_bridge.shape.0;
let shape = prepared_bridge.shape;
let nnz = sparse_exprs.len();
let prepared_sparse = prepared_bridge.as_prepared_problem();
let residual_chunks = prepared_sparse
.residual_plan
.chunks
.iter()
.map(|chunk| {
let input_name_strings = input_name_strings.clone();
let chunk_exprs = chunk.residuals.iter().cloned().collect::<Vec<_>>();
LinkedResidualChunk::new(
chunk.output_offset,
chunk.residuals.len(),
Arc::new(move |args, out| {
let input_names = input_name_strings
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>();
for (slot, expr) in out.iter_mut().zip(chunk_exprs.iter()) {
*slot = expr.lambdify_borrowed_thread_safe(&input_names)(args)
+ chunk_offset_residual;
}
}),
)
})
.collect::<Vec<_>>();
let jacobian_value_chunks = prepared_sparse
.jacobian_plan
.chunks
.iter()
.map(|chunk| {
let input_name_strings = input_name_strings.clone();
let chunk_entries = chunk
.entries
.iter()
.map(|entry| (entry.row, entry.col, entry.expr.clone()))
.collect::<Vec<_>>();
LinkedSparseJacobianChunk::new(
chunk.value_offset,
chunk.entries.len(),
Arc::new(move |args, out| {
let input_names = input_name_strings
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>();
for (slot, (_, _, expr)) in out.iter_mut().zip(chunk_entries.iter()) {
*slot = expr.lambdify_borrowed_thread_safe(&input_names)(args)
+ chunk_offset_jacobian;
}
}),
)
})
.collect::<Vec<_>>();
register_linked_sparse_backend(
LinkedSparseAotBackend::new(
problem_key.clone(),
residual_len,
shape,
nnz,
Arc::new(move |args, out| {
let input_names = residual_input_name_strings
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>();
for (slot, expr) in out.iter_mut().zip(residual_exprs.iter()) {
*slot = expr.lambdify_borrowed_thread_safe(&input_names)(args)
+ offset_residual;
}
}),
Arc::new(move |args, out| {
let input_names = jacobian_input_name_strings
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>();
for (slot, (_, _, expr)) in out.iter_mut().zip(sparse_exprs.iter()) {
*slot = expr.lambdify_borrowed_thread_safe(&input_names)(args)
+ offset_jacobian;
}
}),
)
.with_chunked_evaluators(residual_chunks, jacobian_value_chunks),
);
let prepared = PreparedProblem::sparse(prepared_bridge.as_prepared_problem());
let manifest = PreparedProblemManifest::from(&prepared);
let dir = unique_test_artifact_dir(&problem_key);
fs::create_dir_all(&dir).expect("workspace test dir should be creatable");
let build = AotBuildRequest::new(
prepared_bridge.generated_aot_crate(
"generated_bvp_solver_handoff_fixture",
"generated_bvp_solver_handoff_module",
),
dir.as_path(),
AotBuildProfile::Release,
)
.materialize()
.expect("build request should materialize");
fs::create_dir_all(&build.artifact_dir).expect("artifact dir should be creatable");
fs::write(&build.expected_rlib, b"fake rlib").expect("expected rlib should be writable");
let mut registry = AotRegistry::new();
registry.register_materialized_build(manifest, &build);
(AotResolver::new(registry), problem_key)
}
fn register_parameterized_linked_backend() -> (AotResolver, String) {
let (eq_system, values, arg, params, border_conditions) = parameterized_bvp_inputs();
let param_refs = params.iter().map(|name| name.as_str()).collect::<Vec<_>>();
let mut staged = Jacobian::new();
staged.set_params(Some(param_refs.as_slice()));
staged.discretization_system_BVP_par(
eq_system,
values,
arg,
0.0,
Some(6),
None,
None,
border_conditions,
None,
None,
"forward".to_string(),
);
staged.calc_jacobian_parallel_smart_optimized();
let prepared_bridge = staged.prepare_sparse_aot_problem(
"eval_bvp_residual",
"eval_bvp_sparse_values",
crate::symbolic::codegen::codegen_runtime_api::recommended_residual_chunking_for_parallelism(
staged.vector_of_functions.len(),
4,
),
crate::symbolic::codegen::codegen_runtime_api::recommended_row_chunking_for_parallelism(
staged.vector_of_functions.len(),
4,
),
);
let problem_key = prepared_bridge.problem_key();
let input_name_strings = prepared_bridge
.param_names
.iter()
.chain(prepared_bridge.variable_names.iter())
.cloned()
.collect::<Vec<_>>();
let residual_input_name_strings = input_name_strings.clone();
let jacobian_input_name_strings = input_name_strings.clone();
let residual_exprs = prepared_bridge.residuals.clone();
let sparse_exprs = prepared_bridge.sparse_entries.clone();
let residual_len = prepared_bridge.shape.0;
let shape = prepared_bridge.shape;
let nnz = sparse_exprs.len();
register_linked_sparse_backend(LinkedSparseAotBackend::new(
problem_key.clone(),
residual_len,
shape,
nnz,
Arc::new(move |args, out| {
let input_names = residual_input_name_strings
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>();
for (slot, expr) in out.iter_mut().zip(residual_exprs.iter()) {
*slot = expr.lambdify_borrowed_thread_safe(&input_names)(args);
}
}),
Arc::new(move |args, out| {
let input_names = jacobian_input_name_strings
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>();
for (slot, (_, _, expr)) in out.iter_mut().zip(sparse_exprs.iter()) {
*slot = expr.lambdify_borrowed_thread_safe(&input_names)(args);
}
}),
));
let prepared = PreparedProblem::sparse(prepared_bridge.as_prepared_problem());
let manifest = PreparedProblemManifest::from(&prepared);
let dir = unique_test_artifact_dir(&problem_key);
fs::create_dir_all(&dir).expect("workspace test dir should be creatable");
let build = AotBuildRequest::new(
prepared_bridge.generated_aot_crate(
"generated_bvp_solver_parameterized_fixture",
"generated_bvp_solver_parameterized_module",
),
dir.as_path(),
AotBuildProfile::Release,
)
.materialize()
.expect("parameterized build request should materialize");
fs::create_dir_all(&build.artifact_dir).expect("artifact dir should be creatable");
fs::write(&build.expected_rlib, b"fake rlib").expect("expected rlib should be writable");
let mut registry = AotRegistry::new();
registry.register_materialized_build(manifest, &build);
(AotResolver::new(registry), problem_key)
}
#[test]
fn damped_solver_handoff_prefers_callable_linked_aot_backend() {
let (resolver, problem_key) = register_offset_linked_backend(100.0, 200.0);
let (eq_system, values, arg, border_conditions) = real_bvp_inputs();
let mut probe = Jacobian::new();
let execution = probe.generate_BVP_with_backend_selection(
eq_system.clone(),
values.clone(),
arg.clone(),
None,
0.0,
None,
Some(6),
None,
None,
border_conditions.clone(),
None,
None,
"forward".to_string(),
"Sparse".to_string(),
Some((2, 2)),
BackendSelectionPolicy::PreferAotThenLambdify,
Some(&resolver),
);
assert_eq!(
execution.selected().prepared_problem.problem_key(),
problem_key
);
assert_eq!(
execution.selected().effective_backend,
crate::symbolic::codegen::codegen_backend_selection::SelectedBackendKind::AotCompiled
);
let request = DampedSolverBuildRequest {
eq_system,
values,
arg,
param_names: None,
param_values: None,
t0: 0.0,
n_steps: Some(6),
h: None,
mesh: None,
border_conditions,
bounds: None,
rel_tolerance: None,
scheme: "forward".to_string(),
method: "Sparse".to_string(),
bandwidth: Some((2, 2)),
backend_policy: BackendSelectionPolicy::PreferAotThenLambdify,
resolver: Some(resolver.clone()),
aot_execution_policy: AotExecutionPolicy::Auto,
aot_build_policy: AotBuildPolicy::UseIfAvailable,
aot_compile_config: AotCompileConfig::default(),
aot_codegen_backend: AotCodegenBackend::Rust,
aot_c_compiler: None,
aot_chunking_policy: AotChunkingPolicy::default(),
symbolic_assembly_backend: BvpSymbolicAssemblyBackend::ExprLegacy,
matrix_backend_override: None,
banded_linear_solver_config: crate::somelinalg::banded::LinearSolverConfig::default(),
};
let state = request.generate().expect("damped handoff should build");
let y = Col::from_fn(state.variable_string.len(), |index| {
0.2 + index as f64 * 0.01
});
let residual = state.fun.call(0.0, &y).to_DVectorType();
let baseline_residual = baseline_sparse_residual(&y);
for (actual, expected) in residual.iter().zip(baseline_residual.iter()) {
assert!((actual - (expected + 100.0)).abs() < 1e-10);
}
assert!(state.jac.is_some(), "jacobian callback should be present");
unregister_linked_sparse_backend(&problem_key);
}
#[test]
fn frozen_solver_handoff_prefers_callable_linked_aot_backend() {
let (resolver, problem_key) = register_offset_linked_backend(50.0, 75.0);
let (eq_system, values, arg, border_conditions) = real_bvp_inputs();
let mut probe = Jacobian::new();
let execution = probe.generate_BVP_with_backend_selection(
eq_system.clone(),
values.clone(),
arg.clone(),
None,
0.0,
None,
Some(6),
None,
None,
border_conditions.clone(),
None,
None,
"forward".to_string(),
"Sparse".to_string(),
Some((2, 2)),
BackendSelectionPolicy::PreferAotThenLambdify,
Some(&resolver),
);
assert_eq!(
execution.selected().prepared_problem.problem_key(),
problem_key
);
assert_eq!(
execution.selected().effective_backend,
crate::symbolic::codegen::codegen_backend_selection::SelectedBackendKind::AotCompiled
);
let request = FrozenSolverBuildRequest {
eq_system,
values,
arg,
param_names: None,
param_values: None,
t0: 0.0,
n_steps: Some(6),
h: None,
mesh: None,
border_conditions,
scheme: "forward".to_string(),
method: "Sparse".to_string(),
bandwidth: Some((2, 2)),
backend_policy: BackendSelectionPolicy::PreferAotThenLambdify,
resolver: Some(resolver.clone()),
aot_execution_policy: AotExecutionPolicy::Auto,
aot_build_policy: AotBuildPolicy::UseIfAvailable,
aot_compile_config: AotCompileConfig::default(),
aot_codegen_backend: AotCodegenBackend::Rust,
aot_c_compiler: None,
aot_chunking_policy: AotChunkingPolicy::default(),
symbolic_assembly_backend: BvpSymbolicAssemblyBackend::ExprLegacy,
matrix_backend_override: None,
banded_linear_solver_config: crate::somelinalg::banded::LinearSolverConfig::default(),
};
let state = request.generate().expect("frozen handoff should build");
let y = Col::from_fn(state.variable_string.len(), |index| {
0.3 + index as f64 * 0.02
});
let residual = state.fun.call(0.0, &y).to_DVectorType();
let baseline_residual = baseline_sparse_residual(&y);
for (actual, expected) in residual.iter().zip(baseline_residual.iter()) {
assert!((actual - (expected + 50.0)).abs() < 1e-10);
}
assert!(state.jac.is_some(), "jacobian callback should be present");
unregister_linked_sparse_backend(&problem_key);
}
#[test]
fn damped_solver_handoff_parallel_policy_uses_chunked_linked_backend_callbacks() {
let (resolver, problem_key) =
register_offset_linked_backend_with_chunk_offsets(100.0, 200.0, 300.0, 400.0);
let (eq_system, values, arg, border_conditions) = real_bvp_inputs();
let request = DampedSolverBuildRequest {
eq_system,
values,
arg,
param_names: None,
param_values: None,
t0: 0.0,
n_steps: Some(6),
h: None,
mesh: None,
border_conditions,
bounds: None,
rel_tolerance: None,
scheme: "forward".to_string(),
method: "Sparse".to_string(),
bandwidth: Some((2, 2)),
backend_policy: BackendSelectionPolicy::PreferAotThenLambdify,
resolver: Some(resolver),
aot_execution_policy: AotExecutionPolicy::Parallel(ParallelExecutorConfig {
jobs_per_worker: 1,
max_residual_jobs: Some(2),
max_sparse_jobs: Some(2),
fallback_policy: ParallelFallbackPolicy::Never,
}),
aot_build_policy: AotBuildPolicy::UseIfAvailable,
aot_compile_config: AotCompileConfig::default(),
aot_codegen_backend: AotCodegenBackend::Rust,
aot_c_compiler: None,
aot_chunking_policy: AotChunkingPolicy::default(),
symbolic_assembly_backend: BvpSymbolicAssemblyBackend::ExprLegacy,
matrix_backend_override: None,
banded_linear_solver_config: crate::somelinalg::banded::LinearSolverConfig::default(),
};
let state = request
.generate()
.expect("parallel damped handoff should build");
let y = Col::from_fn(state.variable_string.len(), |index| {
0.2 + index as f64 * 0.01
});
let residual = state.fun.call(0.0, &y).to_DVectorType();
let baseline_residual = baseline_sparse_residual(&y);
for (actual, expected) in residual.iter().zip(baseline_residual.iter()) {
assert!((actual - (expected + 300.0)).abs() < 1e-10);
}
unregister_linked_sparse_backend(&problem_key);
}
#[test]
fn require_prebuilt_errors_when_compiled_backend_is_missing() {
let (eq_system, values, arg, border_conditions) = real_bvp_inputs();
let request = DampedSolverBuildRequest {
eq_system,
values,
arg,
param_names: None,
param_values: None,
t0: 0.0,
n_steps: Some(6),
h: None,
mesh: None,
border_conditions,
bounds: None,
rel_tolerance: None,
scheme: "forward".to_string(),
method: "Sparse".to_string(),
bandwidth: Some((2, 2)),
backend_policy: BackendSelectionPolicy::PreferAotThenLambdify,
resolver: None,
aot_execution_policy: AotExecutionPolicy::Auto,
aot_build_policy: AotBuildPolicy::RequirePrebuilt,
aot_compile_config: AotCompileConfig::default(),
aot_codegen_backend: AotCodegenBackend::Rust,
aot_c_compiler: None,
aot_chunking_policy: AotChunkingPolicy::default(),
symbolic_assembly_backend: BvpSymbolicAssemblyBackend::ExprLegacy,
matrix_backend_override: None,
banded_linear_solver_config: crate::somelinalg::banded::LinearSolverConfig::default(),
};
let err = request
.generate()
.err()
.expect("require-prebuilt should reject missing compiled AOT");
assert!(matches!(
err,
BvpBackendIntegrationError::CompiledAotRequiredButUnavailable {
effective_backend: SelectedBackendKind::AotMissing,
..
}
));
}
#[test]
fn generated_backend_config_can_select_non_rust_codegen_backend() {
let config = super::GeneratedBackendConfig::sparse_build_if_missing_release()
.with_aot_codegen_backend(AotCodegenBackend::C);
assert_eq!(config.aot_codegen_backend, AotCodegenBackend::C);
assert_eq!(
config.aot_build_policy,
AotBuildPolicy::BuildIfMissing {
profile: super::AotBuildProfile::Release,
}
);
}
#[test]
fn banded_generated_backend_defaults_select_faithful_lapack_without_refinement() {
let config = GeneratedBackendConfig::from_banded_mode(BandedGeneratedBackendMode::Defaults);
assert_eq!(
config.matrix_backend_override,
Some(crate::symbolic::codegen::codegen_provider_api::MatrixBackend::Banded)
);
assert_eq!(
config.banded_linear_solver_config.policy,
crate::somelinalg::banded::LinearSolverPolicy::ForceBanded
);
assert_eq!(
config
.banded_linear_solver_config
.iterative_refinement_steps,
0
);
assert_eq!(config.effective_method("Sparse"), "Banded");
assert_eq!(
config.effective_backend_policy("Banded"),
BackendSelectionPolicy::PreferAotThenLambdify
);
}
#[test]
fn banded_lambdify_preset_keeps_banded_matrix_backend() {
let config = GeneratedBackendConfig::from_banded_mode(BandedGeneratedBackendMode::Lambdify);
assert_eq!(config.effective_method("Sparse"), "Banded");
assert_eq!(
config.effective_backend_policy("Banded"),
BackendSelectionPolicy::LambdifyOnly
);
assert_eq!(
config
.banded_linear_solver_config
.iterative_refinement_steps,
0
);
}
#[test]
fn build_if_missing_materializes_generated_crate_and_keeps_callable_runtime() {
let (eq_system, values, arg, border_conditions) = real_bvp_inputs();
let mut staged = Jacobian::new();
staged.discretization_system_BVP_par(
eq_system.clone(),
values.clone(),
arg.clone(),
0.0,
Some(6),
None,
None,
border_conditions.clone(),
None,
None,
"forward".to_string(),
);
staged.calc_jacobian_parallel_smart_optimized();
let prepared_bridge = staged.prepare_sparse_aot_problem(
"eval_bvp_residual",
"eval_bvp_sparse_values",
crate::symbolic::codegen::codegen_runtime_api::recommended_residual_chunking_for_parallelism(
staged.vector_of_functions.len(),
4,
),
crate::symbolic::codegen::codegen_runtime_api::recommended_row_chunking_for_parallelism(
staged.vector_of_functions.len(),
4,
),
);
let problem_key = prepared_bridge.problem_key();
let request = FrozenSolverBuildRequest {
eq_system,
values,
arg,
param_names: None,
param_values: None,
t0: 0.0,
n_steps: Some(6),
h: None,
mesh: None,
border_conditions,
scheme: "forward".to_string(),
method: "Sparse".to_string(),
bandwidth: Some((2, 2)),
backend_policy: BackendSelectionPolicy::PreferAotThenLambdify,
resolver: None,
aot_execution_policy: AotExecutionPolicy::Auto,
aot_build_policy: AotBuildPolicy::BuildIfMissing {
profile:
crate::numerical::BVP_Damp::generated_solver_handoff::AotBuildProfile::Release,
},
aot_compile_config: AotCompileConfig::default(),
aot_codegen_backend: AotCodegenBackend::Rust,
aot_c_compiler: None,
aot_chunking_policy: AotChunkingPolicy::default(),
symbolic_assembly_backend: BvpSymbolicAssemblyBackend::ExprLegacy,
matrix_backend_override: None,
banded_linear_solver_config: crate::somelinalg::banded::LinearSolverConfig::default(),
};
let state = request
.generate()
.expect("build-if-missing should build artifact and keep runtime callable");
assert!(
state.jac.is_some(),
"jacobian callback should stay callable"
);
let updated_resolver = state
.updated_resolver
.as_ref()
.expect("build-if-missing should return an updated resolver snapshot");
let resolved = updated_resolver.resolve_by_problem_key(&problem_key);
assert!(
resolved.is_compiled(),
"updated resolver should see the freshly built compiled artifact"
);
let expected_rlib = &resolved.registered.expected_rlib;
assert!(
expected_rlib.exists(),
"build-if-missing should produce compiled rlib at {}",
expected_rlib.display()
);
}
#[test]
fn require_prebuilt_errors_when_artifact_is_registered_but_not_built() {
let (eq_system, values, arg, border_conditions) = real_bvp_inputs();
let mut staged = Jacobian::new();
staged.discretization_system_BVP_par(
eq_system.clone(),
values.clone(),
arg.clone(),
0.0,
Some(6),
None,
None,
border_conditions.clone(),
None,
None,
"forward".to_string(),
);
staged.calc_jacobian_parallel_smart_optimized();
let prepared_bridge = staged.prepare_sparse_aot_problem(
"eval_bvp_residual",
"eval_bvp_sparse_values",
crate::symbolic::codegen::codegen_runtime_api::recommended_residual_chunking_for_parallelism(
staged.vector_of_functions.len(),
4,
),
crate::symbolic::codegen::codegen_runtime_api::recommended_row_chunking_for_parallelism(
staged.vector_of_functions.len(),
4,
),
);
let prepared = PreparedProblem::sparse(prepared_bridge.as_prepared_problem());
let manifest = PreparedProblemManifest::from(&prepared);
let dir = unique_test_artifact_dir(&prepared_bridge.problem_key());
fs::create_dir_all(&dir).expect("workspace test dir should be creatable");
let build = AotBuildRequest::new(
prepared_bridge.generated_aot_crate(
"generated_bvp_solver_registered_only_fixture",
"generated_bvp_solver_registered_only_module",
),
dir.as_path(),
AotBuildProfile::Release,
)
.materialize()
.expect("build request should materialize");
let mut registry = AotRegistry::new();
registry.register_materialized_build(manifest, &build);
let resolver = AotResolver::new(registry);
let request = DampedSolverBuildRequest {
eq_system,
values,
arg,
param_names: None,
param_values: None,
t0: 0.0,
n_steps: Some(6),
h: None,
mesh: None,
border_conditions,
bounds: None,
rel_tolerance: None,
scheme: "forward".to_string(),
method: "Sparse".to_string(),
bandwidth: Some((2, 2)),
backend_policy: BackendSelectionPolicy::PreferAotThenLambdify,
resolver: Some(resolver),
aot_execution_policy: AotExecutionPolicy::Auto,
aot_build_policy: AotBuildPolicy::RequirePrebuilt,
aot_compile_config: AotCompileConfig::default(),
aot_codegen_backend: AotCodegenBackend::Rust,
aot_c_compiler: None,
aot_chunking_policy: AotChunkingPolicy::default(),
symbolic_assembly_backend: BvpSymbolicAssemblyBackend::ExprLegacy,
matrix_backend_override: None,
banded_linear_solver_config: crate::somelinalg::banded::LinearSolverConfig::default(),
};
let err = request
.generate()
.err()
.expect("require-prebuilt should reject not-built compiled AOT");
assert!(matches!(
err,
BvpBackendIntegrationError::CompiledAotRequiredButUnavailable {
effective_backend: SelectedBackendKind::AotRegisteredButNotBuilt,
..
}
));
}
#[test]
fn damped_solver_handoff_reuses_compiled_backend_when_only_param_values_change() {
let (resolver, problem_key) = register_parameterized_linked_backend();
let (eq_system, values, arg, params, border_conditions) = parameterized_bvp_inputs();
let param_refs = params.iter().map(|name| name.as_str()).collect::<Vec<_>>();
let mut probe_a = Jacobian::new();
probe_a.set_params(Some(param_refs.as_slice()));
probe_a.set_param_values(Some(vec![1.0]));
let execution_a = probe_a.generate_BVP_with_backend_selection(
eq_system.clone(),
values.clone(),
arg.clone(),
Some(param_refs.as_slice()),
0.0,
None,
Some(6),
None,
None,
border_conditions.clone(),
None,
None,
"forward".to_string(),
"Sparse".to_string(),
Some((2, 2)),
BackendSelectionPolicy::PreferAotThenLambdify,
Some(&resolver),
);
let mut probe_b = Jacobian::new();
probe_b.set_params(Some(param_refs.as_slice()));
probe_b.set_param_values(Some(vec![3.0]));
let execution_b = probe_b.generate_BVP_with_backend_selection(
eq_system.clone(),
values.clone(),
arg.clone(),
Some(param_refs.as_slice()),
0.0,
None,
Some(6),
None,
None,
border_conditions.clone(),
None,
None,
"forward".to_string(),
"Sparse".to_string(),
Some((2, 2)),
BackendSelectionPolicy::PreferAotThenLambdify,
Some(&resolver),
);
assert_eq!(
execution_a.selected().prepared_problem.problem_key(),
problem_key
);
assert_eq!(
execution_b.selected().prepared_problem.problem_key(),
problem_key
);
let request_a = DampedSolverBuildRequest {
eq_system: eq_system.clone(),
values: values.clone(),
arg: arg.clone(),
param_names: Some(params.clone()),
param_values: Some(vec![1.0]),
t0: 0.0,
n_steps: Some(6),
h: None,
mesh: None,
border_conditions: border_conditions.clone(),
bounds: None,
rel_tolerance: None,
scheme: "forward".to_string(),
method: "Sparse".to_string(),
bandwidth: Some((2, 2)),
backend_policy: BackendSelectionPolicy::PreferAotThenLambdify,
resolver: Some(resolver.clone()),
aot_execution_policy: AotExecutionPolicy::Auto,
aot_build_policy: AotBuildPolicy::RequirePrebuilt,
aot_compile_config: AotCompileConfig::default(),
aot_codegen_backend: AotCodegenBackend::Rust,
aot_c_compiler: None,
aot_chunking_policy: AotChunkingPolicy::default(),
symbolic_assembly_backend: BvpSymbolicAssemblyBackend::ExprLegacy,
matrix_backend_override: None,
banded_linear_solver_config: crate::somelinalg::banded::LinearSolverConfig::default(),
};
let request_b = DampedSolverBuildRequest {
eq_system,
values,
arg,
param_names: Some(params),
param_values: Some(vec![3.0]),
t0: 0.0,
n_steps: Some(6),
h: None,
mesh: None,
border_conditions,
bounds: None,
rel_tolerance: None,
scheme: "forward".to_string(),
method: "Sparse".to_string(),
bandwidth: Some((2, 2)),
backend_policy: BackendSelectionPolicy::PreferAotThenLambdify,
resolver: Some(resolver),
aot_execution_policy: AotExecutionPolicy::Auto,
aot_build_policy: AotBuildPolicy::RequirePrebuilt,
aot_compile_config: AotCompileConfig::default(),
aot_codegen_backend: AotCodegenBackend::Rust,
aot_c_compiler: None,
aot_chunking_policy: AotChunkingPolicy::default(),
symbolic_assembly_backend: BvpSymbolicAssemblyBackend::ExprLegacy,
matrix_backend_override: None,
banded_linear_solver_config: crate::somelinalg::banded::LinearSolverConfig::default(),
};
let state_a = request_a
.generate()
.expect("parameterized prebuilt backend should be callable for first param set");
let state_b = request_b
.generate()
.expect("parameterized prebuilt backend should be callable for second param set");
let y = Col::from_fn(state_a.variable_string.len(), |index| {
0.2 + index as f64 * 0.01
});
let residual_a = state_a.fun.call(0.0, &y).to_DVectorType();
let residual_b = state_b.fun.call(0.0, &y).to_DVectorType();
let max_diff = residual_a
.iter()
.zip(residual_b.iter())
.map(|(lhs, rhs)| (lhs - rhs).abs())
.fold(0.0_f64, f64::max);
assert!(
max_diff > 1e-8,
"changing only param_values should change residuals without rebuild"
);
unregister_linked_sparse_backend(&problem_key);
}
}