use crate::symbolic::bvp::atom_aot::AtomAotPreparedPlan;
use crate::symbolic::codegen::codegen_provider_api::{
BackendKind, MatrixBackend, PreparedBandedProblem, PreparedDenseProblem, PreparedProblem,
PreparedSparseProblem,
};
use crate::symbolic::codegen::codegen_runtime_api::ResidualRuntimePlan;
use crate::symbolic::symbolic_engine::Expr;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ProblemIoManifest {
pub input_names: Vec<String>,
pub residual_len: usize,
pub jacobian_rows: usize,
pub jacobian_cols: usize,
pub jacobian_nnz: Option<usize>,
pub jacobian_layout: Option<PreparedJacobianLayout>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PreparedJacobianLayout {
Dense,
SparseExplicit,
BandedExplicit,
BandedCompact { kl: usize, ku: usize },
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GeneratedChunkManifest {
pub fn_name: String,
pub offset: usize,
pub len: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GeneratedFunctionsManifest {
pub residual_fn_name: String,
pub residual_chunk_names: Vec<String>,
pub residual_chunks: Vec<GeneratedChunkManifest>,
pub jacobian_fn_name: String,
pub jacobian_chunk_names: Vec<String>,
pub jacobian_chunks: Vec<GeneratedChunkManifest>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PreparedSymbolicRoute {
ExprLegacy,
AtomViewNative,
Generic,
}
impl PreparedSymbolicRoute {
pub const fn as_str(self) -> &'static str {
match self {
Self::ExprLegacy => "ExprLegacy",
Self::AtomViewNative => "AtomViewNative",
Self::Generic => "Generic",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PreparedProblemManifest {
pub backend_kind: BackendKind,
pub matrix_backend: MatrixBackend,
pub symbolic_route: PreparedSymbolicRoute,
pub io: ProblemIoManifest,
pub functions: GeneratedFunctionsManifest,
pub expression_signature: u64,
}
impl PreparedProblemManifest {
pub fn problem_key(&self) -> String {
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
pub fn residual_only(
backend_kind: BackendKind,
matrix_backend: MatrixBackend,
residual_plan: &ResidualRuntimePlan<'_>,
) -> Self {
Self {
backend_kind,
matrix_backend,
symbolic_route: PreparedSymbolicRoute::Generic,
io: ProblemIoManifest {
input_names: residual_plan
.input_names
.iter()
.map(|name| (*name).to_string())
.collect(),
residual_len: residual_plan.output_len,
jacobian_rows: 0,
jacobian_cols: 0,
jacobian_nnz: Some(0),
jacobian_layout: None,
},
functions: GeneratedFunctionsManifest {
residual_fn_name: residual_plan.fn_name.to_string(),
residual_chunk_names: residual_plan
.chunks
.iter()
.map(|chunk| chunk.fn_name.clone())
.collect(),
residual_chunks: residual_plan
.chunks
.iter()
.map(|chunk| GeneratedChunkManifest {
fn_name: chunk.fn_name.clone(),
offset: chunk.output_offset,
len: chunk.residuals.len(),
})
.collect(),
jacobian_fn_name: String::new(),
jacobian_chunk_names: Vec::new(),
jacobian_chunks: Vec::new(),
},
expression_signature: hash_expr_structures(
residual_plan
.chunks
.iter()
.flat_map(|chunk| chunk.residuals.iter()),
),
}
}
pub fn from_atom_aot_plan(
backend_kind: BackendKind,
matrix_backend: MatrixBackend,
plan: &AtomAotPreparedPlan,
functions: GeneratedFunctionsManifest,
) -> Self {
let (rows, cols) = plan.matrix_layout().shape();
Self {
backend_kind,
matrix_backend,
symbolic_route: PreparedSymbolicRoute::AtomViewNative,
io: ProblemIoManifest {
input_names: plan.input_names().to_vec(),
residual_len: plan.residuals().len(),
jacobian_rows: rows,
jacobian_cols: cols,
jacobian_nnz: Some(plan.matrix_layout().value_count()),
jacobian_layout: Some(match plan.matrix_layout() {
crate::symbolic::bvp::atom_aot::AtomAotMatrixLayout::Dense { .. } => {
PreparedJacobianLayout::Dense
}
crate::symbolic::bvp::atom_aot::AtomAotMatrixLayout::SparseCsc { .. } => {
PreparedJacobianLayout::SparseExplicit
}
crate::symbolic::bvp::atom_aot::AtomAotMatrixLayout::Banded { .. } => {
PreparedJacobianLayout::BandedExplicit
}
crate::symbolic::bvp::atom_aot::AtomAotMatrixLayout::BandedCompact {
kl,
ku,
..
} => PreparedJacobianLayout::BandedCompact { kl: *kl, ku: *ku },
}),
},
functions,
expression_signature: atom_expression_signature(plan),
}
}
}
fn atom_expression_signature(plan: &AtomAotPreparedPlan) -> u64 {
let mut hasher = DefaultHasher::new();
for atom in plan.residuals() {
atom.hash(&mut hasher);
}
for entry in plan.jacobian_entries() {
entry.row.hash(&mut hasher);
entry.col.hash(&mut hasher);
entry.value.hash(&mut hasher);
}
plan.input_names().hash(&mut hasher);
plan.matrix_layout().hash(&mut hasher);
hasher.finish()
}
fn hash_expr_structures<'a>(exprs: impl IntoIterator<Item = &'a Expr>) -> u64 {
let mut hasher = DefaultHasher::new();
for expr in exprs {
hash_expr_structure(expr, &mut hasher);
}
hasher.finish()
}
fn hash_expr_structure(expr: &Expr, hasher: &mut DefaultHasher) {
match expr {
Expr::Var(name) => {
1_u8.hash(hasher);
name.hash(hasher);
}
Expr::Const(value) => {
2_u8.hash(hasher);
value.to_bits().hash(hasher);
}
Expr::Add(lhs, rhs) => hash_binary_expr(3, lhs, rhs, hasher),
Expr::Sub(lhs, rhs) => hash_binary_expr(4, lhs, rhs, hasher),
Expr::Mul(lhs, rhs) => hash_binary_expr(5, lhs, rhs, hasher),
Expr::Div(lhs, rhs) => hash_binary_expr(6, lhs, rhs, hasher),
Expr::Pow(base, exp) => hash_binary_expr(7, base, exp, hasher),
Expr::Exp(inner) => hash_unary_expr(8, inner, hasher),
Expr::Ln(inner) => hash_unary_expr(9, inner, hasher),
Expr::sin(inner) => hash_unary_expr(10, inner, hasher),
Expr::cos(inner) => hash_unary_expr(11, inner, hasher),
Expr::tg(inner) => hash_unary_expr(12, inner, hasher),
Expr::ctg(inner) => hash_unary_expr(13, inner, hasher),
Expr::arcsin(inner) => hash_unary_expr(14, inner, hasher),
Expr::arccos(inner) => hash_unary_expr(15, inner, hasher),
Expr::arctg(inner) => hash_unary_expr(16, inner, hasher),
Expr::arcctg(inner) => hash_unary_expr(17, inner, hasher),
}
}
fn hash_binary_expr(tag: u8, lhs: &Expr, rhs: &Expr, hasher: &mut DefaultHasher) {
tag.hash(hasher);
hash_expr_structure(lhs, hasher);
hash_expr_structure(rhs, hasher);
}
fn hash_unary_expr(tag: u8, inner: &Expr, hasher: &mut DefaultHasher) {
tag.hash(hasher);
hash_expr_structure(inner, hasher);
}
fn dense_expression_signature(problem: &PreparedDenseProblem<'_>) -> u64 {
hash_expr_structures(
problem
.residual_plan
.chunks
.iter()
.flat_map(|chunk| chunk.residuals.iter())
.chain(
problem
.jacobian_plan
.chunks
.iter()
.flat_map(|chunk| chunk.jacobian_rows.iter().flat_map(|row| row.iter())),
),
)
}
fn sparse_expression_signature(problem: &PreparedSparseProblem<'_>) -> u64 {
hash_expr_structures(
problem
.residual_plan
.chunks
.iter()
.flat_map(|chunk| chunk.residuals.iter())
.chain(
problem
.jacobian_plan
.chunks
.iter()
.flat_map(|chunk| chunk.entries.iter().map(|entry| entry.expr)),
),
)
}
fn banded_expression_signature(problem: &PreparedBandedProblem<'_>) -> u64 {
hash_expr_structures(
problem
.residual_plan
.chunks
.iter()
.flat_map(|chunk| chunk.residuals.iter())
.chain(
problem
.jacobian_plan
.chunks
.iter()
.flat_map(|chunk| chunk.entries.iter().map(|entry| entry.expr)),
),
)
}
impl<'a> From<&PreparedDenseProblem<'a>> for PreparedProblemManifest {
fn from(problem: &PreparedDenseProblem<'a>) -> Self {
Self {
backend_kind: problem.backend_kind,
matrix_backend: problem.matrix_backend,
symbolic_route: PreparedSymbolicRoute::ExprLegacy,
io: ProblemIoManifest {
input_names: problem
.input_names()
.iter()
.map(|name| (*name).to_string())
.collect(),
residual_len: problem.residual_len(),
jacobian_rows: problem.jacobian_plan.rows,
jacobian_cols: problem.jacobian_plan.cols,
jacobian_nnz: None,
jacobian_layout: Some(PreparedJacobianLayout::Dense),
},
functions: GeneratedFunctionsManifest {
residual_fn_name: problem.residual_plan.fn_name.to_string(),
residual_chunk_names: problem
.residual_plan
.chunks
.iter()
.map(|chunk| chunk.fn_name.clone())
.collect(),
residual_chunks: problem
.residual_plan
.chunks
.iter()
.map(|chunk| GeneratedChunkManifest {
fn_name: chunk.fn_name.clone(),
offset: chunk.output_offset,
len: chunk.residuals.len(),
})
.collect(),
jacobian_fn_name: problem.jacobian_plan.fn_name.to_string(),
jacobian_chunk_names: problem
.jacobian_plan
.chunks
.iter()
.map(|chunk| chunk.fn_name.clone())
.collect(),
jacobian_chunks: problem
.jacobian_plan
.chunks
.iter()
.map(|chunk| GeneratedChunkManifest {
fn_name: chunk.fn_name.clone(),
offset: chunk.value_offset,
len: chunk.value_range().len(),
})
.collect(),
},
expression_signature: dense_expression_signature(problem),
}
}
}
impl<'a> From<&PreparedSparseProblem<'a>> for PreparedProblemManifest {
fn from(problem: &PreparedSparseProblem<'a>) -> Self {
Self {
backend_kind: problem.backend_kind,
matrix_backend: problem.matrix_backend,
symbolic_route: PreparedSymbolicRoute::ExprLegacy,
io: ProblemIoManifest {
input_names: problem
.input_names()
.iter()
.map(|name| (*name).to_string())
.collect(),
residual_len: problem.residual_len(),
jacobian_rows: problem.jacobian_plan.structure.rows,
jacobian_cols: problem.jacobian_plan.structure.cols,
jacobian_nnz: Some(problem.jacobian_plan.structure.nnz()),
jacobian_layout: Some(PreparedJacobianLayout::SparseExplicit),
},
functions: GeneratedFunctionsManifest {
residual_fn_name: problem.residual_plan.fn_name.to_string(),
residual_chunk_names: problem
.residual_plan
.chunks
.iter()
.map(|chunk| chunk.fn_name.clone())
.collect(),
residual_chunks: problem
.residual_plan
.chunks
.iter()
.map(|chunk| GeneratedChunkManifest {
fn_name: chunk.fn_name.clone(),
offset: chunk.output_offset,
len: chunk.residuals.len(),
})
.collect(),
jacobian_fn_name: problem.jacobian_plan.fn_name.to_string(),
jacobian_chunk_names: problem
.jacobian_plan
.chunks
.iter()
.map(|chunk| chunk.fn_name.clone())
.collect(),
jacobian_chunks: problem
.jacobian_plan
.chunks
.iter()
.map(|chunk| GeneratedChunkManifest {
fn_name: chunk.fn_name.clone(),
offset: chunk.value_offset,
len: chunk.entries.len(),
})
.collect(),
},
expression_signature: sparse_expression_signature(problem),
}
}
}
impl<'a> From<&PreparedBandedProblem<'a>> for PreparedProblemManifest {
fn from(problem: &PreparedBandedProblem<'a>) -> Self {
Self {
backend_kind: problem.backend_kind,
matrix_backend: problem.matrix_backend,
symbolic_route: PreparedSymbolicRoute::ExprLegacy,
io: ProblemIoManifest {
input_names: problem
.input_names()
.iter()
.map(|name| (*name).to_string())
.collect(),
residual_len: problem.residual_len(),
jacobian_rows: problem.jacobian_plan.structure.rows,
jacobian_cols: problem.jacobian_plan.structure.cols,
jacobian_nnz: Some(problem.jacobian_plan.structure.nnz()),
jacobian_layout: Some(if problem.jacobian_plan.compact {
PreparedJacobianLayout::BandedCompact {
kl: problem.jacobian_plan.structure.kl,
ku: problem.jacobian_plan.structure.ku,
}
} else {
PreparedJacobianLayout::BandedExplicit
}),
},
functions: GeneratedFunctionsManifest {
residual_fn_name: problem.residual_plan.fn_name.to_string(),
residual_chunk_names: problem
.residual_plan
.chunks
.iter()
.map(|chunk| chunk.fn_name.clone())
.collect(),
residual_chunks: problem
.residual_plan
.chunks
.iter()
.map(|chunk| GeneratedChunkManifest {
fn_name: chunk.fn_name.clone(),
offset: chunk.output_offset,
len: chunk.residuals.len(),
})
.collect(),
jacobian_fn_name: problem.jacobian_plan.fn_name.to_string(),
jacobian_chunk_names: problem
.jacobian_plan
.chunks
.iter()
.map(|chunk| chunk.fn_name.clone())
.collect(),
jacobian_chunks: problem
.jacobian_plan
.chunks
.iter()
.map(|chunk| GeneratedChunkManifest {
fn_name: chunk.fn_name.clone(),
offset: chunk.value_offset,
len: chunk.entries.len(),
})
.collect(),
},
expression_signature: banded_expression_signature(problem),
}
}
}
impl<'a> From<&PreparedProblem<'a>> for PreparedProblemManifest {
fn from(problem: &PreparedProblem<'a>) -> Self {
match problem {
PreparedProblem::Dense(problem) => Self::from(problem),
PreparedProblem::Banded(problem) => Self::from(problem),
PreparedProblem::Sparse(problem) => Self::from(problem),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::symbolic::codegen::codegen_provider_api::{
PreparedDenseProblem, PreparedSparseProblem,
};
use crate::symbolic::codegen::codegen_runtime_api::{
DenseJacobianChunkingStrategy, ResidualChunkingStrategy,
};
use crate::symbolic::codegen::codegen_tasks::{
JacobianTask, ResidualTask, SparseChunkingStrategy, SparseExprEntry, SparseJacobianTask,
};
use crate::symbolic::symbolic_engine::Expr;
#[test]
fn dense_manifest_captures_owned_function_and_io_metadata() {
let residuals = vec![Expr::parse_expression("x + y")];
let jacobian = vec![vec![
Expr::parse_expression("1"),
Expr::parse_expression("1"),
]];
let vars = vec!["x", "y"];
let prepared = PreparedDenseProblem::new(
BackendKind::Aot,
MatrixBackend::Dense,
ResidualTask {
fn_name: "eval_residual",
residuals: &residuals,
variables: &vars,
params: None,
}
.runtime_plan(ResidualChunkingStrategy::Whole),
JacobianTask {
fn_name: "eval_jacobian",
jacobian: &jacobian,
variables: &vars,
params: None,
}
.runtime_plan(DenseJacobianChunkingStrategy::Whole),
);
let manifest = PreparedProblemManifest::from(&prepared);
assert_eq!(manifest.backend_kind, BackendKind::Aot);
assert_eq!(manifest.matrix_backend, MatrixBackend::Dense);
assert_eq!(manifest.symbolic_route, PreparedSymbolicRoute::ExprLegacy);
assert_eq!(
manifest.io.input_names,
vec!["x".to_string(), "y".to_string()]
);
assert_eq!(manifest.io.residual_len, 1);
assert_eq!(manifest.io.jacobian_rows, 1);
assert_eq!(manifest.io.jacobian_cols, 2);
assert_eq!(manifest.io.jacobian_nnz, None);
assert_eq!(manifest.functions.residual_fn_name, "eval_residual");
assert_eq!(manifest.functions.jacobian_fn_name, "eval_jacobian");
}
#[test]
fn dense_problem_key_changes_when_expressions_change_but_shape_stays_the_same() {
let residuals0 = vec![Expr::parse_expression("x + y")];
let residuals1 = vec![Expr::parse_expression("x - y")];
let jacobian0 = vec![vec![
Expr::parse_expression("1"),
Expr::parse_expression("1"),
]];
let jacobian1 = vec![vec![
Expr::parse_expression("1"),
Expr::parse_expression("-1"),
]];
let vars = vec!["x", "y"];
let prepared0 = PreparedDenseProblem::new(
BackendKind::Aot,
MatrixBackend::Dense,
ResidualTask {
fn_name: "eval_residual",
residuals: &residuals0,
variables: &vars,
params: None,
}
.runtime_plan(ResidualChunkingStrategy::Whole),
JacobianTask {
fn_name: "eval_jacobian",
jacobian: &jacobian0,
variables: &vars,
params: None,
}
.runtime_plan(DenseJacobianChunkingStrategy::Whole),
);
let prepared1 = PreparedDenseProblem::new(
BackendKind::Aot,
MatrixBackend::Dense,
ResidualTask {
fn_name: "eval_residual",
residuals: &residuals1,
variables: &vars,
params: None,
}
.runtime_plan(ResidualChunkingStrategy::Whole),
JacobianTask {
fn_name: "eval_jacobian",
jacobian: &jacobian1,
variables: &vars,
params: None,
}
.runtime_plan(DenseJacobianChunkingStrategy::Whole),
);
let manifest0 = PreparedProblemManifest::from(&prepared0);
let manifest1 = PreparedProblemManifest::from(&prepared1);
assert_ne!(manifest0.problem_key(), manifest1.problem_key());
}
#[test]
fn dense_problem_key_is_reproducible_and_tracks_backend_identity() {
let residuals = vec![Expr::parse_expression("x + y")];
let jacobian = vec![vec![
Expr::parse_expression("1"),
Expr::parse_expression("1"),
]];
let vars = vec!["x", "y"];
let make_prepared = |matrix_backend| {
PreparedDenseProblem::new(
BackendKind::Aot,
matrix_backend,
ResidualTask {
fn_name: "eval_residual",
residuals: &residuals,
variables: &vars,
params: None,
}
.runtime_plan(ResidualChunkingStrategy::Whole),
JacobianTask {
fn_name: "eval_jacobian",
jacobian: &jacobian,
variables: &vars,
params: None,
}
.runtime_plan(DenseJacobianChunkingStrategy::Whole),
)
};
let dense0 = PreparedProblemManifest::from(&make_prepared(MatrixBackend::Dense));
let dense1 = PreparedProblemManifest::from(&make_prepared(MatrixBackend::Dense));
let values_only = PreparedProblemManifest::from(&make_prepared(MatrixBackend::ValuesOnly));
assert_eq!(
dense0.problem_key(),
dense1.problem_key(),
"identical manifests must produce stable reusable artifact keys"
);
assert_ne!(
dense0.problem_key(),
values_only.problem_key(),
"artifact keys must include backend identity to avoid stale cross-route reuse"
);
}
#[test]
fn artifact_key_tracks_symbolic_frontend_route() {
let residuals = vec![Expr::parse_expression("x + 1")];
let jacobian = vec![vec![Expr::parse_expression("1")]];
let vars = vec!["x"];
let prepared = PreparedDenseProblem::new(
BackendKind::Aot,
MatrixBackend::Dense,
ResidualTask {
fn_name: "eval_residual",
residuals: &residuals,
variables: &vars,
params: None,
}
.runtime_plan(ResidualChunkingStrategy::Whole),
JacobianTask {
fn_name: "eval_jacobian",
jacobian: &jacobian,
variables: &vars,
params: None,
}
.runtime_plan(DenseJacobianChunkingStrategy::Whole),
);
let legacy = PreparedProblemManifest::from(&prepared);
let mut atom = legacy.clone();
atom.symbolic_route = PreparedSymbolicRoute::AtomViewNative;
assert_eq!(legacy.symbolic_route.as_str(), "ExprLegacy");
assert_eq!(atom.symbolic_route.as_str(), "AtomViewNative");
assert_ne!(
legacy.problem_key(),
atom.problem_key(),
"frontend route must participate in artifact identity"
);
}
#[test]
fn sparse_manifest_keeps_nnz_and_chunk_names() {
let residuals = vec![
Expr::parse_expression("p + x"),
Expr::parse_expression("y - p"),
];
let entry0 = Expr::parse_expression("1");
let entry1 = Expr::parse_expression("2");
let vars = vec!["x", "y"];
let params = vec!["p"];
let entries = vec![
SparseExprEntry {
row: 0,
col: 0,
expr: &entry0,
},
SparseExprEntry {
row: 1,
col: 1,
expr: &entry1,
},
];
let prepared = PreparedSparseProblem::new(
BackendKind::Aot,
MatrixBackend::SparseCol,
ResidualTask {
fn_name: "eval_residual",
residuals: &residuals,
variables: &vars,
params: Some(¶ms),
}
.runtime_plan(ResidualChunkingStrategy::ByOutputCount {
max_outputs_per_chunk: 1,
}),
SparseJacobianTask {
fn_name: "eval_sparse_values",
shape: (2, 2),
entries: &entries,
variables: &vars,
params: Some(¶ms),
}
.runtime_plan(SparseChunkingStrategy::ByRowCount { rows_per_chunk: 1 }),
);
let manifest = PreparedProblemManifest::from(&PreparedProblem::sparse(prepared));
assert_eq!(manifest.backend_kind, BackendKind::Aot);
assert_eq!(manifest.matrix_backend, MatrixBackend::SparseCol);
assert_eq!(manifest.io.input_names, vec!["p", "x", "y"]);
assert_eq!(manifest.io.jacobian_nnz, Some(2));
assert_eq!(
manifest.functions.residual_chunk_names,
vec![
"eval_residual_chunk_0".to_string(),
"eval_residual_chunk_1".to_string()
]
);
assert_eq!(
manifest.functions.jacobian_chunk_names,
vec![
"eval_sparse_values_chunk_0".to_string(),
"eval_sparse_values_chunk_1".to_string()
]
);
}
#[test]
fn manifest_problem_key_changes_with_function_layout() {
let residuals = vec![Expr::parse_expression("x")];
let jacobian = vec![vec![Expr::parse_expression("1")]];
let vars = vec!["x"];
let prepared0 = PreparedDenseProblem::new(
BackendKind::Aot,
MatrixBackend::Dense,
ResidualTask {
fn_name: "eval_residual_a",
residuals: &residuals,
variables: &vars,
params: None,
}
.runtime_plan(ResidualChunkingStrategy::Whole),
JacobianTask {
fn_name: "eval_jacobian",
jacobian: &jacobian,
variables: &vars,
params: None,
}
.runtime_plan(DenseJacobianChunkingStrategy::Whole),
);
let prepared1 = PreparedDenseProblem::new(
BackendKind::Aot,
MatrixBackend::Dense,
ResidualTask {
fn_name: "eval_residual_b",
residuals: &residuals,
variables: &vars,
params: None,
}
.runtime_plan(ResidualChunkingStrategy::Whole),
JacobianTask {
fn_name: "eval_jacobian",
jacobian: &jacobian,
variables: &vars,
params: None,
}
.runtime_plan(DenseJacobianChunkingStrategy::Whole),
);
let manifest0 = PreparedProblemManifest::from(&prepared0);
let manifest1 = PreparedProblemManifest::from(&prepared1);
assert_ne!(manifest0.problem_key(), manifest1.problem_key());
}
}