use crate::somelinalg::banded::{
banded_assembly::BandedAssembly, error::BandedError, storage::Banded,
};
use crate::symbolic::codegen::codegen_orchestrator::rayon_overhead_baseline;
use crate::symbolic::codegen::codegen_tasks::{
BandedChunkingStrategy, BandedExprEntry, BandedJacobianTask, CodegenTask, CodegenTaskPlan,
IvpJacobianTask, IvpResidualTask, JacobianTask, ResidualTask, SparseChunkingStrategy,
SparseExprEntry, SparseJacobianTask,
};
use crate::symbolic::symbolic_engine::Expr;
use faer::sparse::{SparseColMat, Triplet};
use nalgebra::DMatrix;
use std::borrow::Cow;
#[derive(Debug, Clone, Copy)]
pub struct RuntimeArguments<'a> {
pub params: Option<&'a [f64]>,
pub variables: &'a [f64],
}
impl<'a> RuntimeArguments<'a> {
pub fn new(variables: &'a [f64], params: Option<&'a [f64]>) -> Self {
Self { params, variables }
}
pub fn total_len(&self) -> usize {
self.variables.len() + self.params.map_or(0, |params| params.len())
}
pub fn flatten(&self) -> Vec<f64> {
let mut flat = Vec::with_capacity(self.total_len());
if let Some(params) = self.params {
flat.extend_from_slice(params);
}
flat.extend_from_slice(self.variables);
flat
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResidualChunkingStrategy {
Whole,
ByTargetChunkCount { target_chunks: usize },
ByOutputCount { max_outputs_per_chunk: usize },
}
#[derive(Debug, Clone)]
pub struct ResidualChunkPlan<'a> {
pub fn_name: String,
pub output_offset: usize,
pub residuals: &'a [Expr],
pub plan: CodegenTaskPlan<'a>,
}
#[derive(Debug, Clone)]
pub struct ResidualRuntimePlan<'a> {
pub fn_name: &'a str,
pub output_len: usize,
pub input_names: Vec<&'a str>,
pub chunks: Vec<ResidualChunkPlan<'a>>,
}
impl<'a> ResidualRuntimePlan<'a> {
pub fn len(&self) -> usize {
self.output_len
}
pub fn is_empty(&self) -> bool {
self.output_len == 0
}
}
impl<'a> ResidualTask<'a> {
pub fn runtime_plan(&self, strategy: ResidualChunkingStrategy) -> ResidualRuntimePlan<'a> {
let chunk_size = match strategy {
ResidualChunkingStrategy::Whole => self.residuals.len().max(1),
ResidualChunkingStrategy::ByTargetChunkCount { target_chunks } => {
assert!(target_chunks > 0, "target_chunks must be positive");
self.residuals.len().max(1).div_ceil(target_chunks).max(1)
}
ResidualChunkingStrategy::ByOutputCount {
max_outputs_per_chunk,
} => {
assert!(
max_outputs_per_chunk > 0,
"max_outputs_per_chunk must be positive"
);
max_outputs_per_chunk
}
};
let chunks = self
.residuals
.chunks(chunk_size)
.enumerate()
.map(|(chunk_index, residuals)| {
let output_offset = chunk_index * chunk_size;
let fn_name = if chunk_index == 0 && residuals.len() == self.residuals.len() {
self.fn_name.to_string()
} else {
format!("{}_chunk_{}", self.fn_name, chunk_index)
};
let chunk_task = ResidualTask {
fn_name: self.fn_name,
residuals,
variables: self.variables,
params: self.params,
};
let mut plan = chunk_task.plan();
plan.fn_name = Cow::Owned(fn_name.clone());
ResidualChunkPlan {
fn_name,
output_offset,
residuals,
plan,
}
})
.collect();
ResidualRuntimePlan {
fn_name: self.fn_name,
output_len: self.residuals.len(),
input_names: self.flattened_argument_names(),
chunks,
}
}
}
impl<'a> IvpResidualTask<'a> {
pub fn runtime_plan(&self, strategy: ResidualChunkingStrategy) -> ResidualRuntimePlan<'a> {
let chunk_size = match strategy {
ResidualChunkingStrategy::Whole => self.residuals.len().max(1),
ResidualChunkingStrategy::ByTargetChunkCount { target_chunks } => {
assert!(target_chunks > 0, "target_chunks must be positive");
self.residuals.len().max(1).div_ceil(target_chunks).max(1)
}
ResidualChunkingStrategy::ByOutputCount {
max_outputs_per_chunk,
} => {
assert!(
max_outputs_per_chunk > 0,
"max_outputs_per_chunk must be positive"
);
max_outputs_per_chunk
}
};
let chunks = self
.residuals
.chunks(chunk_size)
.enumerate()
.map(|(chunk_index, residuals)| {
let output_offset = chunk_index * chunk_size;
let fn_name = if chunk_index == 0 && residuals.len() == self.residuals.len() {
self.fn_name.to_string()
} else {
format!("{}_chunk_{}", self.fn_name, chunk_index)
};
let chunk_task = IvpResidualTask {
fn_name: self.fn_name,
time_arg: self.time_arg,
residuals,
variables: self.variables,
params: self.params,
};
let mut plan = chunk_task.plan();
plan.fn_name = Cow::Owned(fn_name.clone());
ResidualChunkPlan {
fn_name,
output_offset,
residuals,
plan,
}
})
.collect();
ResidualRuntimePlan {
fn_name: self.fn_name,
output_len: self.residuals.len(),
input_names: self.flattened_argument_names_with_time(),
chunks,
}
}
}
#[derive(Debug, Clone)]
pub struct SparseJacobianValuesChunkPlan<'a> {
pub fn_name: String,
pub value_offset: usize,
pub entries: Vec<SparseExprEntry<'a>>,
pub plan: CodegenTaskPlan<'a>,
}
#[derive(Debug, Clone)]
pub struct BandedJacobianValuesChunkPlan<'a> {
pub fn_name: String,
pub value_offset: usize,
pub entries: Vec<BandedExprEntry<'a>>,
pub plan: CodegenTaskPlan<'a>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DenseJacobianChunkingStrategy {
Whole,
ByTargetChunkCount { target_chunks: usize },
ByRowCount { rows_per_chunk: usize },
}
#[derive(Debug, Clone)]
pub struct DenseJacobianChunkPlan<'a> {
pub fn_name: String,
pub row_offset: usize,
pub value_offset: usize,
pub jacobian_rows: &'a [Vec<Expr>],
pub plan: CodegenTaskPlan<'a>,
}
impl<'a> DenseJacobianChunkPlan<'a> {
pub fn row_len(&self) -> usize {
self.jacobian_rows.len()
}
pub fn value_range(&self) -> std::ops::Range<usize> {
let cols = self.jacobian_rows.first().map_or(0usize, |row| row.len());
self.value_offset..(self.value_offset + self.row_len() * cols)
}
}
#[derive(Debug, Clone)]
pub struct DenseJacobianRuntimePlan<'a> {
pub fn_name: &'a str,
pub rows: usize,
pub cols: usize,
pub input_names: Vec<&'a str>,
pub chunks: Vec<DenseJacobianChunkPlan<'a>>,
}
impl<'a> DenseJacobianRuntimePlan<'a> {
pub fn len(&self) -> usize {
self.rows * self.cols
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn assemble_dense_matrix(&self, values: &[f64]) -> DMatrix<f64> {
assert_eq!(
values.len(),
self.len(),
"expected {} dense Jacobian values, got {}",
self.len(),
values.len()
);
DMatrix::from_row_slice(self.rows, self.cols, values)
}
}
impl<'a> SparseJacobianValuesChunkPlan<'a> {
pub fn value_range(&self) -> std::ops::Range<usize> {
self.value_offset..(self.value_offset + self.entries.len())
}
}
impl<'a> BandedJacobianValuesChunkPlan<'a> {
pub fn value_range(&self) -> std::ops::Range<usize> {
self.value_offset..(self.value_offset + self.entries.len())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SparseJacobianStructure {
pub rows: usize,
pub cols: usize,
pub row_indices: Vec<usize>,
pub col_indices: Vec<usize>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BandedJacobianStructure {
pub rows: usize,
pub cols: usize,
pub kl: usize,
pub ku: usize,
pub diagonal_offsets: Vec<isize>,
pub diagonal_positions: Vec<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BandedCompactJacobianStructure {
pub rows: usize,
pub cols: usize,
pub kl: usize,
pub ku: usize,
}
impl BandedCompactJacobianStructure {
pub fn storage_len(&self) -> Result<usize, BandedError> {
if self.rows == 0 || self.rows != self.cols {
return Err(BandedError::DimensionMismatch);
}
self.kl
.checked_add(self.ku)
.and_then(|width| width.checked_add(1))
.and_then(|width| width.checked_mul(self.cols))
.ok_or(BandedError::InvalidBand {
n: self.rows,
kl: self.kl,
ku: self.ku,
})
}
pub fn try_from_values(&self, values: Vec<f64>) -> Result<Banded<f64>, BandedError> {
let expected = self.storage_len()?;
if values.len() != expected {
return Err(BandedError::DimensionMismatch);
}
Banded::from_vec(self.rows, self.kl, self.ku, values)
}
pub fn try_to_assembly(&self, values: &[f64]) -> Result<BandedAssembly, BandedError> {
let expected = self.storage_len()?;
if values.len() != expected {
return Err(BandedError::DimensionMismatch);
}
let mut assembly = BandedAssembly::zeros(self.rows, self.kl, self.ku)?;
for offset in -(self.kl as isize)..=(self.ku as isize) {
let diagonal = assembly
.diag_mut(offset)
.ok_or(BandedError::DimensionMismatch)?;
let band_row = (self.ku as isize - offset) as usize;
let column_start = if offset >= 0 { offset as usize } else { 0 };
let source_start = band_row * self.cols + column_start;
let source_end = source_start + diagonal.len();
diagonal.copy_from_slice(
values
.get(source_start..source_end)
.ok_or(BandedError::DimensionMismatch)?,
);
}
Ok(assembly)
}
}
impl SparseJacobianStructure {
pub fn nnz(&self) -> usize {
self.row_indices.len()
}
pub fn assemble_sparse_col_mat(&self, values: &[f64]) -> SparseColMat<usize, f64> {
assert_eq!(
values.len(),
self.nnz(),
"expected {} sparse values, got {}",
self.nnz(),
values.len()
);
let triplets: Vec<Triplet<usize, usize, f64>> = self
.row_indices
.iter()
.copied()
.zip(self.col_indices.iter().copied())
.zip(values.iter().copied())
.map(|((row, col), value)| Triplet::new(row, col, value))
.collect();
SparseColMat::try_new_from_triplets(self.rows, self.cols, &triplets)
.expect("sparse Jacobian structure should assemble into SparseColMat")
}
}
impl BandedJacobianStructure {
pub fn nnz(&self) -> usize {
self.diagonal_offsets.len()
}
pub fn compact_storage_len(&self) -> Result<usize, BandedError> {
if self.rows == 0 || self.rows != self.cols {
return Err(BandedError::DimensionMismatch);
}
self.kl
.checked_add(self.ku)
.and_then(|width| width.checked_add(1))
.and_then(|width| width.checked_mul(self.rows))
.ok_or(BandedError::InvalidBand {
n: self.rows,
kl: self.kl,
ku: self.ku,
})
}
pub fn try_assemble_banded_assembly(
&self,
values: &[f64],
) -> Result<BandedAssembly, BandedError> {
if values.len() != self.nnz()
|| self.rows == 0
|| self.rows != self.cols
|| self.diagonal_offsets.len() != self.diagonal_positions.len()
{
return Err(BandedError::DimensionMismatch);
}
let mut asm = BandedAssembly::zeros(self.rows, self.kl, self.ku)?;
let mut seen = Vec::with_capacity(self.nnz());
for ((offset, position), value) in self
.diagonal_offsets
.iter()
.copied()
.zip(self.diagonal_positions.iter().copied())
.zip(values.iter().copied())
{
let diag = asm.diag_mut(offset).ok_or(BandedError::DimensionMismatch)?;
if position >= diag.len() || seen.contains(&(offset, position)) {
return Err(BandedError::DimensionMismatch);
}
seen.push((offset, position));
diag[position] = value;
}
Ok(asm)
}
pub fn try_assemble_compact_banded_storage(
&self,
values: &[f64],
) -> Result<Banded<f64>, BandedError> {
self.try_assemble_banded_assembly(values)?.to_banded()
}
pub fn assemble_banded_assembly(&self, values: &[f64]) -> BandedAssembly {
self.try_assemble_banded_assembly(values)
.expect("banded Jacobian structure should assemble from valid values")
}
}
#[derive(Debug, Clone)]
pub struct SparseJacobianRuntimePlan<'a> {
pub fn_name: &'a str,
pub input_names: Vec<&'a str>,
pub structure: SparseJacobianStructure,
pub chunks: Vec<SparseJacobianValuesChunkPlan<'a>>,
}
#[derive(Debug, Clone)]
pub struct BandedJacobianRuntimePlan<'a> {
pub fn_name: &'a str,
pub input_names: Vec<&'a str>,
pub structure: BandedJacobianStructure,
pub compact: bool,
pub chunks: Vec<BandedJacobianValuesChunkPlan<'a>>,
}
#[derive(Debug, Clone, Copy)]
pub struct BandedCompactJacobianRuntimePlan<'a> {
pub fn_name: &'a str,
pub input_names: &'a [&'a str],
pub structure: BandedCompactJacobianStructure,
}
impl<'a> BandedCompactJacobianRuntimePlan<'a> {
pub fn output_len(&self) -> Result<usize, BandedError> {
self.structure.storage_len()
}
pub fn try_assemble_compact_storage(
&self,
values: Vec<f64>,
) -> Result<Banded<f64>, BandedError> {
self.structure.try_from_values(values)
}
}
impl<'a> SparseJacobianRuntimePlan<'a> {
pub fn nnz(&self) -> usize {
self.structure.nnz()
}
pub fn assemble_sparse_col_mat(&self, values: &[f64]) -> SparseColMat<usize, f64> {
self.structure.assemble_sparse_col_mat(values)
}
}
impl<'a> BandedJacobianRuntimePlan<'a> {
pub fn nnz(&self) -> usize {
self.structure.nnz()
}
pub fn assemble_banded_assembly(&self, values: &[f64]) -> BandedAssembly {
self.structure.assemble_banded_assembly(values)
}
pub fn try_assemble_compact_banded_storage(
&self,
values: &[f64],
) -> Result<Banded<f64>, BandedError> {
self.structure.try_assemble_compact_banded_storage(values)
}
}
impl<'a> SparseJacobianTask<'a> {
pub fn runtime_plan(&self, strategy: SparseChunkingStrategy) -> SparseJacobianRuntimePlan<'a> {
let chunks: Vec<SparseJacobianValuesChunkPlan<'a>> = self
.chunk_with_strategy(strategy)
.into_iter()
.map(|chunk| SparseJacobianValuesChunkPlan {
fn_name: chunk.chunk_fn_name(),
value_offset: chunk.entry_offset,
entries: chunk.entries.clone(),
plan: chunk.plan(),
})
.collect();
SparseJacobianRuntimePlan {
fn_name: self.fn_name,
input_names: self.flattened_argument_names(),
structure: SparseJacobianStructure {
rows: self.shape.0,
cols: self.shape.1,
row_indices: self.entries.iter().map(|entry| entry.row).collect(),
col_indices: self.entries.iter().map(|entry| entry.col).collect(),
},
chunks,
}
}
}
impl<'a> BandedJacobianTask<'a> {
pub fn runtime_plan(&self, strategy: BandedChunkingStrategy) -> BandedJacobianRuntimePlan<'a> {
let chunks: Vec<BandedJacobianValuesChunkPlan<'a>> = self
.chunk_with_strategy(strategy)
.into_iter()
.map(|chunk| BandedJacobianValuesChunkPlan {
fn_name: chunk.chunk_fn_name(),
value_offset: chunk.value_offset,
entries: chunk.entries.clone(),
plan: chunk.plan(),
})
.collect();
BandedJacobianRuntimePlan {
fn_name: self.fn_name,
input_names: self.flattened_argument_names(),
structure: BandedJacobianStructure {
rows: self.shape.0,
cols: self.shape.1,
kl: self.kl,
ku: self.ku,
diagonal_offsets: self.entries.iter().map(|entry| entry.diag_offset).collect(),
diagonal_positions: self
.entries
.iter()
.map(|entry| entry.diag_position)
.collect(),
},
compact: self.compact,
chunks,
}
}
}
impl<'a> JacobianTask<'a> {
pub fn runtime_plan(
&self,
strategy: DenseJacobianChunkingStrategy,
) -> DenseJacobianRuntimePlan<'a> {
let rows = self.jacobian.len();
let cols = self.jacobian.first().map_or(0, |row| row.len());
let rows_per_chunk = match strategy {
DenseJacobianChunkingStrategy::Whole => rows.max(1),
DenseJacobianChunkingStrategy::ByTargetChunkCount { target_chunks } => {
assert!(target_chunks > 0, "target_chunks must be positive");
rows.max(1).div_ceil(target_chunks).max(1)
}
DenseJacobianChunkingStrategy::ByRowCount { rows_per_chunk } => {
assert!(rows_per_chunk > 0, "rows_per_chunk must be positive");
rows_per_chunk
}
};
let chunks = self
.jacobian
.chunks(rows_per_chunk)
.enumerate()
.map(|(chunk_index, jacobian_rows)| {
let row_offset = chunk_index * rows_per_chunk;
let value_offset = row_offset * cols;
let fn_name = if chunk_index == 0 && jacobian_rows.len() == rows {
self.fn_name.to_string()
} else {
format!("{}_chunk_{}", self.fn_name, chunk_index)
};
let chunk_task = JacobianTask {
fn_name: self.fn_name,
jacobian: jacobian_rows,
variables: self.variables,
params: self.params,
};
let mut plan = chunk_task.plan();
plan.fn_name = Cow::Owned(fn_name.clone());
DenseJacobianChunkPlan {
fn_name,
row_offset,
value_offset,
jacobian_rows,
plan,
}
})
.collect();
DenseJacobianRuntimePlan {
fn_name: self.fn_name,
rows,
cols,
input_names: self.flattened_argument_names(),
chunks,
}
}
}
impl<'a> IvpJacobianTask<'a> {
pub fn runtime_plan(
&self,
strategy: DenseJacobianChunkingStrategy,
) -> DenseJacobianRuntimePlan<'a> {
let rows = self.jacobian.len();
let cols = self.jacobian.first().map_or(0, |row| row.len());
let rows_per_chunk = match strategy {
DenseJacobianChunkingStrategy::Whole => rows.max(1),
DenseJacobianChunkingStrategy::ByTargetChunkCount { target_chunks } => {
assert!(target_chunks > 0, "target_chunks must be positive");
rows.max(1).div_ceil(target_chunks).max(1)
}
DenseJacobianChunkingStrategy::ByRowCount { rows_per_chunk } => {
assert!(rows_per_chunk > 0, "rows_per_chunk must be positive");
rows_per_chunk
}
};
let chunks = self
.jacobian
.chunks(rows_per_chunk)
.enumerate()
.map(|(chunk_index, jacobian_rows)| {
let row_offset = chunk_index * rows_per_chunk;
let value_offset = row_offset * cols;
let fn_name = if chunk_index == 0 && jacobian_rows.len() == rows {
self.fn_name.to_string()
} else {
format!("{}_chunk_{}", self.fn_name, chunk_index)
};
let chunk_task = IvpJacobianTask {
fn_name: self.fn_name,
time_arg: self.time_arg,
jacobian: jacobian_rows,
variables: self.variables,
params: self.params,
};
let mut plan = chunk_task.plan();
plan.fn_name = Cow::Owned(fn_name.clone());
DenseJacobianChunkPlan {
fn_name,
row_offset,
value_offset,
jacobian_rows,
plan,
}
})
.collect();
DenseJacobianRuntimePlan {
fn_name: self.fn_name,
rows,
cols,
input_names: self.flattened_argument_names_with_time(),
chunks,
}
}
}
pub fn extract_sparse_entries_from_dense_jacobian<'a>(
jacobian: &'a [Vec<Expr>],
) -> Vec<SparseExprEntry<'a>> {
let mut entries = Vec::new();
for (row, dense_row) in jacobian.iter().enumerate() {
for (col, expr) in dense_row.iter().enumerate() {
if !expr.is_zero() {
entries.push(SparseExprEntry { row, col, expr });
}
}
}
entries
}
pub fn recommended_row_chunking_for_parallelism(
total_rows: usize,
chunks_per_worker: usize,
) -> SparseChunkingStrategy {
assert!(total_rows > 0, "total_rows must be positive");
assert!(chunks_per_worker > 0, "chunks_per_worker must be positive");
let workers = std::thread::available_parallelism()
.map(|parallelism| parallelism.get())
.unwrap_or(1);
let target_chunks = (workers * chunks_per_worker).max(1);
let rows_per_chunk = total_rows.div_ceil(target_chunks).max(1);
SparseChunkingStrategy::ByRowCount { rows_per_chunk }
}
pub fn recommended_banded_chunking_for_parallelism(
total_diagonals: usize,
chunks_per_worker: usize,
) -> BandedChunkingStrategy {
assert!(total_diagonals > 0, "total_diagonals must be positive");
assert!(chunks_per_worker > 0, "chunks_per_worker must be positive");
let workers = std::thread::available_parallelism()
.map(|parallelism| parallelism.get())
.unwrap_or(1);
let target_chunks = (workers * chunks_per_worker).max(1);
let diagonals_per_chunk = total_diagonals.div_ceil(target_chunks).max(1);
BandedChunkingStrategy::ByDiagonalCount {
diagonals_per_chunk,
}
}
pub fn recommended_row_chunking_for_auto_parallelism(total_rows: usize) -> SparseChunkingStrategy {
assert!(total_rows > 0, "total_rows must be positive");
let workers = rayon_overhead_baseline().workers.max(1);
let rows_per_chunk = total_rows.div_ceil(workers).max(1);
SparseChunkingStrategy::ByRowCount { rows_per_chunk }
}
pub fn recommended_residual_chunking_for_parallelism(
total_outputs: usize,
chunks_per_worker: usize,
) -> ResidualChunkingStrategy {
assert!(total_outputs > 0, "total_outputs must be positive");
assert!(chunks_per_worker > 0, "chunks_per_worker must be positive");
let workers = std::thread::available_parallelism()
.map(|parallelism| parallelism.get())
.unwrap_or(1);
let target_chunks = (workers * chunks_per_worker).max(1);
let max_outputs_per_chunk = total_outputs.div_ceil(target_chunks).max(1);
ResidualChunkingStrategy::ByOutputCount {
max_outputs_per_chunk,
}
}
pub fn recommended_residual_chunking_for_auto_parallelism(
total_outputs: usize,
) -> ResidualChunkingStrategy {
assert!(total_outputs > 0, "total_outputs must be positive");
let workers = rayon_overhead_baseline().workers.max(1);
let max_outputs_per_chunk = total_outputs.div_ceil(workers).max(1);
ResidualChunkingStrategy::ByOutputCount {
max_outputs_per_chunk,
}
}
pub fn recommended_dense_jacobian_chunking_for_parallelism(
total_rows: usize,
chunks_per_worker: usize,
) -> DenseJacobianChunkingStrategy {
assert!(total_rows > 0, "total_rows must be positive");
assert!(chunks_per_worker > 0, "chunks_per_worker must be positive");
let workers = std::thread::available_parallelism()
.map(|parallelism| parallelism.get())
.unwrap_or(1);
let target_chunks = (workers * chunks_per_worker).max(1);
let rows_per_chunk = total_rows.div_ceil(target_chunks).max(1);
DenseJacobianChunkingStrategy::ByRowCount { rows_per_chunk }
}
pub fn recommended_dense_jacobian_chunking_for_auto_parallelism(
total_rows: usize,
) -> DenseJacobianChunkingStrategy {
assert!(total_rows > 0, "total_rows must be positive");
let workers = rayon_overhead_baseline().workers.max(1);
let rows_per_chunk = total_rows.div_ceil(workers).max(1);
DenseJacobianChunkingStrategy::ByRowCount { rows_per_chunk }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::symbolic::symbolic_engine::Expr;
#[test]
fn runtime_arguments_flatten_params_before_variables() {
let args = RuntimeArguments::new(&[10.0, 20.0], Some(&[1.5, 2.5]));
assert_eq!(args.total_len(), 4);
assert_eq!(args.flatten(), vec![1.5, 2.5, 10.0, 20.0]);
}
#[test]
fn residual_runtime_plan_hides_chunking_behind_output_offsets() {
let residuals = vec![
Expr::Var("y0".to_string()),
Expr::Var("y1".to_string()),
Expr::Var("y2".to_string()),
];
let task = ResidualTask {
fn_name: "eval_residual",
residuals: &residuals,
variables: &["y0", "y1", "y2"],
params: Some(&["alpha"]),
};
let plan = task.runtime_plan(ResidualChunkingStrategy::ByOutputCount {
max_outputs_per_chunk: 2,
});
assert_eq!(plan.fn_name, "eval_residual");
assert_eq!(plan.len(), 3);
assert_eq!(plan.input_names, vec!["alpha", "y0", "y1", "y2"]);
assert_eq!(plan.chunks.len(), 2);
assert_eq!(plan.chunks[0].output_offset, 0);
assert_eq!(plan.chunks[0].residuals.len(), 2);
assert_eq!(plan.chunks[1].output_offset, 2);
assert_eq!(plan.chunks[1].residuals.len(), 1);
}
#[test]
fn sparse_runtime_plan_assembles_sparse_col_mat_from_global_value_order() {
let e0 = Expr::Var("y0".to_string());
let e1 = Expr::Var("y1".to_string());
let e2 = Expr::Var("y2".to_string());
let entries = vec![
SparseExprEntry {
row: 0,
col: 0,
expr: &e0,
},
SparseExprEntry {
row: 1,
col: 2,
expr: &e1,
},
SparseExprEntry {
row: 2,
col: 1,
expr: &e2,
},
];
let task = SparseJacobianTask {
fn_name: "eval_sparse_values",
shape: (3, 3),
entries: &entries,
variables: &["y0", "y1", "y2"],
params: Some(&["alpha"]),
};
let runtime_plan = task.runtime_plan(SparseChunkingStrategy::ByNonZeroCount {
max_entries_per_chunk: 2,
});
assert_eq!(runtime_plan.fn_name, "eval_sparse_values");
assert_eq!(runtime_plan.nnz(), 3);
assert_eq!(runtime_plan.input_names, vec!["alpha", "y0", "y1", "y2"]);
assert_eq!(runtime_plan.chunks.len(), 2);
assert_eq!(runtime_plan.chunks[0].value_range(), 0..2);
assert_eq!(runtime_plan.chunks[1].value_range(), 2..3);
let matrix = runtime_plan.assemble_sparse_col_mat(&[10.0, 20.0, 30.0]);
assert_eq!(matrix.nrows(), 3);
assert_eq!(matrix.ncols(), 3);
assert_eq!(matrix.compute_nnz(), 3);
}
#[test]
fn dense_runtime_plan_assembles_dense_matrix_from_row_major_values() {
let jacobian = vec![
vec![Expr::Var("y0".to_string()), Expr::Const(1.0)],
vec![Expr::Const(2.0), Expr::Var("y1".to_string())],
vec![Expr::Const(3.0), Expr::Const(4.0)],
];
let task = JacobianTask {
fn_name: "eval_dense_jacobian",
jacobian: &jacobian,
variables: &["y0", "y1"],
params: Some(&["alpha"]),
};
let plan =
task.runtime_plan(DenseJacobianChunkingStrategy::ByRowCount { rows_per_chunk: 2 });
assert_eq!(plan.fn_name, "eval_dense_jacobian");
assert_eq!(plan.rows, 3);
assert_eq!(plan.cols, 2);
assert_eq!(plan.input_names, vec!["alpha", "y0", "y1"]);
assert_eq!(plan.chunks.len(), 2);
assert_eq!(plan.chunks[0].row_offset, 0);
assert_eq!(plan.chunks[0].value_range(), 0..4);
assert_eq!(plan.chunks[1].row_offset, 2);
assert_eq!(plan.chunks[1].value_range(), 4..6);
let matrix = plan.assemble_dense_matrix(&[10.0, 11.0, 20.0, 21.0, 30.0, 31.0]);
assert_eq!(matrix.nrows(), 3);
assert_eq!(matrix.ncols(), 2);
assert_eq!(matrix[(0, 0)], 10.0);
assert_eq!(matrix[(2, 1)], 31.0);
}
#[test]
fn extract_sparse_entries_from_dense_jacobian_skips_symbolic_zeros() {
let jacobian = vec![
vec![Expr::Const(0.0), Expr::Var("y0".to_string())],
vec![Expr::Const(2.0), Expr::Const(0.0)],
];
let entries = extract_sparse_entries_from_dense_jacobian(&jacobian);
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].row, 0);
assert_eq!(entries[0].col, 1);
assert_eq!(entries[1].row, 1);
assert_eq!(entries[1].col, 0);
}
#[test]
fn recommended_row_chunking_for_parallelism_returns_row_strategy() {
let strategy = recommended_row_chunking_for_parallelism(128, 4);
match strategy {
SparseChunkingStrategy::ByRowCount { rows_per_chunk } => {
assert!(rows_per_chunk >= 1);
assert!(rows_per_chunk <= 128);
}
_ => panic!("expected row-count chunking strategy"),
}
}
#[test]
fn recommended_residual_chunking_for_parallelism_returns_output_strategy() {
let strategy = recommended_residual_chunking_for_parallelism(256, 4);
match strategy {
ResidualChunkingStrategy::ByOutputCount {
max_outputs_per_chunk,
} => {
assert!(max_outputs_per_chunk >= 1);
assert!(max_outputs_per_chunk <= 256);
}
_ => panic!("expected output-count chunking strategy"),
}
}
#[test]
fn recommended_dense_jacobian_chunking_for_parallelism_returns_row_strategy() {
let strategy = recommended_dense_jacobian_chunking_for_parallelism(256, 4);
match strategy {
DenseJacobianChunkingStrategy::ByRowCount { rows_per_chunk } => {
assert!(rows_per_chunk >= 1);
assert!(rows_per_chunk <= 256);
}
_ => panic!("expected row-count chunking strategy"),
}
}
#[test]
fn banded_runtime_plan_assembles_native_banded_storage() {
let e0 = Expr::Const(10.0);
let e1 = Expr::Const(20.0);
let e2 = Expr::Const(30.0);
let entries = vec![
BandedExprEntry {
row: 1,
col: 0,
diag_offset: -1,
diag_position: 0,
expr: &e0,
},
BandedExprEntry {
row: 0,
col: 0,
diag_offset: 0,
diag_position: 0,
expr: &e1,
},
BandedExprEntry {
row: 0,
col: 1,
diag_offset: 1,
diag_position: 0,
expr: &e2,
},
];
let task = BandedJacobianTask {
fn_name: "eval_banded_values",
shape: (2, 2),
kl: 1,
ku: 1,
compact: false,
entries: &entries,
variables: &["y0", "y1"],
params: None,
};
let runtime_plan = task.runtime_plan(BandedChunkingStrategy::ByDiagonalCount {
diagonals_per_chunk: 1,
});
let asm = runtime_plan.assemble_banded_assembly(&[10.0, 20.0, 30.0]);
assert_eq!(runtime_plan.nnz(), 3);
assert_eq!(runtime_plan.chunks.len(), 3);
assert_eq!(asm.get(1, 0).unwrap(), 10.0);
assert_eq!(asm.get(0, 0).unwrap(), 20.0);
assert_eq!(asm.get(0, 1).unwrap(), 30.0);
}
#[test]
fn banded_structure_assembles_values_in_metadata_order_not_diagonal_major_order() {
let structure = BandedJacobianStructure {
rows: 2,
cols: 2,
kl: 1,
ku: 1,
diagonal_offsets: vec![0, 1, -1],
diagonal_positions: vec![0, 0, 0],
};
let asm = structure.assemble_banded_assembly(&[20.0, 30.0, 10.0]);
assert_eq!(asm.get(0, 0).unwrap(), 20.0);
assert_eq!(asm.get(0, 1).unwrap(), 30.0);
assert_eq!(asm.get(1, 0).unwrap(), 10.0);
}
#[test]
fn banded_structure_exposes_compact_native_storage_with_boundary_slots() {
let structure = BandedJacobianStructure {
rows: 3,
cols: 3,
kl: 1,
ku: 1,
diagonal_offsets: vec![0, 1, -1, 0],
diagonal_positions: vec![0, 0, 0, 2],
};
let compact = structure
.try_assemble_compact_banded_storage(&[10.0, 20.0, 30.0, 40.0])
.unwrap();
assert_eq!(structure.compact_storage_len().unwrap(), 9);
assert_eq!(
compact.as_slice(),
&[0.0, 20.0, 0.0, 10.0, 0.0, 40.0, 30.0, 0.0, 0.0]
);
}
#[test]
fn compact_banded_runtime_plan_accepts_only_complete_storage() {
let input_names = ["y0", "y1", "y2"];
let plan = BandedCompactJacobianRuntimePlan {
fn_name: "eval_native_banded_values",
input_names: &input_names,
structure: BandedCompactJacobianStructure {
rows: 3,
cols: 3,
kl: 1,
ku: 1,
},
};
assert_eq!(plan.output_len().unwrap(), 9);
let compact = plan
.try_assemble_compact_storage(vec![0.0; 9])
.expect("complete compact callback output should be accepted");
assert_eq!(compact.as_slice().len(), 9);
let assembly = plan
.structure
.try_to_assembly(&[0.0, 1.0, 0.0, 2.0, 3.0, 4.0, 4.0, 5.0, 0.0])
.expect("compact storage should convert directly to banded assembly");
assert_eq!(assembly.get(0, 1).unwrap(), 1.0);
assert_eq!(assembly.get(1, 0).unwrap(), 4.0);
assert_eq!(assembly.get(2, 1).unwrap(), 5.0);
assert!(matches!(
plan.try_assemble_compact_storage(vec![0.0; 7]),
Err(BandedError::DimensionMismatch)
));
}
#[test]
fn banded_structure_typed_assembly_rejects_invalid_metadata() {
let duplicate = BandedJacobianStructure {
rows: 2,
cols: 2,
kl: 1,
ku: 1,
diagonal_offsets: vec![0, 0],
diagonal_positions: vec![0, 0],
};
assert!(
duplicate
.try_assemble_compact_banded_storage(&[1.0, 2.0])
.is_err()
);
let out_of_band = BandedJacobianStructure {
rows: 2,
cols: 2,
kl: 0,
ku: 0,
diagonal_offsets: vec![1],
diagonal_positions: vec![0],
};
assert!(
out_of_band
.try_assemble_compact_banded_storage(&[1.0])
.is_err()
);
let rectangular = BandedJacobianStructure {
rows: 2,
cols: 3,
kl: 1,
ku: 1,
diagonal_offsets: vec![],
diagonal_positions: vec![],
};
assert!(rectangular.compact_storage_len().is_err());
}
#[test]
fn recommended_banded_chunking_for_parallelism_returns_diagonal_strategy() {
let strategy = recommended_banded_chunking_for_parallelism(17, 3);
match strategy {
BandedChunkingStrategy::ByDiagonalCount {
diagonals_per_chunk,
} => {
assert!(diagonals_per_chunk >= 1);
assert!(diagonals_per_chunk <= 17);
}
_ => panic!("expected diagonal-count chunking strategy"),
}
}
#[test]
fn residual_runtime_plan_supports_target_chunk_count() {
let residuals = vec![
Expr::Var("y0".to_string()),
Expr::Var("y1".to_string()),
Expr::Var("y2".to_string()),
Expr::Var("y3".to_string()),
];
let task = ResidualTask {
fn_name: "eval_residual",
residuals: &residuals,
variables: &["y0", "y1", "y2", "y3"],
params: None,
};
let plan =
task.runtime_plan(ResidualChunkingStrategy::ByTargetChunkCount { target_chunks: 2 });
assert_eq!(plan.chunks.len(), 2);
assert_eq!(plan.chunks[0].residuals.len(), 2);
assert_eq!(plan.chunks[1].residuals.len(), 2);
}
}