use crate::common::IntegrateFloat;
use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
use scirs2_core::parallel_ops::*;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct SparsePattern {
pub entries: Vec<(usize, usize)>,
pub nrows: usize,
pub n_cols: usize,
pub row_indices: Vec<Vec<usize>>,
pub col_indices: Vec<Vec<usize>>,
}
impl SparsePattern {
pub fn new(_nrows: usize, ncols: usize) -> Self {
SparsePattern {
entries: Vec::new(),
nrows: _nrows,
n_cols: ncols,
row_indices: vec![Vec::new(); _nrows],
col_indices: vec![Vec::new(); ncols],
}
}
pub fn add_entry(&mut self, row: usize, col: usize) {
if row < self.nrows && col < self.n_cols {
self.entries.push((row, col));
self.row_indices[row].push(col);
self.col_indices[col].push(row);
}
}
pub fn nnz(&self) -> usize {
self.entries.len()
}
pub fn sparsity(&self) -> f64 {
let total = (self.nrows * self.n_cols) as f64;
if total > 0.0 {
1.0 - (self.nnz() as f64 / total)
} else {
0.0
}
}
pub fn is_sparse(&self, threshold: f64) -> bool {
self.sparsity() > threshold
}
pub fn compute_coloring(&self) -> ColGrouping {
let mut degrees: Vec<(usize, usize)> = Vec::new();
for col in 0..self.n_cols {
let mut neighbors = HashSet::new();
for &row in &self.col_indices[col] {
for &other_col in &self.row_indices[row] {
if other_col != col {
neighbors.insert(other_col);
}
}
}
degrees.push((col, neighbors.len()));
}
degrees.sort_by_key(|&(_, deg)| std::cmp::Reverse(deg));
let mut colors: HashMap<usize, usize> = HashMap::new();
let mut max_color = 0;
for (col_, _) in degrees {
let mut used_colors = HashSet::new();
for &row in &self.col_indices[col_] {
for &other_col in &self.row_indices[row] {
if other_col != col_ {
if let Some(&color) = colors.get(&other_col) {
used_colors.insert(color);
}
}
}
}
let mut color = 0;
while used_colors.contains(&color) {
color += 1;
}
colors.insert(col_, color);
max_color = max_color.max(color);
}
let mut groups = vec![Vec::new(); max_color + 1];
for (&col, &color) in &colors {
groups[color].push(col);
}
ColGrouping { groups }
}
}
pub struct ColGrouping {
pub groups: Vec<Vec<usize>>,
}
impl ColGrouping {
pub fn n_groups(&self) -> usize {
self.groups.len()
}
}
pub struct SparseJacobian<F: IntegrateFloat> {
pub pattern: SparsePattern,
pub values: Vec<F>,
pub index_map: HashMap<(usize, usize), usize>,
}
impl<F: IntegrateFloat> SparseJacobian<F> {
pub fn new(pattern: SparsePattern) -> Self {
let nnz = pattern.nnz();
let mut index_map = HashMap::new();
for (i, &(row, col)) in pattern.entries.iter().enumerate() {
index_map.insert((row, col), i);
}
SparseJacobian {
pattern,
values: vec![F::zero(); nnz],
index_map,
}
}
pub fn set(&mut self, row: usize, col: usize, value: F) -> IntegrateResult<()> {
if let Some(&idx) = self.index_map.get(&(row, col)) {
self.values[idx] = value;
Ok(())
} else {
Err(IntegrateError::IndexError(format!(
"Entry ({row}, {col}) not in sparsity pattern"
)))
}
}
pub fn get(&self, row: usize, col: usize) -> Option<F> {
self.index_map.get(&(row, col)).map(|&idx| self.values[idx])
}
pub fn to_dense(&self) -> Array2<F> {
let mut dense = Array2::zeros((self.pattern.nrows, self.pattern.n_cols));
for (&(row, col), &idx) in &self.index_map {
dense[[row, col]] = self.values[idx];
}
dense
}
pub fn matvec(&self, x: ArrayView1<F>) -> IntegrateResult<Array1<F>> {
if x.len() != self.pattern.n_cols {
return Err(IntegrateError::DimensionMismatch(format!(
"Expected {} columns, got {}",
self.pattern.n_cols,
x.len()
)));
}
let mut y = Array1::zeros(self.pattern.nrows);
for (&(row, col), &idx) in &self.index_map {
y[row] += self.values[idx] * x[col];
}
Ok(y)
}
pub fn matvec_transpose(&self, x: ArrayView1<F>) -> IntegrateResult<Array1<F>> {
if x.len() != self.pattern.nrows {
return Err(IntegrateError::DimensionMismatch(format!(
"Expected {} rows, got {}",
self.pattern.nrows,
x.len()
)));
}
let mut y = Array1::zeros(self.pattern.n_cols);
for (&(row, col), &idx) in &self.index_map {
y[col] += self.values[idx] * x[row];
}
Ok(y)
}
}
pub struct CSRJacobian<F: IntegrateFloat> {
pub nrows: usize,
pub n_cols: usize,
pub row_ptr: Vec<usize>,
pub col_idx: Vec<usize>,
pub values: Vec<F>,
}
pub struct CSCJacobian<F: IntegrateFloat> {
pub nrows: usize,
pub n_cols: usize,
pub col_ptr: Vec<usize>,
pub row_idx: Vec<usize>,
pub values: Vec<F>,
}
impl<F: IntegrateFloat> SparseJacobian<F> {
pub fn from_pattern(pattern: SparsePattern) -> Self {
let mut index_map = HashMap::new();
for (idx, &(row, col)) in pattern.entries.iter().enumerate() {
index_map.insert((row, col), idx);
}
SparseJacobian {
values: vec![F::zero(); pattern.entries.len()],
pattern,
index_map,
}
}
pub fn set_unchecked(&mut self, row: usize, col: usize, value: F) {
if let Some(&idx) = self.index_map.get(&(row, col)) {
self.values[idx] = value;
}
}
pub fn get_or_zero(&self, row: usize, col: usize) -> F {
if let Some(&idx) = self.index_map.get(&(row, col)) {
self.values[idx]
} else {
F::zero()
}
}
pub fn to_dense_alt(&self) -> Array2<F> {
let mut dense = Array2::zeros((self.pattern.nrows, self.pattern.n_cols));
for (idx, &(row, col)) in self.pattern.entries.iter().enumerate() {
dense[[row, col]] = self.values[idx];
}
dense
}
pub fn apply(&self, x: ArrayView1<F>) -> IntegrateResult<Array1<F>> {
if x.len() != self.pattern.n_cols {
return Err(IntegrateError::DimensionMismatch(format!(
"Expected {} columns, got {}",
self.pattern.n_cols,
x.len()
)));
}
let mut result = Array1::zeros(self.pattern.nrows);
for (idx, &(row, col)) in self.pattern.entries.iter().enumerate() {
result[row] += self.values[idx] * x[col];
}
Ok(result)
}
pub fn to_csr(&self) -> CSRJacobian<F> {
let mut entries: Vec<(usize, usize, F)> = Vec::new();
for (idx, &(row, col)) in self.pattern.entries.iter().enumerate() {
entries.push((row, col, self.values[idx]));
}
entries.sort_by_key(|&(r, c, _)| (r, c));
let mut row_ptr = vec![0];
let mut col_idx = Vec::new();
let mut values = Vec::new();
let mut currentrow = 0;
for (row, col, val) in entries {
while currentrow < row {
row_ptr.push(col_idx.len());
currentrow += 1;
}
col_idx.push(col);
values.push(val);
}
while row_ptr.len() <= self.pattern.nrows {
row_ptr.push(col_idx.len());
}
CSRJacobian {
nrows: self.pattern.nrows,
n_cols: self.pattern.n_cols,
row_ptr,
col_idx,
values,
}
}
}
#[allow(dead_code)]
pub fn detect_sparsity<F, Func>(f: Func, x: ArrayView1<F>, eps: F) -> IntegrateResult<SparsePattern>
where
F: IntegrateFloat + Send + Sync,
Func: Fn(ArrayView1<F>) -> IntegrateResult<Array1<F>> + Sync,
{
let n = x.len();
let f0 = f(x)?;
let m = f0.len();
let mut pattern = SparsePattern::new(m, n);
let results: Vec<_> = (0..n)
.collect::<Vec<_>>()
.par_chunks(n / scirs2_core::parallel_ops::num_threads().max(1) + 1)
.map(|chunk| {
let mut local_entries = Vec::new();
for &j in chunk {
let mut x_pert = x.to_owned();
x_pert[j] += eps;
if let Ok(f_pert) = f(x_pert.view()) {
for i in 0..m {
if (f_pert[i] - f0[i]).abs() > F::epsilon() {
local_entries.push((i, j));
}
}
}
}
local_entries
})
.collect();
for entries in results {
for (i, j) in entries {
pattern.add_entry(i, j);
}
}
Ok(pattern)
}
impl<F: IntegrateFloat + Send + Sync> CSRJacobian<F> {
pub fn apply(&self, x: ArrayView1<F>) -> IntegrateResult<Array1<F>> {
if x.len() != self.n_cols {
return Err(IntegrateError::DimensionMismatch(format!(
"Expected {} columns, got {}",
self.n_cols,
x.len()
)));
}
let mut result = Array1::zeros(self.nrows);
let chunk_size = (self.nrows / scirs2_core::parallel_ops::num_threads()).max(1);
let chunks: Vec<_> = (0..self.nrows)
.collect::<Vec<_>>()
.par_chunks(chunk_size)
.map(|rows| {
let mut local_result = Array1::zeros(rows.len());
for (local_idx, &row) in rows.iter().enumerate() {
let start = self.row_ptr[row];
let end = self.row_ptr[row + 1];
for idx in start..end {
local_result[local_idx] += self.values[idx] * x[self.col_idx[idx]];
}
}
(rows[0], local_result)
})
.collect();
for (startrow, chunk) in chunks {
for (i, val) in chunk.iter().enumerate() {
result[startrow + i] = *val;
}
}
Ok(result)
}
pub fn transpose(&self) -> CSCJacobian<F> {
let mut entries: Vec<(usize, usize, F)> = Vec::new();
for row in 0..self.nrows {
let start = self.row_ptr[row];
let end = self.row_ptr[row + 1];
for idx in start..end {
entries.push((self.col_idx[idx], row, self.values[idx]));
}
}
entries.sort_by_key(|&(c, r, _)| (c, r));
let mut col_ptr = vec![0];
let mut row_idx = Vec::new();
let mut values = Vec::new();
let mut current_col = 0;
for (col, row, val) in entries {
while current_col < col {
col_ptr.push(row_idx.len());
current_col += 1;
}
row_idx.push(row);
values.push(val);
}
while col_ptr.len() <= self.n_cols {
col_ptr.push(row_idx.len());
}
CSCJacobian {
nrows: self.n_cols,
n_cols: self.nrows,
col_ptr,
row_idx,
values,
}
}
}
#[allow(dead_code)]
pub fn compress_jacobian<F: IntegrateFloat>(
dense: ArrayView2<F>,
pattern: &SparsePattern,
) -> SparseJacobian<F> {
let mut sparse = SparseJacobian::from_pattern(pattern.clone());
for (idx, &(row, col)) in pattern.entries.iter().enumerate() {
sparse.values[idx] = dense[[row, col]];
}
sparse
}
#[allow(dead_code)]
pub fn colored_jacobian<F, Func>(
f: Func,
x: ArrayView1<F>,
pattern: &SparsePattern,
eps: F,
) -> IntegrateResult<SparseJacobian<F>>
where
F: IntegrateFloat,
Func: Fn(ArrayView1<F>) -> IntegrateResult<Array1<F>>,
{
let coloring = pattern.compute_coloring();
let f0 = f(x)?;
let mut jacobian = SparseJacobian::from_pattern(pattern.clone());
for group in &coloring.groups {
let mut x_pert = x.to_owned();
for &col in group {
x_pert[col] += eps;
}
let f_pert = f(x_pert.view())?;
for &col in group {
for &row in &pattern.col_indices[col] {
let deriv = (f_pert[row] - f0[row]) / eps;
let _ = jacobian.set(row, col, deriv);
}
}
}
Ok(jacobian)
}
#[allow(dead_code)]
pub fn example_tridiagonal_pattern(n: usize) -> SparsePattern {
let mut pattern = SparsePattern::new(n, n);
for i in 0..n {
pattern.add_entry(i, i); if i > 0 {
pattern.add_entry(i, i - 1); }
if i < n - 1 {
pattern.add_entry(i, i + 1); }
}
pattern
}
pub struct SparseJacobianUpdater<F: IntegrateFloat> {
pattern: SparsePattern,
threshold: F,
}
impl<F: IntegrateFloat> SparseJacobianUpdater<F> {
pub fn new(pattern: SparsePattern, threshold: F) -> Self {
SparseJacobianUpdater { pattern, threshold }
}
pub fn broyden_update(
&self,
jac: &mut SparseJacobian<F>,
dx: ArrayView1<F>,
df: ArrayView1<F>,
) -> IntegrateResult<()> {
let jdx = jac.apply(dx)?;
let dy = &df - &jdx;
let dx_norm_sq = dx.dot(&dx);
if dx_norm_sq < self.threshold {
return Ok(());
}
for (idx, &(i, j)) in self.pattern.entries.iter().enumerate() {
jac.values[idx] += dy[i] * dx[j] / dx_norm_sq;
}
Ok(())
}
}
#[allow(dead_code)]
pub fn detect_sparsity_adaptive<F, Func>(
f: Func,
x: ArrayView1<F>,
eps_range: (F, F),
n_samples: usize,
) -> IntegrateResult<SparsePattern>
where
F: IntegrateFloat + Send + Sync,
Func: Fn(ArrayView1<F>) -> IntegrateResult<Array1<F>> + Sync,
{
let n = x.len();
let f0 = f(x)?;
let m = f0.len();
let mut accumulated_pattern = SparsePattern::new(m, n);
let eps_min = eps_range.0;
let eps_max = eps_range.1;
for sample in 0..n_samples {
let alpha = F::from(sample).expect("Failed to convert to float")
/ F::from(n_samples - 1).expect("Failed to convert to float");
let eps = eps_min * (F::one() - alpha) + eps_max * alpha;
let pattern = detect_sparsity(&f, x, eps)?;
for &(i, j) in &pattern.entries {
accumulated_pattern.add_entry(i, j);
}
}
Ok(accumulated_pattern)
}
pub struct BlockPattern {
pub block_sizes: Vec<(usize, usize)>,
pub blocks: Vec<(usize, usize)>,
pub nrows: usize,
pub n_cols: usize,
}
impl BlockPattern {
pub fn to_sparse_pattern(&self) -> SparsePattern {
let mut pattern = SparsePattern::new(self.nrows, self.n_cols);
let mut row_offset = 0;
let mut col_offset = 0;
for &(blockrow, block_col) in &self.blocks {
let (block_height, block_width) = self.block_sizes[blockrow];
for i in 0..block_height {
for j in 0..block_width {
pattern.add_entry(row_offset + i, col_offset + j);
}
}
col_offset += block_width;
if block_col == self.blocks.len() - 1 {
col_offset = 0;
row_offset += block_height;
}
}
pattern
}
}
pub struct HybridJacobian<F: IntegrateFloat> {
pub sparse_blocks: Vec<CSRJacobian<F>>,
pub dense_blocks: Vec<Array2<F>>,
pub block_info: BlockPattern,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{SparseJacobian, SparsePattern};
use scirs2_core::ndarray::Array1;
#[test]
fn test_sparse_pattern() {
let mut pattern = SparsePattern::new(3, 3);
pattern.add_entry(0, 0);
pattern.add_entry(1, 1);
pattern.add_entry(2, 2);
pattern.add_entry(0, 1);
assert_eq!(pattern.nnz(), 4);
assert!(pattern.sparsity() > 0.5);
}
#[test]
fn test_coloring() {
let pattern = example_tridiagonal_pattern(5);
let coloring = pattern.compute_coloring();
assert!(coloring.n_groups() <= 3);
}
#[test]
fn test_sparse_jacobian() {
let pattern = example_tridiagonal_pattern(3);
let mut jac = SparseJacobian::from_pattern(pattern);
let _ = jac.set(0, 0, 2.0);
let _ = jac.set(0, 1, -1.0);
let _ = jac.set(1, 0, -1.0);
let _ = jac.set(1, 1, 2.0);
let _ = jac.set(1, 2, -1.0);
let _ = jac.set(2, 1, -1.0);
let _ = jac.set(2, 2, 2.0);
let x = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let y = jac.apply(x.view()).expect("Operation failed");
assert!((y[0] - 0.0_f64).abs() < 1e-10);
assert!((y[1] - 0.0_f64).abs() < 1e-10);
assert!((y[2] - 4.0_f64).abs() < 1e-10);
}
#[test]
fn test_csr_format() {
let pattern = example_tridiagonal_pattern(3);
let mut jac = SparseJacobian::from_pattern(pattern);
let _ = jac.set(0, 0, 2.0);
let _ = jac.set(0, 1, -1.0);
let _ = jac.set(1, 0, -1.0);
let _ = jac.set(1, 1, 2.0);
let _ = jac.set(1, 2, -1.0);
let _ = jac.set(2, 1, -1.0);
let _ = jac.set(2, 2, 2.0);
let csr = jac.to_csr();
let x = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let y = csr.apply(x.view()).expect("Operation failed");
assert!((y[0] - 0.0_f64).abs() < 1e-10);
assert!((y[1] - 0.0_f64).abs() < 1e-10);
assert!((y[2] - 4.0_f64).abs() < 1e-10);
}
}