use crate::somelinalg::banded::{
BandedError, LinearSolverConfig, NodeMajorLayout, banded_assembly::BandedAssembly,
};
use crate::symbolic::View::atom::Atom;
use crate::symbolic::View::evaluate::{
FunctionMap, PreparedEvaluatorMetrics, PreparedVariableContext,
};
use crate::symbolic::View::jacobian::SparseAtomJacobianEntry;
use crate::symbolic::View::lambdify::lambdify_with_context_and_metrics;
use crate::symbolic::View::state::Symbol;
use crate::symbolic::bvp::parameter_binding::BvpParameterBindingHandle;
use crate::symbolic::bvp::telemetry::{
BvpDirectJacobianTelemetry, BvpDirectJacobianTelemetrySnapshot, BvpLambdifyExecutionPolicy,
BvpLambdifyTelemetryMode, flatten_lambdify_args,
};
use crate::symbolic::symbolic_engine::Expr;
use crate::wrap_symbol;
use rayon::prelude::*;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::Arc;
use std::time::Instant;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BandedJacobianChunking {
Diagonal,
EntryChunks,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BandedLinearSolverBackend {
BlockTridiagonalNative,
GeneralBandedNative,
FaerSparseFallback,
}
#[derive(Clone, Debug)]
pub struct BandedLambdifyConfig {
pub execution_policy: BvpLambdifyExecutionPolicy,
pub jacobian_chunking: BandedJacobianChunking,
pub linear_solver_backend: BandedLinearSolverBackend,
pub linear_solver_config: LinearSolverConfig,
pub structural_threshold: f64,
pub telemetry_mode: BvpLambdifyTelemetryMode,
}
impl Default for BandedLambdifyConfig {
fn default() -> Self {
Self {
execution_policy: BvpLambdifyExecutionPolicy::default(),
jacobian_chunking: BandedJacobianChunking::Diagonal,
linear_solver_backend: BandedLinearSolverBackend::BlockTridiagonalNative,
linear_solver_config: LinearSolverConfig::default(),
structural_threshold: 0.0,
telemetry_mode: BvpLambdifyTelemetryMode::Detailed,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BandedStructurePlan {
pub n_unknowns: usize,
pub n_equations: usize,
pub scalar_bandwidth: (usize, usize),
pub layout: NodeMajorLayout,
pub block_tridiagonal_compatible: bool,
}
#[derive(Clone, Default)]
pub struct DirectBandedProblem {
pub vector_of_functions: Vec<Expr>,
pub vector_of_variables: Vec<Expr>,
pub variable_string: Vec<String>,
pub parameters_string: Vec<String>,
pub parameter_values: Option<Vec<f64>>,
parameter_binding: Option<BvpParameterBindingHandle>,
pub symbolic_jacobian_sparse: Vec<(usize, usize, Expr)>,
pub atom_vector_of_functions: Option<Vec<Atom>>,
pub atom_symbolic_jacobian_sparse: Option<Vec<SparseAtomJacobianEntry>>,
pub bandwidth: Option<(usize, usize)>,
}
impl fmt::Debug for DirectBandedProblem {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("DirectBandedProblem")
.field("vector_of_functions_len", &self.vector_of_functions.len())
.field("vector_of_variables_len", &self.vector_of_variables.len())
.field("variable_string", &self.variable_string)
.field("parameters_string", &self.parameters_string)
.field("parameter_values", &self.parameter_values)
.field(
"symbolic_jacobian_sparse_len",
&self.symbolic_jacobian_sparse.len(),
)
.field(
"atom_vector_of_functions_len",
&self.atom_vector_of_functions.as_ref().map(Vec::len),
)
.field(
"atom_symbolic_jacobian_sparse_len",
&self.atom_symbolic_jacobian_sparse.as_ref().map(Vec::len),
)
.field("bandwidth", &self.bandwidth)
.finish()
}
}
impl DirectBandedProblem {
#[allow(clippy::too_many_arguments)]
pub fn new(
vector_of_functions: Vec<Expr>,
vector_of_variables: Vec<Expr>,
variable_string: Vec<String>,
parameters_string: Vec<String>,
parameter_values: Option<Vec<f64>>,
symbolic_jacobian_sparse: Vec<(usize, usize, Expr)>,
bandwidth: Option<(usize, usize)>,
) -> Self {
Self {
vector_of_functions,
vector_of_variables,
variable_string,
parameters_string,
parameter_values,
parameter_binding: None,
symbolic_jacobian_sparse,
atom_vector_of_functions: None,
atom_symbolic_jacobian_sparse: None,
bandwidth,
}
}
pub fn with_atom_view_data(
mut self,
residuals: Vec<Atom>,
jacobian: Vec<SparseAtomJacobianEntry>,
) -> Self {
self.atom_vector_of_functions = Some(residuals);
self.atom_symbolic_jacobian_sparse = Some(jacobian);
self
}
pub(crate) fn with_parameter_binding(mut self, binding: BvpParameterBindingHandle) -> Self {
self.parameter_binding = Some(binding);
self
}
#[inline]
fn uses_dynamic_parameter_binding(&self) -> bool {
self.parameter_binding.is_some()
}
#[inline]
fn parameter_values_for_callback(&self) -> Option<Arc<[f64]>> {
self.parameter_binding
.as_ref()
.and_then(BvpParameterBindingHandle::snapshot)
.or_else(|| self.parameter_values.clone().map(Arc::from))
}
}
type BandedScalarEvaluator = Box<dyn Fn(&[f64]) -> f64 + Send + Sync>;
#[derive(Clone, Copy, Debug, Default)]
struct BandedPreparedAtomMetrics {
evaluators: usize,
nodes: usize,
add_nodes: usize,
mul_nodes: usize,
powi_nodes: usize,
pow_nodes: usize,
builtin_nodes: usize,
custom_nodes: usize,
}
impl BandedPreparedAtomMetrics {
fn add(&mut self, metrics: PreparedEvaluatorMetrics) {
self.evaluators += 1;
self.nodes += metrics.nodes;
self.add_nodes += metrics.add_nodes;
self.mul_nodes += metrics.mul_nodes;
self.powi_nodes += metrics.powi_nodes;
self.pow_nodes += metrics.pow_nodes;
self.builtin_nodes += metrics.builtin_nodes;
self.custom_nodes += metrics.custom_nodes;
}
}
#[inline]
fn validate_callback_value(
stage: &'static str,
row: usize,
col: usize,
value: f64,
) -> Result<f64, BandedError> {
if value.is_finite() {
Ok(value)
} else {
Err(BandedError::NonFiniteCallbackValue {
stage,
row,
col,
value,
})
}
}
pub struct DirectBandedJacobianRuntime {
callback: Box<dyn Fn(&[f64]) -> Result<BandedAssembly, BandedError> + Send + Sync>,
telemetry: BvpDirectJacobianTelemetry,
}
impl DirectBandedJacobianRuntime {
pub fn callback(
&self,
) -> &(dyn Fn(&[f64]) -> Result<BandedAssembly, BandedError> + Send + Sync) {
&*self.callback
}
pub fn telemetry(&self) -> BvpDirectJacobianTelemetry {
self.telemetry.clone()
}
pub fn telemetry_snapshot(&self) -> BvpDirectJacobianTelemetrySnapshot {
self.telemetry.snapshot()
}
pub fn into_callback(
self,
) -> Box<dyn Fn(&[f64]) -> Result<BandedAssembly, BandedError> + Send + Sync> {
self.callback
}
}
struct CompiledBandedDiagonal {
evaluators: Vec<(usize, BandedScalarEvaluator)>,
}
struct CompiledBandedEntry {
row: usize,
col: usize,
evaluator: BandedScalarEvaluator,
atom_metrics: Option<PreparedEvaluatorMetrics>,
}
impl BandedStructurePlan {
#[inline]
pub fn n(&self) -> usize {
self.n_unknowns
}
#[inline]
pub fn n_nodes(&self) -> usize {
self.layout.n_nodes()
}
#[inline]
pub fn vars_per_node(&self) -> usize {
self.layout.vars_per_node()
}
}
fn base_variable_name(name: &str) -> &str {
name.rsplit_once('_').map_or(name, |(base, _)| base)
}
fn compile_banded_scalar_evaluator(expr: &Expr, argument_names: &[&str]) -> BandedScalarEvaluator {
Expr::lambdify_borrowed_thread_safe(expr, argument_names)
}
fn atom_variable_context(argument_names: &[String]) -> Arc<PreparedVariableContext> {
let symbols = argument_names
.iter()
.map(|name| Symbol::new(wrap_symbol!(name.as_str())))
.collect::<Vec<_>>();
Arc::new(PreparedVariableContext::new(symbols.as_slice()))
}
fn compile_atom_scalar_evaluator(
atom: &Atom,
context: &PreparedVariableContext,
function_map: &FunctionMap,
) -> (BandedScalarEvaluator, PreparedEvaluatorMetrics) {
lambdify_with_context_and_metrics(atom, context, function_map)
}
impl DirectBandedProblem {
pub fn infer_banded_structure_plan(&self) -> Result<BandedStructurePlan, BandedError> {
let n_unknowns = self
.atom_vector_of_functions
.as_ref()
.and_then(|_| self.atom_symbolic_jacobian_sparse.as_ref())
.map(|_| self.variable_string.len())
.unwrap_or(self.vector_of_variables.len());
let n_equations = self
.atom_vector_of_functions
.as_ref()
.map(Vec::len)
.unwrap_or(self.vector_of_functions.len());
if n_unknowns == 0 || n_equations == 0 || n_unknowns != n_equations {
return Err(BandedError::DimensionMismatch);
}
let scalar_bandwidth = self.bandwidth.ok_or(BandedError::DimensionMismatch)?;
if self.variable_string.is_empty() {
return Err(BandedError::DimensionMismatch);
}
let mut seen: HashSet<&str> = HashSet::new();
let mut vars_per_node = 0usize;
for name in &self.variable_string {
let inserted = seen.insert(base_variable_name(name));
if inserted {
vars_per_node += 1;
}
}
if vars_per_node == 0 || n_unknowns % vars_per_node != 0 {
return Err(BandedError::DimensionMismatch);
}
let n_nodes = n_unknowns / vars_per_node;
let layout = NodeMajorLayout::new(n_nodes, vars_per_node)?;
let max_scalar_half_bandwidth = vars_per_node
.checked_mul(2)
.and_then(|value| value.checked_sub(1))
.ok_or(BandedError::DimensionMismatch)?;
let block_tridiagonal_compatible = scalar_bandwidth.0 <= max_scalar_half_bandwidth
&& scalar_bandwidth.1 <= max_scalar_half_bandwidth;
Ok(BandedStructurePlan {
n_unknowns,
n_equations,
scalar_bandwidth,
layout,
block_tridiagonal_compatible,
})
}
pub fn allocate_banded_assembly_from_plan(
&self,
plan: &BandedStructurePlan,
) -> Result<BandedAssembly, BandedError> {
BandedAssembly::zeros(
plan.n_unknowns,
plan.scalar_bandwidth.0,
plan.scalar_bandwidth.1,
)
}
pub fn allocate_banded_assembly(&self) -> Result<BandedAssembly, BandedError> {
let plan = self.infer_banded_structure_plan()?;
self.allocate_banded_assembly_from_plan(&plan)
}
pub fn is_block_tridiagonal_candidate(&self) -> bool {
self.infer_banded_structure_plan()
.map(|plan| plan.block_tridiagonal_compatible)
.unwrap_or(false)
}
fn compile_banded_diagonal_plan(
&self,
plan: &BandedStructurePlan,
) -> Result<(Vec<CompiledBandedDiagonal>, BandedPreparedAtomMetrics), BandedError> {
let atom_native = self.atom_symbolic_jacobian_sparse.is_some();
let parameter_map = (!atom_native && !self.uses_dynamic_parameter_binding())
.then(|| self.parameter_substitution_map())
.flatten();
let argument_names = if atom_native {
self.atom_runtime_argument_names()?
} else if self.uses_dynamic_parameter_binding() {
self.flattened_runtime_argument_names()
} else {
self.runtime_argument_names(¶meter_map)?
};
let atom_context = atom_native.then(|| atom_variable_context(&argument_names));
let atom_function_map = atom_native.then(FunctionMap::new);
let argument_name_refs: Vec<&str> =
argument_names.iter().map(|name| name.as_str()).collect();
let mut diagonals: Vec<CompiledBandedDiagonal> =
Vec::with_capacity(plan.scalar_bandwidth.0 + plan.scalar_bandwidth.1 + 1);
for _offset in -(plan.scalar_bandwidth.0 as isize)..=(plan.scalar_bandwidth.1 as isize) {
diagonals.push(CompiledBandedDiagonal {
evaluators: Vec::new(),
});
}
let compiled_entries: Vec<(
usize,
usize,
BandedScalarEvaluator,
Option<PreparedEvaluatorMetrics>,
)> = if let Some(entries) = self.atom_symbolic_jacobian_sparse.as_ref() {
entries
.par_iter()
.filter_map(|entry| {
let offset = entry.col as isize - entry.row as isize;
if offset < -(plan.scalar_bandwidth.0 as isize)
|| offset > plan.scalar_bandwidth.1 as isize
{
return None;
}
let diag_index = (offset + plan.scalar_bandwidth.0 as isize) as usize;
let pos = if offset >= 0 { entry.row } else { entry.col };
let (evaluator, metrics) = compile_atom_scalar_evaluator(
&entry.value,
atom_context.as_ref().expect("Atom context must exist"),
atom_function_map
.as_ref()
.expect("Atom function map must exist"),
);
Some((diag_index, pos, evaluator, Some(metrics)))
})
.collect()
} else {
self.symbolic_jacobian_sparse
.par_iter()
.filter_map(|(row, col, expr)| {
let offset = *col as isize - *row as isize;
if offset < -(plan.scalar_bandwidth.0 as isize)
|| offset > plan.scalar_bandwidth.1 as isize
{
return None;
}
let diag_index = (offset + plan.scalar_bandwidth.0 as isize) as usize;
let pos = if offset >= 0 { *row } else { *col };
let evaluator = if let Some(ref map) = parameter_map {
let prepared_expr = expr.set_variable_from_map(map);
compile_banded_scalar_evaluator(&prepared_expr, &argument_name_refs)
} else {
compile_banded_scalar_evaluator(expr, &argument_name_refs)
};
Some((diag_index, pos, evaluator, None))
})
.collect()
};
let mut metrics = BandedPreparedAtomMetrics::default();
for (diag_index, pos, evaluator, evaluator_metrics) in compiled_entries {
if let Some(evaluator_metrics) = evaluator_metrics {
metrics.add(evaluator_metrics);
}
diagonals[diag_index].evaluators.push((pos, evaluator));
}
Ok((diagonals, metrics))
}
fn compile_banded_entry_plan(
&self,
plan: &BandedStructurePlan,
) -> Result<(Vec<CompiledBandedEntry>, BandedPreparedAtomMetrics), BandedError> {
let atom_native = self.atom_symbolic_jacobian_sparse.is_some();
let parameter_map = (!atom_native && !self.uses_dynamic_parameter_binding())
.then(|| self.parameter_substitution_map())
.flatten();
let argument_names = if atom_native {
self.atom_runtime_argument_names()?
} else if self.uses_dynamic_parameter_binding() {
self.flattened_runtime_argument_names()
} else {
self.runtime_argument_names(¶meter_map)?
};
let atom_context = atom_native.then(|| atom_variable_context(&argument_names));
let atom_function_map = atom_native.then(FunctionMap::new);
let argument_name_refs: Vec<&str> =
argument_names.iter().map(|name| name.as_str()).collect();
if let Some(entries) = self.atom_symbolic_jacobian_sparse.as_ref() {
let compiled: Vec<CompiledBandedEntry> = entries
.par_iter()
.filter_map(|entry| {
let offset = entry.col as isize - entry.row as isize;
if offset < -(plan.scalar_bandwidth.0 as isize)
|| offset > plan.scalar_bandwidth.1 as isize
{
return None;
}
let (evaluator, metrics) = compile_atom_scalar_evaluator(
&entry.value,
atom_context.as_ref().expect("Atom context must exist"),
atom_function_map
.as_ref()
.expect("Atom function map must exist"),
);
Some(CompiledBandedEntry {
row: entry.row,
col: entry.col,
evaluator,
atom_metrics: Some(metrics),
})
})
.collect();
let mut metrics = BandedPreparedAtomMetrics::default();
for entry in &compiled {
let offset = entry.col as isize - entry.row as isize;
if offset >= -(plan.scalar_bandwidth.0 as isize)
&& offset <= plan.scalar_bandwidth.1 as isize
{
if let Some(atom_metrics) = entry.atom_metrics {
metrics.add(atom_metrics);
}
}
}
return Ok((compiled, metrics));
}
Ok((
self.symbolic_jacobian_sparse
.par_iter()
.filter_map(|(row, col, expr)| {
let offset = *col as isize - *row as isize;
if offset < -(plan.scalar_bandwidth.0 as isize)
|| offset > plan.scalar_bandwidth.1 as isize
{
return None;
}
let evaluator = if let Some(ref map) = parameter_map {
let prepared_expr = expr.set_variable_from_map(map);
compile_banded_scalar_evaluator(&prepared_expr, &argument_name_refs)
} else {
compile_banded_scalar_evaluator(expr, &argument_name_refs)
};
Some(CompiledBandedEntry {
row: *row,
col: *col,
evaluator,
atom_metrics: None,
})
})
.collect(),
BandedPreparedAtomMetrics::default(),
))
}
pub(crate) fn generate_banded_jacobian_assembly_from_plan_parallel(
&self,
plan: &BandedStructurePlan,
config: &BandedLambdifyConfig,
) -> Result<Box<dyn Fn(&[f64]) -> Result<BandedAssembly, BandedError> + Send + Sync>, BandedError>
{
Ok(self
.generate_banded_jacobian_runtime_from_plan_parallel(plan, config)?
.into_callback())
}
pub fn generate_banded_jacobian_runtime_from_plan_parallel(
&self,
plan: &BandedStructurePlan,
config: &BandedLambdifyConfig,
) -> Result<DirectBandedJacobianRuntime, BandedError> {
let n = plan.n_unknowns;
let kl = plan.scalar_bandwidth.0;
let ku = plan.scalar_bandwidth.1;
let threshold = config.structural_threshold.abs();
let atom_native = self.atom_symbolic_jacobian_sparse.is_some();
let parameter_binding = self.parameter_binding.clone();
let parameter_values = self.parameter_values_for_callback();
let parameter_count = self.parameters_string.len();
let telemetry = match config.telemetry_mode {
BvpLambdifyTelemetryMode::Off => BvpDirectJacobianTelemetry::disabled(),
BvpLambdifyTelemetryMode::Counters => BvpDirectJacobianTelemetry::counters(),
BvpLambdifyTelemetryMode::Detailed => BvpDirectJacobianTelemetry::new(),
};
let telemetry_for_callback = telemetry.clone();
let telemetry_enabled = telemetry.is_enabled();
let telemetry_timing_enabled = telemetry.timing_enabled();
let execution_policy = config.execution_policy;
enum CompiledPlan {
Diagonal(Vec<CompiledBandedDiagonal>),
Entry(Vec<CompiledBandedEntry>),
}
let (compiled, prepared_atom_metrics) = match config.jacobian_chunking {
BandedJacobianChunking::Diagonal => {
let (plan, metrics) = self.compile_banded_diagonal_plan(&plan)?;
(CompiledPlan::Diagonal(plan), metrics)
}
BandedJacobianChunking::EntryChunks => {
let (plan, metrics) = self.compile_banded_entry_plan(&plan)?;
(CompiledPlan::Entry(plan), metrics)
}
};
telemetry.record_prepared_atom_metrics(
prepared_atom_metrics.evaluators,
prepared_atom_metrics.nodes,
prepared_atom_metrics.add_nodes,
prepared_atom_metrics.mul_nodes,
prepared_atom_metrics.powi_nodes,
prepared_atom_metrics.pow_nodes,
prepared_atom_metrics.builtin_nodes,
prepared_atom_metrics.custom_nodes,
);
let work_items = match &compiled {
CompiledPlan::Diagonal(diagonals) => diagonals
.iter()
.map(|diagonal| diagonal.evaluators.len())
.sum(),
CompiledPlan::Entry(entries) => entries.len(),
};
let storage_writes = match &compiled {
CompiledPlan::Diagonal(diagonals) => diagonals
.iter()
.map(|diagonal| diagonal.evaluators.len())
.sum(),
CompiledPlan::Entry(entries) => entries.len(),
};
let parallel_tasks = match &compiled {
CompiledPlan::Diagonal(diagonals) => diagonals
.iter()
.map(|diagonal| {
BvpLambdifyExecutionPolicy::auto_task_count(diagonal.evaluators.len())
})
.sum::<usize>(),
CompiledPlan::Entry(entries) => entries.len(),
};
let callback = Box::new(
move |unknowns: &[f64]| -> Result<BandedAssembly, BandedError> {
let begin = telemetry_timing_enabled.then(Instant::now);
if unknowns.len() != n {
if telemetry_enabled {
telemetry_for_callback.record_error();
}
return Err(BandedError::DimensionMismatch);
}
let argument_prepare_begin = telemetry_timing_enabled.then(Instant::now);
let uses_parameter_abi = atom_native || parameter_binding.is_some();
let needs_parameter_prefix = uses_parameter_abi && parameter_count > 0;
let runtime_args: Cow<'_, [f64]> = if needs_parameter_prefix {
Cow::Owned(flatten_lambdify_args(
parameter_binding
.as_ref()
.and_then(BvpParameterBindingHandle::snapshot)
.as_deref()
.or(parameter_values.as_deref()),
unknowns,
))
} else {
Cow::Borrowed(unknowns)
};
let argument_prepare_elapsed = argument_prepare_begin
.map(|started| started.elapsed())
.unwrap_or_default();
let parallel =
execution_policy.should_parallel_with_tasks(work_items, parallel_tasks);
let result = (|| -> Result<BandedAssembly, BandedError> {
let assembly_alloc_begin = telemetry_timing_enabled.then(Instant::now);
let mut asm = BandedAssembly::zeros(n, kl, ku)?;
let assembly_alloc_elapsed = assembly_alloc_begin
.map(|started| started.elapsed())
.unwrap_or_default();
let evaluator_begin = telemetry_timing_enabled.then(Instant::now);
let mut storage_write_elapsed = std::time::Duration::ZERO;
match &compiled {
CompiledPlan::Diagonal(compiled_diagonals) => {
let evaluate_diagonal =
|(diag_index, (diag_values, compiled_diag)): (
usize,
(&mut Vec<f64>, &CompiledBandedDiagonal),
)| {
let offset = diag_index as isize - kl as isize;
for (pos, eval) in &compiled_diag.evaluators {
let value = eval(runtime_args.as_ref());
let (row, col) = if offset >= 0 {
(*pos, offset as usize + *pos)
} else {
((-offset) as usize + *pos, *pos)
};
let value =
validate_callback_value("Jacobian", row, col, value)?;
let slot = diag_values
.get_mut(*pos)
.ok_or(BandedError::DimensionMismatch)?;
*slot = if value.abs() < threshold { 0.0 } else { value };
}
Ok(())
};
let split_long_diagonals =
parallel && compiled_diagonals.len() < rayon::current_num_threads();
if split_long_diagonals {
for (diag_index, (diag_values, compiled_diag)) in asm
.diagonals_mut()
.iter_mut()
.zip(compiled_diagonals.iter())
.enumerate()
{
let offset = diag_index as isize - kl as isize;
if compiled_diag.evaluators.len() > 1 {
let values: Vec<(usize, f64)> = compiled_diag
.evaluators
.par_iter()
.map(|(pos, eval)| {
let value = eval(runtime_args.as_ref());
let (row, col) = if offset >= 0 {
(*pos, offset as usize + *pos)
} else {
((-offset) as usize + *pos, *pos)
};
let value = validate_callback_value(
"Jacobian", row, col, value,
)?;
Ok::<(usize, f64), BandedError>((*pos, value))
})
.collect::<Result<Vec<_>, BandedError>>()?;
for (pos, value) in values {
let slot = diag_values
.get_mut(pos)
.ok_or(BandedError::DimensionMismatch)?;
*slot =
if value.abs() < threshold { 0.0 } else { value };
}
} else {
evaluate_diagonal((
diag_index,
(diag_values, compiled_diag),
))?;
}
}
} else if parallel {
asm.diagonals_mut()
.par_iter_mut()
.zip(compiled_diagonals.par_iter())
.enumerate()
.try_for_each(evaluate_diagonal)?;
} else {
asm.diagonals_mut()
.iter_mut()
.zip(compiled_diagonals.iter())
.enumerate()
.try_for_each(evaluate_diagonal)?;
}
}
CompiledPlan::Entry(compiled_entries) => {
let evaluate_entry = |entry: &CompiledBandedEntry| {
let value = validate_callback_value(
"Jacobian",
entry.row,
entry.col,
(entry.evaluator)(runtime_args.as_ref()),
)?;
Ok((entry.row, entry.col, value))
};
if parallel {
let values: Vec<(usize, usize, f64)> = compiled_entries
.par_iter()
.map(evaluate_entry)
.collect::<Result<Vec<_>, BandedError>>()?;
let storage_write_begin =
telemetry_timing_enabled.then(Instant::now);
for (row, col, mut value) in values {
if value.abs() < threshold {
value = 0.0;
}
asm.set(row, col, value)?;
}
storage_write_elapsed = storage_write_begin
.map(|started| started.elapsed())
.unwrap_or_default();
} else {
let storage_write_begin =
telemetry_timing_enabled.then(Instant::now);
for entry in compiled_entries {
let (row, col, mut value) = evaluate_entry(entry)?;
if value.abs() < threshold {
value = 0.0;
}
asm.set(row, col, value)?;
}
storage_write_elapsed = storage_write_begin
.map(|started| started.elapsed())
.unwrap_or_default();
}
}
}
if telemetry_enabled {
telemetry_for_callback.record_dispatch_with_tasks(
parallel,
matches!(&compiled, CompiledPlan::Diagonal(_)),
parallel_tasks,
);
telemetry_for_callback.record_stage_breakdown(
argument_prepare_elapsed,
evaluator_begin
.map(|started| started.elapsed())
.unwrap_or_default(),
storage_write_elapsed,
assembly_alloc_elapsed,
work_items,
storage_writes,
);
telemetry_for_callback.record_call(
begin.map(|started| started.elapsed()).unwrap_or_default(),
work_items,
);
}
Ok(asm)
})();
if telemetry_enabled && result.is_err() {
telemetry_for_callback.record_error();
}
result
},
);
Ok(DirectBandedJacobianRuntime {
callback,
telemetry,
})
}
pub fn generate_banded_jacobian_assembly_parallel(
&self,
config: &BandedLambdifyConfig,
) -> Result<Box<dyn Fn(&[f64]) -> Result<BandedAssembly, BandedError> + Send + Sync>, BandedError>
{
let plan = self.infer_banded_structure_plan()?;
self.generate_banded_jacobian_assembly_from_plan_parallel(&plan, config)
}
pub fn generate_banded_jacobian_runtime_parallel(
&self,
config: &BandedLambdifyConfig,
) -> Result<DirectBandedJacobianRuntime, BandedError> {
let plan = self.infer_banded_structure_plan()?;
self.generate_banded_jacobian_runtime_from_plan_parallel(&plan, config)
}
pub fn generate_banded_residual_parallel(
&self,
) -> Result<Box<dyn Fn(&[f64]) -> Result<Vec<f64>, BandedError> + Send + Sync>, BandedError>
{
self.generate_banded_residual_with_config(&BandedLambdifyConfig::default())
}
pub fn generate_banded_residual_with_config(
&self,
config: &BandedLambdifyConfig,
) -> Result<Box<dyn Fn(&[f64]) -> Result<Vec<f64>, BandedError> + Send + Sync>, BandedError>
{
let atom_native = self.atom_vector_of_functions.is_some();
let n_unknowns = if atom_native {
self.variable_string.len()
} else {
self.vector_of_variables.len()
};
let parameter_map = (!atom_native && !self.uses_dynamic_parameter_binding())
.then(|| self.parameter_substitution_map())
.flatten();
let argument_names = if atom_native {
self.atom_runtime_argument_names()?
} else if self.uses_dynamic_parameter_binding() {
self.flattened_runtime_argument_names()
} else {
self.runtime_argument_names(¶meter_map)?
};
let atom_context = atom_native.then(|| atom_variable_context(&argument_names));
let atom_function_map = atom_native.then(FunctionMap::new);
let argument_name_refs: Vec<&str> =
argument_names.iter().map(|name| name.as_str()).collect();
let compiled_residuals: Vec<BandedScalarEvaluator> =
if let Some(functions) = self.atom_vector_of_functions.as_ref() {
functions
.par_iter()
.map(|atom| {
compile_atom_scalar_evaluator(
atom,
atom_context.as_ref().expect("Atom context must exist"),
atom_function_map
.as_ref()
.expect("Atom function map must exist"),
)
.0
})
.collect()
} else {
self.vector_of_functions
.par_iter()
.map(|expr| {
if let Some(ref map) = parameter_map {
let prepared_expr = expr.set_variable_from_map(map);
compile_banded_scalar_evaluator(&prepared_expr, &argument_name_refs)
} else {
compile_banded_scalar_evaluator(expr, &argument_name_refs)
}
})
.collect()
};
let parameter_binding = self.parameter_binding.clone();
let parameter_values = self.parameter_values_for_callback();
let parameter_count = self.parameters_string.len();
let execution_policy = config.execution_policy;
Ok(Box::new(
move |unknowns: &[f64]| -> Result<Vec<f64>, BandedError> {
if unknowns.len() != n_unknowns {
return Err(BandedError::DimensionMismatch);
}
let uses_parameter_abi = atom_native || parameter_binding.is_some();
let needs_parameter_prefix = uses_parameter_abi && parameter_count > 0;
let runtime_args: Cow<'_, [f64]> = if needs_parameter_prefix {
Cow::Owned(flatten_lambdify_args(
parameter_binding
.as_ref()
.and_then(BvpParameterBindingHandle::snapshot)
.as_deref()
.or(parameter_values.as_deref()),
unknowns,
))
} else {
Cow::Borrowed(unknowns)
};
if execution_policy.should_parallel(compiled_residuals.len()) {
compiled_residuals
.par_iter()
.enumerate()
.map(|(row, eval)| {
validate_callback_value("residual", row, 0, eval(runtime_args.as_ref()))
})
.collect::<Result<Vec<_>, BandedError>>()
} else {
compiled_residuals
.iter()
.enumerate()
.map(|(row, eval)| {
validate_callback_value("residual", row, 0, eval(runtime_args.as_ref()))
})
.collect::<Result<Vec<_>, BandedError>>()
}
},
))
}
fn parameter_substitution_map(&self) -> Option<HashMap<String, f64>> {
match (
self.parameters_string.is_empty(),
self.parameter_values.as_ref(),
) {
(true, _) => None,
(false, Some(values)) if values.len() == self.parameters_string.len() => Some(
self.parameters_string
.iter()
.cloned()
.zip(values.iter().copied())
.collect(),
),
_ => None,
}
}
fn runtime_argument_names(
&self,
parameter_map: &Option<HashMap<String, f64>>,
) -> Result<Vec<String>, BandedError> {
if parameter_map.is_some() || self.parameters_string.is_empty() {
return Ok(self.variable_string.clone());
}
Err(BandedError::DimensionMismatch)
}
fn flattened_runtime_argument_names(&self) -> Vec<String> {
self.parameters_string
.iter()
.cloned()
.chain(self.variable_string.iter().cloned())
.collect()
}
fn atom_runtime_argument_names(&self) -> Result<Vec<String>, BandedError> {
if self.parameters_string.is_empty() {
return Ok(self.variable_string.clone());
}
if self.uses_dynamic_parameter_binding()
|| self
.parameter_values
.as_ref()
.is_some_and(|values| values.len() == self.parameters_string.len())
{
return Ok(self
.parameters_string
.iter()
.cloned()
.chain(self.variable_string.iter().cloned())
.collect());
}
Err(BandedError::DimensionMismatch)
}
}
#[cfg(test)]
mod tests {
use super::{BandedJacobianChunking, BandedLambdifyConfig, DirectBandedProblem};
use crate::parse;
use crate::somelinalg::banded::BandedError;
use crate::symbolic::View::jacobian::PreparedSparseAtomSystem;
use crate::symbolic::bvp::parameter_binding::BvpParameterBindingHandle;
use crate::symbolic::bvp::telemetry::{
BvpDirectJacobianTelemetrySnapshot, BvpLambdifyTelemetryMode,
};
use crate::symbolic::symbolic_engine::Expr;
#[test]
fn infers_node_major_banded_structure() {
let mut jac = DirectBandedProblem::default();
jac.vector_of_functions = vec![Expr::Const(0.0); 4];
jac.vector_of_variables = vec![
Expr::Var("y_0".to_string()),
Expr::Var("z_0".to_string()),
Expr::Var("y_1".to_string()),
Expr::Var("z_1".to_string()),
];
jac.variable_string = vec![
"y_0".to_string(),
"z_0".to_string(),
"y_1".to_string(),
"z_1".to_string(),
];
jac.bandwidth = Some((3, 3));
let plan = jac.infer_banded_structure_plan().unwrap();
assert_eq!(plan.n_nodes(), 2);
assert_eq!(plan.vars_per_node(), 2);
assert!(plan.block_tridiagonal_compatible);
}
#[test]
fn generates_banded_jacobian_assembly_from_sparse_symbolic_entries() {
let mut jac = DirectBandedProblem::default();
jac.vector_of_functions = vec![Expr::Const(0.0); 4];
jac.vector_of_variables = vec![
Expr::Var("y_0".to_string()),
Expr::Var("z_0".to_string()),
Expr::Var("y_1".to_string()),
Expr::Var("z_1".to_string()),
];
jac.variable_string = vec![
"y_0".to_string(),
"z_0".to_string(),
"y_1".to_string(),
"z_1".to_string(),
];
jac.bandwidth = Some((3, 3));
jac.symbolic_jacobian_sparse = vec![
(0, 0, Expr::Const(2.0) * Expr::Var("y_0".to_string())),
(0, 1, Expr::Const(1.0)),
(1, 0, Expr::Const(-1.0)),
(1, 1, Expr::Const(3.0) * Expr::Var("z_0".to_string())),
(2, 2, Expr::Const(4.0) * Expr::Var("y_1".to_string())),
(2, 3, Expr::Const(2.0)),
(3, 2, Expr::Const(-2.0)),
(3, 3, Expr::Const(5.0) * Expr::Var("z_1".to_string())),
];
let generator = jac
.generate_banded_jacobian_assembly_parallel(&BandedLambdifyConfig {
jacobian_chunking: BandedJacobianChunking::Diagonal,
..BandedLambdifyConfig::default()
})
.unwrap();
let asm = generator(&[2.0, 3.0, 4.0, 5.0]).unwrap();
assert_eq!(asm.get(0, 0).unwrap(), 4.0);
assert_eq!(asm.get(0, 1).unwrap(), 1.0);
assert_eq!(asm.get(1, 0).unwrap(), -1.0);
assert_eq!(asm.get(1, 1).unwrap(), 9.0);
assert_eq!(asm.get(2, 2).unwrap(), 16.0);
assert_eq!(asm.get(3, 3).unwrap(), 25.0);
}
#[test]
fn direct_banded_runtime_reports_typed_telemetry_without_changing_values() {
let mut jac = DirectBandedProblem::default();
jac.vector_of_functions = vec![Expr::Const(0.0); 2];
jac.vector_of_variables = vec![Expr::Var("y_0".to_string()), Expr::Var("y_1".to_string())];
jac.variable_string = vec!["y_0".to_string(), "y_1".to_string()];
jac.bandwidth = Some((1, 1));
jac.symbolic_jacobian_sparse = vec![
(0, 0, Expr::Const(2.0) * Expr::Var("y_0".to_string())),
(0, 1, Expr::Const(1.0)),
(1, 0, Expr::Const(-1.0)),
(1, 1, Expr::Const(3.0) * Expr::Var("y_1".to_string())),
];
let runtime = jac
.generate_banded_jacobian_runtime_parallel(&BandedLambdifyConfig::default())
.unwrap();
let asm = runtime.callback()(&[2.0, 4.0]).unwrap();
let snapshot = runtime.telemetry_snapshot();
assert_eq!(asm.get(0, 0).unwrap(), 4.0);
assert_eq!(asm.get(1, 1).unwrap(), 12.0);
assert_eq!(snapshot.calls, 1);
assert_eq!(snapshot.work_items, 4);
assert_eq!(snapshot.errors, 0);
assert!(snapshot.elapsed > std::time::Duration::ZERO);
assert_eq!(snapshot.evaluator_calls, 4);
assert_eq!(snapshot.storage_writes, 4);
assert_eq!(snapshot.parallel_dispatches, 1);
assert_eq!(snapshot.sequential_dispatches, 0);
assert_eq!(snapshot.diagonal_dispatches, 1);
assert_eq!(snapshot.entry_dispatches, 0);
assert!(snapshot.assembly_alloc_elapsed >= std::time::Duration::ZERO);
assert!(snapshot.argument_prepare_elapsed >= std::time::Duration::ZERO);
assert!(snapshot.evaluator_elapsed > std::time::Duration::ZERO);
assert!(runtime.callback()(&[2.0]).is_err());
assert_eq!(runtime.telemetry_snapshot().errors, 1);
}
#[test]
fn direct_banded_runtime_can_disable_callback_telemetry_without_changing_values() {
let mut jac = DirectBandedProblem::default();
jac.vector_of_functions = vec![Expr::Const(0.0)];
jac.vector_of_variables = vec![Expr::Var("y_0".to_string())];
jac.variable_string = vec!["y_0".to_string()];
jac.bandwidth = Some((0, 0));
jac.symbolic_jacobian_sparse = vec![(0, 0, Expr::Const(3.0))];
let runtime = jac
.generate_banded_jacobian_runtime_parallel(&BandedLambdifyConfig {
telemetry_mode: BvpLambdifyTelemetryMode::Off,
..BandedLambdifyConfig::default()
})
.unwrap();
let assembly = runtime.callback()(&[2.0]).unwrap();
assert_eq!(assembly.get(0, 0).unwrap(), 3.0);
assert_eq!(
runtime.telemetry_snapshot(),
BvpDirectJacobianTelemetrySnapshot::default()
);
}
#[test]
fn diagonal_runtime_skips_structural_zero_slots() {
let mut jac = DirectBandedProblem::default();
jac.vector_of_functions = vec![Expr::Const(0.0); 4];
jac.vector_of_variables = (0..4)
.map(|index| Expr::Var(format!("y_{index}")))
.collect();
jac.variable_string = (0..4).map(|index| format!("y_{index}")).collect();
jac.bandwidth = Some((3, 3));
jac.symbolic_jacobian_sparse = vec![
(0, 0, Expr::Const(2.0)),
(0, 1, Expr::Const(3.0)),
(1, 0, Expr::Const(4.0)),
(1, 1, Expr::Const(5.0)),
(2, 2, Expr::Const(6.0)),
(2, 3, Expr::Const(7.0)),
(3, 2, Expr::Const(8.0)),
(3, 3, Expr::Const(9.0)),
];
let runtime = jac
.generate_banded_jacobian_runtime_parallel(&BandedLambdifyConfig::default())
.unwrap();
let assembly = runtime.callback()(&[0.0; 4]).unwrap();
let snapshot = runtime.telemetry_snapshot();
assert_eq!(assembly.get(0, 0).unwrap(), 2.0);
assert_eq!(assembly.get(0, 3).unwrap(), 0.0);
assert_eq!(assembly.get(3, 0).unwrap(), 0.0);
assert_eq!(snapshot.evaluator_calls, 8);
assert_eq!(snapshot.storage_writes, 8);
}
#[test]
fn direct_banded_residual_rejects_non_finite_callback_values() {
let mut problem = DirectBandedProblem::default();
problem.vector_of_functions = vec![Expr::Const(f64::NAN)];
problem.vector_of_variables = vec![Expr::Var("y_0".to_string())];
problem.variable_string = vec!["y_0".to_string()];
let residual = problem.generate_banded_residual_parallel().unwrap();
let error = residual(&[0.0]).expect_err("NaN must not cross the callback boundary");
assert!(matches!(
error,
BandedError::NonFiniteCallbackValue {
stage: "residual",
row: 0,
col: 0,
value
} if value.is_nan()
));
}
#[test]
fn direct_banded_jacobian_rejects_non_finite_callback_values_before_thresholding() {
let mut problem = DirectBandedProblem::default();
problem.vector_of_functions = vec![Expr::Const(0.0)];
problem.vector_of_variables = vec![Expr::Var("y_0".to_string())];
problem.variable_string = vec!["y_0".to_string()];
problem.bandwidth = Some((0, 0));
problem.symbolic_jacobian_sparse = vec![(0, 0, Expr::Const(f64::INFINITY))];
for chunking in [
BandedJacobianChunking::Diagonal,
BandedJacobianChunking::EntryChunks,
] {
let jacobian = problem
.generate_banded_jacobian_runtime_parallel(&BandedLambdifyConfig {
jacobian_chunking: chunking,
..BandedLambdifyConfig::default()
})
.unwrap();
let error = jacobian.callback()(&[0.0])
.expect_err("infinity must not be converted into a stored Jacobian value");
assert!(matches!(
error,
BandedError::NonFiniteCallbackValue {
stage: "Jacobian",
row: 0,
col: 0,
value
} if value.is_infinite()
));
assert_eq!(jacobian.telemetry_snapshot().errors, 1);
}
}
#[test]
fn substitutes_fixed_parameters_before_compilation() {
let mut jac = DirectBandedProblem::default();
jac.vector_of_functions =
vec![Expr::Var("a".to_string()) * Expr::Var("y_0".to_string()) + Expr::Const(1.0)];
jac.vector_of_variables = vec![Expr::Var("y_0".to_string())];
jac.variable_string = vec!["y_0".to_string()];
jac.bandwidth = Some((0, 0));
jac.symbolic_jacobian_sparse = vec![(0, 0, Expr::Var("a".to_string()))];
jac.parameters_string = vec!["a".to_string()];
jac.parameter_values = Some(vec![7.0]);
let residual = jac.generate_banded_residual_parallel().unwrap();
let jacobian = jac
.generate_banded_jacobian_assembly_parallel(&BandedLambdifyConfig::default())
.unwrap();
let r = residual(&[3.0]).unwrap();
let j = jacobian(&[3.0]).unwrap();
assert_eq!(r[0], 22.0);
assert_eq!(j.get(0, 0).unwrap(), 7.0);
}
#[test]
fn atom_native_banded_runtime_uses_parameter_abi_without_expr_roundtrip() {
let residuals = vec![parse!("a*y_0 + y_1").unwrap(), parse!("y_0 - y_1").unwrap()];
let variable_names = vec!["y_0".to_string(), "y_1".to_string()];
let atom_system = PreparedSparseAtomSystem::from_atoms(
&residuals,
&variable_names,
&[variable_names.clone(), variable_names.clone()],
);
let atom_jacobian = atom_system.calc_sparse_jacobian_with_bandwidth(Some((1, 1)));
let mut problem =
DirectBandedProblem::default().with_atom_view_data(residuals, atom_jacobian);
problem.vector_of_functions = vec![Expr::Const(0.0); 2];
problem.vector_of_variables = variable_names
.iter()
.map(|name| Expr::Var(name.clone()))
.collect();
problem.variable_string = variable_names;
problem.parameters_string = vec!["a".to_string()];
problem.parameter_values = Some(vec![7.0]);
problem.bandwidth = Some((1, 1));
let residual = problem.generate_banded_residual_parallel().unwrap();
let jacobian = problem
.generate_banded_jacobian_assembly_parallel(&BandedLambdifyConfig::default())
.unwrap();
let x = [2.0, 3.0];
let r = residual(&x).unwrap();
let j = jacobian(&x).unwrap();
assert_eq!(r, vec![17.0, -1.0]);
assert_eq!(j.get(0, 0).unwrap(), 7.0);
assert_eq!(j.get(0, 1).unwrap(), 1.0);
assert_eq!(j.get(1, 0).unwrap(), 1.0);
assert_eq!(j.get(1, 1).unwrap(), -1.0);
}
#[test]
fn atom_native_unparameterized_runtime_preserves_state_values() {
let residuals = vec![parse!("y_0 + y_1").unwrap(), parse!("y_0 - y_1").unwrap()];
let variable_names = vec!["y_0".to_string(), "y_1".to_string()];
let atom_system = PreparedSparseAtomSystem::from_atoms(
&residuals,
&variable_names,
&[variable_names.clone(), variable_names.clone()],
);
let atom_jacobian = atom_system.calc_sparse_jacobian_with_bandwidth(Some((1, 1)));
let mut problem =
DirectBandedProblem::default().with_atom_view_data(residuals, atom_jacobian);
problem.vector_of_functions = vec![Expr::Const(0.0); 2];
problem.vector_of_variables = variable_names
.iter()
.map(|name| Expr::Var(name.clone()))
.collect();
problem.variable_string = variable_names;
problem.bandwidth = Some((1, 1));
let residual = problem.generate_banded_residual_parallel().unwrap();
let jacobian = problem
.generate_banded_jacobian_assembly_parallel(&BandedLambdifyConfig::default())
.unwrap();
assert_eq!(residual(&[2.0, 3.0]).unwrap(), vec![5.0, -1.0]);
let assembly = jacobian(&[2.0, 3.0]).unwrap();
assert_eq!(assembly.get(0, 0).unwrap(), 1.0);
assert_eq!(assembly.get(0, 1).unwrap(), 1.0);
assert_eq!(assembly.get(1, 0).unwrap(), 1.0);
assert_eq!(assembly.get(1, 1).unwrap(), -1.0);
}
#[test]
fn prepared_banded_runtime_rebinds_numeric_parameters_without_recompilation() {
let mut problem = DirectBandedProblem::default();
problem.vector_of_functions =
vec![Expr::Var("a".to_string()) * Expr::Var("y_0".to_string())];
problem.vector_of_variables = vec![Expr::Var("y_0".to_string())];
problem.variable_string = vec!["y_0".to_string()];
problem.parameters_string = vec!["a".to_string()];
problem.parameter_values = Some(vec![2.0]);
problem.bandwidth = Some((0, 0));
problem.symbolic_jacobian_sparse = vec![(0, 0, Expr::Var("a".to_string()))];
let binding = BvpParameterBindingHandle::new(Some(vec![2.0]));
problem = problem.with_parameter_binding(binding.clone());
let residual = problem.generate_banded_residual_parallel().unwrap();
let jacobian = problem
.generate_banded_jacobian_runtime_parallel(&BandedLambdifyConfig::default())
.unwrap();
assert_eq!(residual(&[3.0]).unwrap(), vec![6.0]);
assert_eq!(jacobian.callback()(&[3.0]).unwrap().get(0, 0).unwrap(), 2.0);
binding.replace(Some(vec![5.0]));
assert_eq!(residual(&[3.0]).unwrap(), vec![15.0]);
assert_eq!(jacobian.callback()(&[3.0]).unwrap().get(0, 0).unwrap(), 5.0);
}
#[test]
fn banded_chunking_modes_produce_identical_assembly_values() {
let mut jac = DirectBandedProblem::default();
jac.vector_of_functions = vec![Expr::Const(0.0); 4];
jac.vector_of_variables = vec![
Expr::Var("y_0".to_string()),
Expr::Var("z_0".to_string()),
Expr::Var("y_1".to_string()),
Expr::Var("z_1".to_string()),
];
jac.variable_string = vec![
"y_0".to_string(),
"z_0".to_string(),
"y_1".to_string(),
"z_1".to_string(),
];
jac.bandwidth = Some((3, 3));
jac.symbolic_jacobian_sparse = vec![
(0, 0, Expr::Const(2.0) * Expr::Var("y_0".to_string())),
(0, 1, Expr::Const(1.0)),
(1, 0, Expr::Const(-1.0)),
(1, 1, Expr::Const(3.0) * Expr::Var("z_0".to_string())),
(2, 2, Expr::Const(4.0) * Expr::Var("y_1".to_string())),
(2, 3, Expr::Const(2.0)),
(3, 2, Expr::Const(-2.0)),
(3, 3, Expr::Const(5.0) * Expr::Var("z_1".to_string())),
];
let diagonal = jac
.generate_banded_jacobian_assembly_parallel(&BandedLambdifyConfig {
jacobian_chunking: BandedJacobianChunking::Diagonal,
..BandedLambdifyConfig::default()
})
.unwrap();
let entry_chunks = jac
.generate_banded_jacobian_assembly_parallel(&BandedLambdifyConfig {
jacobian_chunking: BandedJacobianChunking::EntryChunks,
..BandedLambdifyConfig::default()
})
.unwrap();
let x = [2.0, 3.0, 4.0, 5.0];
let d = diagonal(&x).unwrap();
let e = entry_chunks(&x).unwrap();
for row in 0..4 {
for col in 0..4 {
let dv = d.get(row, col).unwrap();
let ev = e.get(row, col).unwrap();
assert!(
(dv - ev).abs() <= 1e-12,
"chunking mismatch at ({row},{col}): diagonal={dv}, entry={ev}"
);
}
}
let entry_runtime = jac
.generate_banded_jacobian_runtime_parallel(&BandedLambdifyConfig {
jacobian_chunking: BandedJacobianChunking::EntryChunks,
..BandedLambdifyConfig::default()
})
.unwrap();
entry_runtime.callback()(&x).unwrap();
let snapshot = entry_runtime.telemetry_snapshot();
assert_eq!(snapshot.entry_dispatches, 1);
assert_eq!(snapshot.diagonal_dispatches, 0);
assert_eq!(snapshot.evaluator_calls, 8);
assert_eq!(snapshot.storage_writes, 8);
}
#[test]
fn banded_sequential_and_parallel_runtime_policies_are_identical() {
let mut problem = DirectBandedProblem::default();
problem.vector_of_functions = vec![
Expr::Var("y_0".to_string()) + Expr::Var("z_0".to_string()),
Expr::Var("y_0".to_string()) - Expr::Var("z_0".to_string()),
];
problem.vector_of_variables =
vec![Expr::Var("y_0".to_string()), Expr::Var("z_0".to_string())];
problem.variable_string = vec!["y_0".to_string(), "z_0".to_string()];
problem.bandwidth = Some((1, 1));
problem.symbolic_jacobian_sparse = vec![
(0, 0, Expr::Const(1.0)),
(0, 1, Expr::Const(1.0)),
(1, 0, Expr::Const(1.0)),
(1, 1, Expr::Const(-1.0)),
];
let sequential_config = BandedLambdifyConfig {
execution_policy:
crate::symbolic::bvp::telemetry::BvpLambdifyExecutionPolicy::Sequential,
..BandedLambdifyConfig::default()
};
let parallel_config = BandedLambdifyConfig {
execution_policy:
crate::symbolic::bvp::telemetry::BvpLambdifyExecutionPolicy::Parallel {
min_work: 0,
},
..BandedLambdifyConfig::default()
};
let threshold_config = BandedLambdifyConfig {
execution_policy:
crate::symbolic::bvp::telemetry::BvpLambdifyExecutionPolicy::Parallel {
min_work: 1,
},
..BandedLambdifyConfig::default()
};
let above_threshold_config = BandedLambdifyConfig {
execution_policy:
crate::symbolic::bvp::telemetry::BvpLambdifyExecutionPolicy::Parallel {
min_work: 5,
},
..BandedLambdifyConfig::default()
};
let auto_config = BandedLambdifyConfig {
execution_policy: crate::symbolic::bvp::telemetry::BvpLambdifyExecutionPolicy::Auto {
min_work: 0,
},
..BandedLambdifyConfig::default()
};
let sequential_jacobian = problem
.generate_banded_jacobian_runtime_parallel(&sequential_config)
.unwrap();
let parallel_jacobian = problem
.generate_banded_jacobian_runtime_parallel(¶llel_config)
.unwrap();
let threshold_jacobian = problem
.generate_banded_jacobian_runtime_parallel(&threshold_config)
.unwrap();
let above_threshold_jacobian = problem
.generate_banded_jacobian_runtime_parallel(&above_threshold_config)
.unwrap();
let auto_jacobian = problem
.generate_banded_jacobian_runtime_parallel(&auto_config)
.unwrap();
let sequential_residual = problem
.generate_banded_residual_with_config(&sequential_config)
.unwrap();
let parallel_residual = problem
.generate_banded_residual_with_config(¶llel_config)
.unwrap();
let unknowns = [2.0, 3.0];
let seq_j = sequential_jacobian.callback()(&unknowns).unwrap();
let par_j = parallel_jacobian.callback()(&unknowns).unwrap();
threshold_jacobian.callback()(&unknowns).unwrap();
above_threshold_jacobian.callback()(&unknowns).unwrap();
let auto_j = auto_jacobian.callback()(&unknowns).unwrap();
let seq_snapshot = sequential_jacobian.telemetry_snapshot();
let par_snapshot = parallel_jacobian.telemetry_snapshot();
let threshold_snapshot = threshold_jacobian.telemetry_snapshot();
let above_threshold_snapshot = above_threshold_jacobian.telemetry_snapshot();
let auto_snapshot = auto_jacobian.telemetry_snapshot();
assert_eq!(seq_snapshot.sequential_dispatches, 1);
assert_eq!(seq_snapshot.parallel_dispatches, 0);
assert_eq!(par_snapshot.sequential_dispatches, 0);
assert_eq!(par_snapshot.parallel_dispatches, 1);
assert_eq!(threshold_snapshot.parallel_dispatches, 1);
assert_eq!(above_threshold_snapshot.sequential_dispatches, 1);
assert_eq!(
auto_snapshot.parallel_dispatches + auto_snapshot.sequential_dispatches,
1
);
assert_eq!(
sequential_residual(&unknowns).unwrap(),
parallel_residual(&unknowns).unwrap()
);
for row in 0..2 {
for col in 0..2 {
assert_eq!(seq_j.get(row, col).unwrap(), par_j.get(row, col).unwrap());
assert_eq!(seq_j.get(row, col).unwrap(), auto_j.get(row, col).unwrap());
}
}
assert_eq!(sequential_residual(&unknowns).unwrap(), vec![5.0, -1.0]);
}
#[test]
fn auto_splits_a_long_single_diagonal_without_changing_values() {
let n = 64;
let mut problem = DirectBandedProblem::default();
problem.vector_of_functions = vec![Expr::Const(0.0); n];
problem.vector_of_variables = (0..n)
.map(|index| Expr::Var(format!("y_{index}")))
.collect();
problem.variable_string = (0..n).map(|index| format!("y_{index}")).collect();
problem.bandwidth = Some((0, 0));
problem.symbolic_jacobian_sparse = (0..n)
.map(|index| (index, index, Expr::Const(index as f64 + 1.0)))
.collect();
let sequential = problem
.generate_banded_jacobian_runtime_parallel(&BandedLambdifyConfig {
execution_policy:
crate::symbolic::bvp::telemetry::BvpLambdifyExecutionPolicy::Sequential,
..BandedLambdifyConfig::default()
})
.unwrap();
let auto = problem
.generate_banded_jacobian_runtime_parallel(&BandedLambdifyConfig {
execution_policy:
crate::symbolic::bvp::telemetry::BvpLambdifyExecutionPolicy::Auto {
min_work: 0,
},
..BandedLambdifyConfig::default()
})
.unwrap();
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(2)
.build()
.unwrap();
let unknowns = vec![1.0; n];
let (expected, actual) = pool.install(|| {
(
sequential.callback()(&unknowns).unwrap(),
auto.callback()(&unknowns).unwrap(),
)
});
for index in 0..n {
assert_eq!(
expected.get(index, index).unwrap(),
actual.get(index, index).unwrap()
);
}
let snapshot = auto.telemetry_snapshot();
assert_eq!(snapshot.parallel_dispatches, 1);
assert_eq!(snapshot.sequential_dispatches, 0);
assert_eq!(snapshot.evaluator_calls, n as u64);
assert_eq!(snapshot.effective_task_count, 8);
}
#[test]
fn banded_structural_threshold_zeros_tiny_entries() {
let mut jac = DirectBandedProblem::default();
jac.vector_of_functions = vec![Expr::Const(0.0); 2];
jac.vector_of_variables = vec![Expr::Var("y_0".to_string()), Expr::Var("z_0".to_string())];
jac.variable_string = vec!["y_0".to_string(), "z_0".to_string()];
jac.bandwidth = Some((1, 1));
jac.symbolic_jacobian_sparse = vec![(0, 0, Expr::Const(1e-14)), (1, 1, Expr::Const(2.0))];
let generator = jac
.generate_banded_jacobian_assembly_parallel(&BandedLambdifyConfig {
jacobian_chunking: BandedJacobianChunking::EntryChunks,
structural_threshold: 1e-12,
..BandedLambdifyConfig::default()
})
.unwrap();
let asm = generator(&[1.0, 1.0]).unwrap();
assert_eq!(asm.get(0, 0).unwrap(), 0.0);
assert_eq!(asm.get(1, 1).unwrap(), 2.0);
}
}