use crate::somelinalg::banded::{
banded_assembly::BandedAssembly, BandedError, LinearSolverConfig, NodeMajorLayout,
};
use crate::symbolic::symbolic_engine::Expr;
use crate::symbolic::symbolic_functions_BVP::Jacobian;
use rayon::prelude::*;
use std::collections::{HashMap, HashSet};
#[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 jacobian_chunking: BandedJacobianChunking,
pub linear_solver_backend: BandedLinearSolverBackend,
pub linear_solver_config: LinearSolverConfig,
pub structural_threshold: f64,
}
impl Default for BandedLambdifyConfig {
fn default() -> Self {
Self {
jacobian_chunking: BandedJacobianChunking::Diagonal,
linear_solver_backend: BandedLinearSolverBackend::BlockTridiagonalNative,
linear_solver_config: LinearSolverConfig::default(),
structural_threshold: 0.0,
}
}
}
#[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,
}
type BandedScalarEvaluator = Box<dyn Fn(&[f64]) -> f64 + Send + Sync>;
struct CompiledBandedDiagonal {
evaluators: Vec<Option<BandedScalarEvaluator>>,
}
struct CompiledBandedEntry {
row: usize,
col: usize,
evaluator: BandedScalarEvaluator,
}
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)
}
impl Jacobian {
pub fn infer_banded_structure_plan(&self) -> Result<BandedStructurePlan, BandedError> {
let n_unknowns = self.vector_of_variables.len();
let n_equations = 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>, BandedError> {
let parameter_map = self.parameter_substitution_map();
let argument_names = self.runtime_argument_names(¶meter_map)?;
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) {
let len = plan.n_unknowns.saturating_sub(offset.unsigned_abs());
diagonals.push(CompiledBandedDiagonal {
evaluators: std::iter::repeat_with(|| None).take(len).collect(),
});
}
let compiled_entries: Vec<(usize, usize, BandedScalarEvaluator)> = 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))
})
.collect();
for (diag_index, pos, evaluator) in compiled_entries {
diagonals[diag_index].evaluators[pos] = Some(evaluator);
}
Ok(diagonals)
}
fn compile_banded_entry_plan(
&self,
plan: &BandedStructurePlan,
) -> Result<Vec<CompiledBandedEntry>, BandedError> {
let parameter_map = self.parameter_substitution_map();
let argument_names = self.runtime_argument_names(¶meter_map)?;
let argument_name_refs: Vec<&str> =
argument_names.iter().map(|name| name.as_str()).collect();
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,
})
})
.collect())
}
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>
{
let n = plan.n_unknowns;
let kl = plan.scalar_bandwidth.0;
let ku = plan.scalar_bandwidth.1;
let threshold = config.structural_threshold.abs();
enum CompiledPlan {
Diagonal(Vec<CompiledBandedDiagonal>),
Entry(Vec<CompiledBandedEntry>),
}
let compiled = match config.jacobian_chunking {
BandedJacobianChunking::Diagonal => {
CompiledPlan::Diagonal(self.compile_banded_diagonal_plan(&plan)?)
}
BandedJacobianChunking::EntryChunks => {
CompiledPlan::Entry(self.compile_banded_entry_plan(&plan)?)
}
};
Ok(Box::new(
move |unknowns: &[f64]| -> Result<BandedAssembly, BandedError> {
if unknowns.len() != n {
return Err(BandedError::DimensionMismatch);
}
let mut asm = BandedAssembly::zeros(n, kl, ku)?;
match &compiled {
CompiledPlan::Diagonal(compiled_diagonals) => {
asm.diagonals_mut()
.par_iter_mut()
.zip(compiled_diagonals.par_iter())
.for_each(|(diag_values, compiled_diag)| {
for (slot, maybe_eval) in
diag_values.iter_mut().zip(compiled_diag.evaluators.iter())
{
let value = maybe_eval
.as_ref()
.map(|eval| eval(unknowns))
.unwrap_or(0.0);
*slot = if value.abs() < threshold { 0.0 } else { value };
}
});
}
CompiledPlan::Entry(compiled_entries) => {
let values: Vec<(usize, usize, f64)> = compiled_entries
.par_iter()
.map(|entry| (entry.row, entry.col, (entry.evaluator)(unknowns)))
.collect();
for (row, col, mut value) in values {
if value.abs() < threshold {
value = 0.0;
}
asm.set(row, col, value)?;
}
}
}
Ok(asm)
},
))
}
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_residual_parallel(
&self,
) -> Result<Box<dyn Fn(&[f64]) -> Result<Vec<f64>, BandedError> + Send + Sync>, BandedError>
{
let n_unknowns = self.vector_of_variables.len();
let parameter_map = self.parameter_substitution_map();
let argument_names = self.runtime_argument_names(¶meter_map)?;
let argument_name_refs: Vec<&str> =
argument_names.iter().map(|name| name.as_str()).collect();
let compiled_residuals: Vec<BandedScalarEvaluator> = 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();
Ok(Box::new(
move |unknowns: &[f64]| -> Result<Vec<f64>, BandedError> {
if unknowns.len() != n_unknowns {
return Err(BandedError::DimensionMismatch);
}
Ok(compiled_residuals
.par_iter()
.map(|eval| eval(unknowns))
.collect())
},
))
}
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)
}
}
#[cfg(test)]
mod tests {
use super::{BandedJacobianChunking, BandedLambdifyConfig};
use crate::symbolic::symbolic_engine::Expr;
use crate::symbolic::symbolic_functions_BVP::Jacobian;
#[test]
fn infers_node_major_banded_structure() {
let mut jac = Jacobian::new();
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 = Jacobian::new();
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 substitutes_fixed_parameters_before_compilation() {
let mut jac = Jacobian::new();
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.set_params(Some(&["a"]));
jac.set_param_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 banded_chunking_modes_produce_identical_assembly_values() {
let mut jac = Jacobian::new();
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}"
);
}
}
}
#[test]
fn banded_structural_threshold_zeros_tiny_entries() {
let mut jac = Jacobian::new();
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);
}
}