use crate::symbolic::symbolic_engine::Expr;
use std::borrow::Cow;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TaskArguments<'a> {
pub variables: &'a [&'a str],
pub params: Option<&'a [&'a str]>,
}
impl<'a> TaskArguments<'a> {
pub fn new(variables: &'a [&'a str], params: Option<&'a [&'a str]>) -> Self {
Self { variables, params }
}
pub fn variable_count(&self) -> usize {
self.variables.len()
}
pub fn parameter_count(&self) -> usize {
self.params.map_or(0, |params| params.len())
}
pub fn total_argument_count(&self) -> usize {
self.variable_count() + self.parameter_count()
}
pub fn flattened_names(&self) -> Vec<&'a str> {
let mut names = Vec::with_capacity(self.total_argument_count());
if let Some(params) = self.params {
names.extend(params.iter().copied());
}
names.extend(self.variables.iter().copied());
names
}
}
pub trait CodegenTask<'a> {
fn fn_name(&self) -> &'a str;
fn variables(&self) -> &'a [&'a str];
fn params(&self) -> Option<&'a [&'a str]>;
fn arguments(&self) -> TaskArguments<'a> {
TaskArguments::new(self.variables(), self.params())
}
fn flattened_argument_names(&self) -> Vec<&'a str> {
self.arguments().flattened_names()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodegenTaskKind {
Residual,
DenseJacobian,
IvpResidual,
IvpJacobian,
SparseJacobianValues,
BandedJacobianValues,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodegenOutputLayout {
Vector {
len: usize,
},
Matrix {
rows: usize,
cols: usize,
},
SparseValues {
rows: usize,
cols: usize,
nnz: usize,
},
BandedValues {
rows: usize,
cols: usize,
kl: usize,
ku: usize,
slots: usize,
},
BandedCompactValues {
rows: usize,
cols: usize,
kl: usize,
ku: usize,
slots: usize,
},
}
#[derive(Debug, Clone, Copy)]
pub struct PlannedOutput<'a> {
pub expr: &'a Expr,
pub coordinate: Option<(usize, usize)>,
}
#[derive(Debug, Clone)]
pub struct CodegenTaskPlan<'a> {
pub fn_name: Cow<'a, str>,
pub kind: CodegenTaskKind,
pub input_names: Vec<&'a str>,
pub outputs: Vec<PlannedOutput<'a>>,
pub layout: CodegenOutputLayout,
}
impl<'a> CodegenTaskPlan<'a> {
pub fn output_exprs(&self) -> Vec<&'a Expr> {
self.outputs.iter().map(|output| output.expr).collect()
}
}
#[derive(Debug, Clone)]
pub struct ResidualTask<'a> {
pub fn_name: &'a str,
pub residuals: &'a [Expr],
pub variables: &'a [&'a str],
pub params: Option<&'a [&'a str]>,
}
impl<'a> CodegenTask<'a> for ResidualTask<'a> {
fn fn_name(&self) -> &'a str {
self.fn_name
}
fn variables(&self) -> &'a [&'a str] {
self.variables
}
fn params(&self) -> Option<&'a [&'a str]> {
self.params
}
}
impl<'a> ResidualTask<'a> {
pub fn plan(&self) -> CodegenTaskPlan<'a> {
CodegenTaskPlan {
fn_name: Cow::Borrowed(self.fn_name()),
kind: CodegenTaskKind::Residual,
input_names: self.flattened_argument_names(),
outputs: self
.residuals
.iter()
.map(|expr| PlannedOutput {
expr,
coordinate: None,
})
.collect(),
layout: CodegenOutputLayout::Vector {
len: self.residuals.len(),
},
}
}
}
#[derive(Debug, Clone)]
pub struct JacobianTask<'a> {
pub fn_name: &'a str,
pub jacobian: &'a [Vec<Expr>],
pub variables: &'a [&'a str],
pub params: Option<&'a [&'a str]>,
}
impl<'a> CodegenTask<'a> for JacobianTask<'a> {
fn fn_name(&self) -> &'a str {
self.fn_name
}
fn variables(&self) -> &'a [&'a str] {
self.variables
}
fn params(&self) -> Option<&'a [&'a str]> {
self.params
}
}
impl<'a> JacobianTask<'a> {
pub fn plan(&self) -> CodegenTaskPlan<'a> {
let rows = self.jacobian.len();
let cols = self.jacobian.first().map_or(0, |row| row.len());
let mut outputs = Vec::with_capacity(rows * cols);
for (row_idx, row) in self.jacobian.iter().enumerate() {
for (col_idx, expr) in row.iter().enumerate() {
outputs.push(PlannedOutput {
expr,
coordinate: Some((row_idx, col_idx)),
});
}
}
CodegenTaskPlan {
fn_name: Cow::Borrowed(self.fn_name()),
kind: CodegenTaskKind::DenseJacobian,
input_names: self.flattened_argument_names(),
outputs,
layout: CodegenOutputLayout::Matrix { rows, cols },
}
}
}
#[derive(Debug, Clone)]
pub struct IvpResidualTask<'a> {
pub fn_name: &'a str,
pub time_arg: &'a str,
pub residuals: &'a [Expr],
pub variables: &'a [&'a str],
pub params: Option<&'a [&'a str]>,
}
impl<'a> CodegenTask<'a> for IvpResidualTask<'a> {
fn fn_name(&self) -> &'a str {
self.fn_name
}
fn variables(&self) -> &'a [&'a str] {
self.variables
}
fn params(&self) -> Option<&'a [&'a str]> {
self.params
}
}
impl<'a> IvpResidualTask<'a> {
pub fn flattened_argument_names_with_time(&self) -> Vec<&'a str> {
let mut names = Vec::with_capacity(1 + self.arguments().total_argument_count());
names.push(self.time_arg);
names.extend(self.flattened_argument_names());
names
}
pub fn plan(&self) -> CodegenTaskPlan<'a> {
CodegenTaskPlan {
fn_name: Cow::Borrowed(self.fn_name()),
kind: CodegenTaskKind::IvpResidual,
input_names: self.flattened_argument_names_with_time(),
outputs: self
.residuals
.iter()
.map(|expr| PlannedOutput {
expr,
coordinate: None,
})
.collect(),
layout: CodegenOutputLayout::Vector {
len: self.residuals.len(),
},
}
}
}
#[derive(Debug, Clone)]
pub struct IvpJacobianTask<'a> {
pub fn_name: &'a str,
pub time_arg: &'a str,
pub jacobian: &'a [Vec<Expr>],
pub variables: &'a [&'a str],
pub params: Option<&'a [&'a str]>,
}
impl<'a> CodegenTask<'a> for IvpJacobianTask<'a> {
fn fn_name(&self) -> &'a str {
self.fn_name
}
fn variables(&self) -> &'a [&'a str] {
self.variables
}
fn params(&self) -> Option<&'a [&'a str]> {
self.params
}
}
impl<'a> IvpJacobianTask<'a> {
pub fn flattened_argument_names_with_time(&self) -> Vec<&'a str> {
let mut names = Vec::with_capacity(1 + self.arguments().total_argument_count());
names.push(self.time_arg);
names.extend(self.flattened_argument_names());
names
}
pub fn plan(&self) -> CodegenTaskPlan<'a> {
let rows = self.jacobian.len();
let cols = self.jacobian.first().map_or(0, |row| row.len());
let mut outputs = Vec::with_capacity(rows * cols);
for (row_idx, row) in self.jacobian.iter().enumerate() {
for (col_idx, expr) in row.iter().enumerate() {
outputs.push(PlannedOutput {
expr,
coordinate: Some((row_idx, col_idx)),
});
}
}
CodegenTaskPlan {
fn_name: Cow::Borrowed(self.fn_name()),
kind: CodegenTaskKind::IvpJacobian,
input_names: self.flattened_argument_names_with_time(),
outputs,
layout: CodegenOutputLayout::Matrix { rows, cols },
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SparseExprEntry<'a> {
pub row: usize,
pub col: usize,
pub expr: &'a Expr,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BandedExprEntry<'a> {
pub row: usize,
pub col: usize,
pub diag_offset: isize,
pub diag_position: usize,
pub expr: &'a Expr,
}
#[derive(Debug, Clone)]
pub struct SparseJacobianTask<'a> {
pub fn_name: &'a str,
pub shape: (usize, usize),
pub entries: &'a [SparseExprEntry<'a>],
pub variables: &'a [&'a str],
pub params: Option<&'a [&'a str]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SparseChunkingStrategy {
Whole,
ByTargetChunkCount { target_chunks: usize },
ByNonZeroCount { max_entries_per_chunk: usize },
ByRowCount { rows_per_chunk: usize },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BandedChunkingStrategy {
Whole,
ByTargetChunkCount { target_chunks: usize },
ByDiagonalCount { diagonals_per_chunk: usize },
}
#[derive(Debug, Clone)]
pub struct SparseJacobianChunkTask<'a> {
pub base_fn_name: &'a str,
pub chunk_index: usize,
pub entry_offset: usize,
pub shape: (usize, usize),
pub entries: Vec<SparseExprEntry<'a>>,
pub variables: &'a [&'a str],
pub params: Option<&'a [&'a str]>,
}
#[derive(Debug, Clone)]
pub struct BandedJacobianTask<'a> {
pub fn_name: &'a str,
pub shape: (usize, usize),
pub kl: usize,
pub ku: usize,
pub compact: bool,
pub entries: &'a [BandedExprEntry<'a>],
pub variables: &'a [&'a str],
pub params: Option<&'a [&'a str]>,
}
#[derive(Debug, Clone)]
pub struct BandedJacobianChunkTask<'a> {
pub base_fn_name: &'a str,
pub chunk_index: usize,
pub value_offset: usize,
pub shape: (usize, usize),
pub kl: usize,
pub ku: usize,
pub compact: bool,
pub diagonal_range: std::ops::RangeInclusive<isize>,
pub entries: Vec<BandedExprEntry<'a>>,
pub variables: &'a [&'a str],
pub params: Option<&'a [&'a str]>,
}
impl<'a> CodegenTask<'a> for SparseJacobianTask<'a> {
fn fn_name(&self) -> &'a str {
self.fn_name
}
fn variables(&self) -> &'a [&'a str] {
self.variables
}
fn params(&self) -> Option<&'a [&'a str]> {
self.params
}
}
impl<'a> SparseJacobianTask<'a> {
pub fn plan(&self) -> CodegenTaskPlan<'a> {
CodegenTaskPlan {
fn_name: Cow::Borrowed(self.fn_name()),
kind: CodegenTaskKind::SparseJacobianValues,
input_names: self.flattened_argument_names(),
outputs: self
.entries
.iter()
.map(|entry| PlannedOutput {
expr: entry.expr,
coordinate: Some((entry.row, entry.col)),
})
.collect(),
layout: CodegenOutputLayout::SparseValues {
rows: self.shape.0,
cols: self.shape.1,
nnz: self.entries.len(),
},
}
}
pub fn chunk_by_nnz(&self, max_entries_per_chunk: usize) -> Vec<SparseJacobianChunkTask<'a>> {
assert!(
max_entries_per_chunk > 0,
"max_entries_per_chunk must be positive"
);
self.entries
.chunks(max_entries_per_chunk)
.enumerate()
.map(|(chunk_index, entries)| SparseJacobianChunkTask {
base_fn_name: self.fn_name,
chunk_index,
entry_offset: chunk_index * max_entries_per_chunk,
shape: self.shape,
entries: entries.to_vec(),
variables: self.variables,
params: self.params,
})
.collect()
}
pub fn chunk_with_strategy(
&self,
strategy: SparseChunkingStrategy,
) -> Vec<SparseJacobianChunkTask<'a>> {
match strategy {
SparseChunkingStrategy::Whole => vec![SparseJacobianChunkTask {
base_fn_name: self.fn_name,
chunk_index: 0,
entry_offset: 0,
shape: self.shape,
entries: self.entries.to_vec(),
variables: self.variables,
params: self.params,
}],
SparseChunkingStrategy::ByTargetChunkCount { target_chunks } => {
assert!(target_chunks > 0, "target_chunks must be positive");
let max_entries_per_chunk =
self.entries.len().max(1).div_ceil(target_chunks).max(1);
self.chunk_with_strategy(SparseChunkingStrategy::ByNonZeroCount {
max_entries_per_chunk,
})
}
SparseChunkingStrategy::ByNonZeroCount {
max_entries_per_chunk,
} => self.chunk_by_nnz(max_entries_per_chunk),
SparseChunkingStrategy::ByRowCount { rows_per_chunk } => {
assert!(rows_per_chunk > 0, "rows_per_chunk must be positive");
let mut chunk_entries: Vec<Vec<(usize, SparseExprEntry<'a>)>> = Vec::new();
let mut chunk_offsets: Vec<usize> = Vec::new();
for (entry_index, entry) in self.entries.iter().copied().enumerate() {
let bucket = entry.row / rows_per_chunk;
if bucket >= chunk_entries.len() {
chunk_entries.resize_with(bucket + 1, Vec::new);
chunk_offsets.resize(bucket + 1, 0);
}
if chunk_entries[bucket].is_empty() {
chunk_offsets[bucket] = entry_index;
}
chunk_entries[bucket].push((entry_index, entry));
}
let non_empty_chunks = chunk_entries
.iter()
.filter(|entries| !entries.is_empty())
.collect::<Vec<_>>();
let all_chunks_are_contiguous = non_empty_chunks.iter().all(|entries| {
let start = entries.first().map(|(index, _)| *index).unwrap_or(0);
entries
.iter()
.enumerate()
.all(|(local_index, (entry_index, _))| *entry_index == start + local_index)
});
if !all_chunks_are_contiguous {
let target_chunks = self.shape.0.max(1).div_ceil(rows_per_chunk).max(1);
let max_entries_per_chunk =
self.entries.len().max(1).div_ceil(target_chunks).max(1);
return self.chunk_with_strategy(SparseChunkingStrategy::ByNonZeroCount {
max_entries_per_chunk,
});
}
chunk_entries
.into_iter()
.enumerate()
.filter(|(_, entries)| !entries.is_empty())
.map(|(chunk_index, entries)| SparseJacobianChunkTask {
base_fn_name: self.fn_name,
chunk_index,
entry_offset: chunk_offsets[chunk_index],
shape: self.shape,
entries: entries.into_iter().map(|(_, entry)| entry).collect(),
variables: self.variables,
params: self.params,
})
.collect()
}
}
}
}
impl<'a> CodegenTask<'a> for BandedJacobianTask<'a> {
fn fn_name(&self) -> &'a str {
self.fn_name
}
fn variables(&self) -> &'a [&'a str] {
self.variables
}
fn params(&self) -> Option<&'a [&'a str]> {
self.params
}
}
impl<'a> BandedJacobianTask<'a> {
pub fn plan(&self) -> CodegenTaskPlan<'a> {
CodegenTaskPlan {
fn_name: Cow::Borrowed(self.fn_name()),
kind: CodegenTaskKind::BandedJacobianValues,
input_names: self.flattened_argument_names(),
outputs: self
.entries
.iter()
.map(|entry| PlannedOutput {
expr: entry.expr,
coordinate: Some((entry.row, entry.col)),
})
.collect(),
layout: if self.compact {
CodegenOutputLayout::BandedCompactValues {
rows: self.shape.0,
cols: self.shape.1,
kl: self.kl,
ku: self.ku,
slots: self.entries.len(),
}
} else {
CodegenOutputLayout::BandedValues {
rows: self.shape.0,
cols: self.shape.1,
kl: self.kl,
ku: self.ku,
slots: self.entries.len(),
}
},
}
}
pub fn chunk_with_strategy(
&self,
strategy: BandedChunkingStrategy,
) -> Vec<BandedJacobianChunkTask<'a>> {
let total_diagonals = self.kl + self.ku + 1;
let diagonals_per_chunk = match strategy {
BandedChunkingStrategy::Whole => total_diagonals.max(1),
BandedChunkingStrategy::ByTargetChunkCount { target_chunks } => {
assert!(target_chunks > 0, "target_chunks must be positive");
total_diagonals.max(1).div_ceil(target_chunks).max(1)
}
BandedChunkingStrategy::ByDiagonalCount {
diagonals_per_chunk,
} => {
assert!(
diagonals_per_chunk > 0,
"diagonals_per_chunk must be positive"
);
diagonals_per_chunk
}
};
let mut chunk_entries: Vec<Vec<BandedExprEntry<'a>>> = Vec::new();
let mut chunk_offsets: Vec<usize> = Vec::new();
let mut chunk_diag_ranges: Vec<Option<(isize, isize)>> = Vec::new();
for (entry_index, entry) in self.entries.iter().copied().enumerate() {
let bucket = ((entry.diag_offset + self.kl as isize) as usize) / diagonals_per_chunk;
if bucket >= chunk_entries.len() {
chunk_entries.resize_with(bucket + 1, Vec::new);
chunk_offsets.resize(bucket + 1, 0);
chunk_diag_ranges.resize(bucket + 1, None);
}
if chunk_entries[bucket].is_empty() {
chunk_offsets[bucket] = entry_index;
}
chunk_entries[bucket].push(entry);
chunk_diag_ranges[bucket] = Some(match chunk_diag_ranges[bucket] {
Some((start, _)) => (start, entry.diag_offset),
None => (entry.diag_offset, entry.diag_offset),
});
}
chunk_entries
.into_iter()
.enumerate()
.filter(|(_, entries)| !entries.is_empty())
.map(|(chunk_index, entries)| {
let (diag_start, diag_end) = chunk_diag_ranges[chunk_index]
.expect("non-empty banded chunk must have a diagonal range");
BandedJacobianChunkTask {
base_fn_name: self.fn_name,
chunk_index,
value_offset: chunk_offsets[chunk_index],
shape: self.shape,
kl: self.kl,
ku: self.ku,
compact: self.compact,
diagonal_range: diag_start..=diag_end,
entries,
variables: self.variables,
params: self.params,
}
})
.collect()
}
}
impl<'a> CodegenTask<'a> for SparseJacobianChunkTask<'a> {
fn fn_name(&self) -> &'a str {
self.base_fn_name
}
fn variables(&self) -> &'a [&'a str] {
self.variables
}
fn params(&self) -> Option<&'a [&'a str]> {
self.params
}
}
impl<'a> SparseJacobianChunkTask<'a> {
pub fn chunk_fn_name(&self) -> String {
format!("{}_chunk_{}", self.base_fn_name, self.chunk_index)
}
pub fn plan(&self) -> CodegenTaskPlan<'a> {
CodegenTaskPlan {
fn_name: Cow::Owned(self.chunk_fn_name()),
kind: CodegenTaskKind::SparseJacobianValues,
input_names: self.flattened_argument_names(),
outputs: self
.entries
.iter()
.map(|entry| PlannedOutput {
expr: entry.expr,
coordinate: Some((entry.row, entry.col)),
})
.collect(),
layout: CodegenOutputLayout::SparseValues {
rows: self.shape.0,
cols: self.shape.1,
nnz: self.entries.len(),
},
}
}
}
impl<'a> CodegenTask<'a> for BandedJacobianChunkTask<'a> {
fn fn_name(&self) -> &'a str {
self.base_fn_name
}
fn variables(&self) -> &'a [&'a str] {
self.variables
}
fn params(&self) -> Option<&'a [&'a str]> {
self.params
}
}
impl<'a> BandedJacobianChunkTask<'a> {
pub fn chunk_fn_name(&self) -> String {
format!("{}_chunk_{}", self.base_fn_name, self.chunk_index)
}
pub fn plan(&self) -> CodegenTaskPlan<'a> {
CodegenTaskPlan {
fn_name: Cow::Owned(self.chunk_fn_name()),
kind: CodegenTaskKind::BandedJacobianValues,
input_names: self.flattened_argument_names(),
outputs: self
.entries
.iter()
.map(|entry| PlannedOutput {
expr: entry.expr,
coordinate: Some((entry.row, entry.col)),
})
.collect(),
layout: if self.compact {
CodegenOutputLayout::BandedCompactValues {
rows: self.shape.0,
cols: self.shape.1,
kl: self.kl,
ku: self.ku,
slots: self.entries.len(),
}
} else {
CodegenOutputLayout::BandedValues {
rows: self.shape.0,
cols: self.shape.1,
kl: self.kl,
ku: self.ku,
slots: self.entries.len(),
}
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn task_arguments_flatten_params_before_variables() {
let args = TaskArguments::new(&["y0", "y1"], Some(&["alpha", "beta"]));
assert_eq!(args.parameter_count(), 2);
assert_eq!(args.variable_count(), 2);
assert_eq!(args.total_argument_count(), 4);
assert_eq!(args.flattened_names(), vec!["alpha", "beta", "y0", "y1"]);
}
#[test]
fn task_arguments_handle_absent_params() {
let args = TaskArguments::new(&["x", "y"], None);
assert_eq!(args.parameter_count(), 0);
assert_eq!(args.total_argument_count(), 2);
assert_eq!(args.flattened_names(), vec!["x", "y"]);
}
#[test]
fn ivp_task_keeps_time_argument_outside_param_group() {
let jacobian = vec![vec![Expr::Const(1.0)]];
let task = IvpJacobianTask {
fn_name: "eval_ivp_jacobian",
time_arg: "t",
jacobian: &jacobian,
variables: &["y"],
params: Some(&["alpha"]),
};
assert_eq!(
task.flattened_argument_names_with_time(),
vec!["t", "alpha", "y"]
);
}
#[test]
fn residual_plan_keeps_param_then_variable_input_order() {
let residuals = vec![Expr::Const(1.0), Expr::Var("y".to_string())];
let task = ResidualTask {
fn_name: "eval_residual",
residuals: &residuals,
variables: &["y"],
params: Some(&["alpha"]),
};
let plan = task.plan();
assert_eq!(plan.fn_name, "eval_residual");
assert_eq!(plan.kind, CodegenTaskKind::Residual);
assert_eq!(plan.input_names, vec!["alpha", "y"]);
assert_eq!(plan.outputs.len(), 2);
assert_eq!(plan.layout, CodegenOutputLayout::Vector { len: 2 });
assert!(
plan.outputs
.iter()
.all(|output| output.coordinate.is_none())
);
}
#[test]
fn dense_jacobian_plan_flattens_row_major_with_coordinates() {
let jacobian = vec![
vec![Expr::Var("a".to_string()), Expr::Var("b".to_string())],
vec![Expr::Var("c".to_string()), Expr::Var("d".to_string())],
];
let task = JacobianTask {
fn_name: "eval_jacobian",
jacobian: &jacobian,
variables: &["x", "y"],
params: Some(&["alpha"]),
};
let plan = task.plan();
assert_eq!(plan.input_names, vec!["alpha", "x", "y"]);
assert_eq!(
plan.layout,
CodegenOutputLayout::Matrix { rows: 2, cols: 2 }
);
assert_eq!(plan.outputs.len(), 4);
assert_eq!(plan.outputs[0].coordinate, Some((0, 0)));
assert_eq!(plan.outputs[1].coordinate, Some((0, 1)));
assert_eq!(plan.outputs[2].coordinate, Some((1, 0)));
assert_eq!(plan.outputs[3].coordinate, Some((1, 1)));
}
#[test]
fn sparse_jacobian_plan_preserves_explicit_entry_order() {
let e0 = Expr::Var("j00".to_string());
let e1 = Expr::Var("j12".to_string());
let entries = vec![
SparseExprEntry {
row: 0,
col: 0,
expr: &e0,
},
SparseExprEntry {
row: 1,
col: 2,
expr: &e1,
},
];
let task = SparseJacobianTask {
fn_name: "eval_sparse_values",
shape: (3, 4),
entries: &entries,
variables: &["y0", "y1"],
params: None,
};
let plan = task.plan();
assert_eq!(
plan.layout,
CodegenOutputLayout::SparseValues {
rows: 3,
cols: 4,
nnz: 2
}
);
assert_eq!(plan.outputs.len(), 2);
assert_eq!(plan.outputs[0].coordinate, Some((0, 0)));
assert_eq!(plan.outputs[1].coordinate, Some((1, 2)));
assert!(std::ptr::eq(plan.outputs[0].expr, &e0));
assert!(std::ptr::eq(plan.outputs[1].expr, &e1));
}
#[test]
fn sparse_jacobian_chunking_preserves_order_and_params() {
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: 1,
expr: &e1,
},
SparseExprEntry {
row: 2,
col: 2,
expr: &e2,
},
];
let task = SparseJacobianTask {
fn_name: "eval_sparse_values",
shape: (4, 4),
entries: &entries,
variables: &["y0", "y1", "y2"],
params: Some(&["alpha"]),
};
let chunks = task.chunk_by_nnz(2);
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].chunk_index, 0);
assert_eq!(chunks[0].entry_offset, 0);
assert_eq!(chunks[0].entries.len(), 2);
assert_eq!(chunks[1].chunk_index, 1);
assert_eq!(chunks[1].entry_offset, 2);
assert_eq!(chunks[1].entries.len(), 1);
assert_eq!(
chunks[0].flattened_argument_names(),
vec!["alpha", "y0", "y1", "y2"]
);
assert_eq!(chunks[1].chunk_fn_name(), "eval_sparse_values_chunk_1");
let plan0 = chunks[0].plan();
let plan1 = chunks[1].plan();
assert_eq!(
plan0.layout,
CodegenOutputLayout::SparseValues {
rows: 4,
cols: 4,
nnz: 2
}
);
assert_eq!(
plan1.layout,
CodegenOutputLayout::SparseValues {
rows: 4,
cols: 4,
nnz: 1
}
);
assert_eq!(plan0.outputs[0].coordinate, Some((0, 0)));
assert_eq!(plan0.outputs[1].coordinate, Some((1, 1)));
assert_eq!(plan1.outputs[0].coordinate, Some((2, 2)));
}
#[test]
fn sparse_chunking_strategy_whole_preserves_single_chunk() {
let e0 = Expr::Var("y0".to_string());
let e1 = Expr::Var("y1".to_string());
let entries = vec![
SparseExprEntry {
row: 0,
col: 0,
expr: &e0,
},
SparseExprEntry {
row: 2,
col: 3,
expr: &e1,
},
];
let task = SparseJacobianTask {
fn_name: "eval_sparse_values",
shape: (4, 4),
entries: &entries,
variables: &["y0", "y1"],
params: None,
};
let chunks = task.chunk_with_strategy(SparseChunkingStrategy::Whole);
assert_eq!(chunks.len(), 1);
assert_eq!(chunks[0].entry_offset, 0);
assert_eq!(chunks[0].entries.len(), 2);
}
#[test]
fn sparse_chunking_strategy_by_row_count_groups_rows() {
let e0 = Expr::Var("y0".to_string());
let e1 = Expr::Var("y1".to_string());
let e2 = Expr::Var("y2".to_string());
let e3 = Expr::Var("y3".to_string());
let entries = vec![
SparseExprEntry {
row: 0,
col: 0,
expr: &e0,
},
SparseExprEntry {
row: 1,
col: 1,
expr: &e1,
},
SparseExprEntry {
row: 2,
col: 2,
expr: &e2,
},
SparseExprEntry {
row: 3,
col: 3,
expr: &e3,
},
];
let task = SparseJacobianTask {
fn_name: "eval_sparse_values",
shape: (4, 4),
entries: &entries,
variables: &["y0", "y1", "y2", "y3"],
params: Some(&["alpha"]),
};
let chunks =
task.chunk_with_strategy(SparseChunkingStrategy::ByRowCount { rows_per_chunk: 2 });
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].entry_offset, 0);
assert_eq!(chunks[0].entries.len(), 2);
assert_eq!(chunks[0].entries[0].row, 0);
assert_eq!(chunks[0].entries[1].row, 1);
assert_eq!(chunks[1].entry_offset, 2);
assert_eq!(chunks[1].entries.len(), 2);
assert_eq!(chunks[1].entries[0].row, 2);
assert_eq!(chunks[1].entries[1].row, 3);
assert_eq!(
chunks[1].flattened_argument_names(),
vec!["alpha", "y0", "y1", "y2", "y3"]
);
}
#[test]
fn sparse_chunking_strategy_by_target_chunk_count_creates_contiguous_value_groups() {
let e0 = Expr::Var("y0".to_string());
let e1 = Expr::Var("y1".to_string());
let e2 = Expr::Var("y2".to_string());
let e3 = Expr::Var("y3".to_string());
let entries = vec![
SparseExprEntry {
row: 0,
col: 0,
expr: &e0,
},
SparseExprEntry {
row: 1,
col: 1,
expr: &e1,
},
SparseExprEntry {
row: 2,
col: 2,
expr: &e2,
},
SparseExprEntry {
row: 3,
col: 3,
expr: &e3,
},
];
let task = SparseJacobianTask {
fn_name: "eval_sparse_values",
shape: (4, 4),
entries: &entries,
variables: &["y0", "y1", "y2", "y3"],
params: None,
};
let chunks = task
.chunk_with_strategy(SparseChunkingStrategy::ByTargetChunkCount { target_chunks: 2 });
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].entry_offset, 0);
assert_eq!(chunks[0].entries.len(), 2);
assert_eq!(chunks[1].entry_offset, 2);
assert_eq!(chunks[1].entries.len(), 2);
assert_eq!(chunks[0].entries[0].row, 0);
assert_eq!(chunks[1].entries[0].row, 2);
}
#[test]
fn sparse_target_chunking_keeps_value_offsets_contiguous_for_column_major_entries() {
let e0 = Expr::Var("y0".to_string());
let e1 = Expr::Var("y1".to_string());
let e2 = Expr::Var("y2".to_string());
let e3 = Expr::Var("y3".to_string());
let entries = vec![
SparseExprEntry {
row: 0,
col: 0,
expr: &e0,
},
SparseExprEntry {
row: 2,
col: 0,
expr: &e1,
},
SparseExprEntry {
row: 1,
col: 1,
expr: &e2,
},
SparseExprEntry {
row: 3,
col: 1,
expr: &e3,
},
];
let task = SparseJacobianTask {
fn_name: "eval_sparse_values",
shape: (4, 4),
entries: &entries,
variables: &["y0", "y1", "y2", "y3"],
params: None,
};
let chunks = task
.chunk_with_strategy(SparseChunkingStrategy::ByTargetChunkCount { target_chunks: 2 });
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].entry_offset, 0);
assert_eq!(chunks[0].entries[0].row, 0);
assert_eq!(chunks[0].entries[1].row, 2);
assert_eq!(chunks[1].entry_offset, 2);
assert_eq!(chunks[1].entries[0].row, 1);
assert_eq!(chunks[1].entries[1].row, 3);
}
#[test]
fn sparse_row_chunking_falls_back_to_contiguous_values_for_column_major_entries() {
let e0 = Expr::Var("y0".to_string());
let e1 = Expr::Var("y1".to_string());
let e2 = Expr::Var("y2".to_string());
let e3 = Expr::Var("y3".to_string());
let entries = vec![
SparseExprEntry {
row: 0,
col: 0,
expr: &e0,
},
SparseExprEntry {
row: 2,
col: 0,
expr: &e1,
},
SparseExprEntry {
row: 1,
col: 1,
expr: &e2,
},
SparseExprEntry {
row: 3,
col: 1,
expr: &e3,
},
];
let task = SparseJacobianTask {
fn_name: "eval_sparse_values",
shape: (4, 4),
entries: &entries,
variables: &["y0", "y1", "y2", "y3"],
params: None,
};
let chunks =
task.chunk_with_strategy(SparseChunkingStrategy::ByRowCount { rows_per_chunk: 2 });
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].entry_offset, 0);
assert_eq!(chunks[0].entries[0].row, 0);
assert_eq!(chunks[0].entries[1].row, 2);
assert_eq!(chunks[1].entry_offset, 2);
assert_eq!(chunks[1].entries[0].row, 1);
assert_eq!(chunks[1].entries[1].row, 3);
}
#[test]
fn banded_jacobian_plan_keeps_banded_kind_and_coordinates() {
let e0 = Expr::Var("y0".to_string());
let e1 = Expr::Var("y1".to_string());
let entries = vec![
BandedExprEntry {
row: 0,
col: 0,
diag_offset: 0,
diag_position: 0,
expr: &e0,
},
BandedExprEntry {
row: 0,
col: 1,
diag_offset: 1,
diag_position: 0,
expr: &e1,
},
];
let task = BandedJacobianTask {
fn_name: "eval_banded_values",
shape: (2, 2),
kl: 0,
ku: 1,
compact: false,
entries: &entries,
variables: &["y0", "y1"],
params: None,
};
let plan = task.plan();
assert_eq!(plan.kind, CodegenTaskKind::BandedJacobianValues);
assert_eq!(
plan.layout,
CodegenOutputLayout::BandedValues {
rows: 2,
cols: 2,
kl: 0,
ku: 1,
slots: 2,
}
);
assert_eq!(plan.outputs[0].coordinate, Some((0, 0)));
assert_eq!(plan.outputs[1].coordinate, Some((0, 1)));
}
#[test]
fn compact_banded_plan_uses_complete_lapack_slot_layout() {
let zero = Expr::Const(0.0);
let lower = Expr::Var("lower".to_string());
let diagonal = Expr::Var("diagonal".to_string());
let upper = Expr::Var("upper".to_string());
let entries = vec![
BandedExprEntry {
row: 0,
col: 0,
diag_offset: 1,
diag_position: 0,
expr: &zero,
},
BandedExprEntry {
row: 0,
col: 0,
diag_offset: 0,
diag_position: 0,
expr: &diagonal,
},
BandedExprEntry {
row: 1,
col: 0,
diag_offset: -1,
diag_position: 0,
expr: &lower,
},
BandedExprEntry {
row: 0,
col: 1,
diag_offset: 1,
diag_position: 1,
expr: &upper,
},
BandedExprEntry {
row: 1,
col: 1,
diag_offset: 0,
diag_position: 1,
expr: &diagonal,
},
BandedExprEntry {
row: 0,
col: 1,
diag_offset: -1,
diag_position: 1,
expr: &zero,
},
];
let task = BandedJacobianTask {
fn_name: "eval_banded_values",
shape: (2, 2),
kl: 1,
ku: 1,
compact: true,
entries: &entries,
variables: &["y0", "y1"],
params: None,
};
let plan = task.plan();
assert_eq!(
plan.layout,
CodegenOutputLayout::BandedCompactValues {
rows: 2,
cols: 2,
kl: 1,
ku: 1,
slots: 6,
}
);
assert_eq!(plan.outputs.len(), 6);
assert_eq!(
task.runtime_plan(BandedChunkingStrategy::Whole).compact,
true
);
}
#[test]
fn banded_chunk_plan_preserves_band_metadata_and_flat_slot_count() {
let e0 = Expr::Var("d0".to_string());
let e1 = Expr::Var("u0".to_string());
let e2 = Expr::Var("d1".to_string());
let entries = vec![
BandedExprEntry {
row: 0,
col: 0,
diag_offset: 0,
diag_position: 0,
expr: &e0,
},
BandedExprEntry {
row: 0,
col: 1,
diag_offset: 1,
diag_position: 0,
expr: &e1,
},
BandedExprEntry {
row: 1,
col: 1,
diag_offset: 0,
diag_position: 1,
expr: &e2,
},
];
let task = BandedJacobianTask {
fn_name: "eval_banded_values",
shape: (2, 2),
kl: 0,
ku: 1,
compact: false,
entries: &entries,
variables: &["y0", "y1"],
params: None,
};
let chunks = task.chunk_with_strategy(BandedChunkingStrategy::ByDiagonalCount {
diagonals_per_chunk: 1,
});
assert_eq!(chunks.len(), 2);
let plan = chunks[0].plan();
assert_eq!(
plan.layout,
CodegenOutputLayout::BandedValues {
rows: 2,
cols: 2,
kl: 0,
ku: 1,
slots: 2,
}
);
assert_eq!(plan.outputs.len(), 2);
assert_eq!(plan.outputs[0].coordinate, Some((0, 0)));
assert_eq!(plan.outputs[1].coordinate, Some((1, 1)));
}
#[test]
fn banded_chunking_strategy_by_diagonal_count_groups_diagonals() {
let e0 = Expr::Var("l".to_string());
let e1 = Expr::Var("d0".to_string());
let e2 = Expr::Var("u".to_string());
let e3 = Expr::Var("d1".to_string());
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,
},
BandedExprEntry {
row: 1,
col: 1,
diag_offset: 0,
diag_position: 1,
expr: &e3,
},
];
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 chunks = task.chunk_with_strategy(BandedChunkingStrategy::ByDiagonalCount {
diagonals_per_chunk: 1,
});
assert_eq!(chunks.len(), 3);
assert_eq!(chunks[0].value_offset, 0);
assert_eq!(chunks[0].diagonal_range, -1..=-1);
assert_eq!(chunks[1].value_offset, 1);
assert_eq!(chunks[1].diagonal_range, 0..=0);
assert_eq!(chunks[1].entries.len(), 2);
assert_eq!(chunks[2].value_offset, 2);
assert_eq!(chunks[2].diagonal_range, 1..=1);
}
}