use crate::common::IntegrateFloat;
use crate::error::IntegrateResult;
use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
use scirs2_core::num_threads;
#[cfg(feature = "parallel_jacobian")]
use scirs2_core::parallel_ops::*;
#[allow(dead_code)]
pub fn parallel_finite_difference_jacobian<F, Func>(
f: &Func,
t: F,
y: &Array1<F>,
f_current: &Array1<F>,
perturbation_scale: F,
) -> IntegrateResult<Array2<F>>
where
F: IntegrateFloat + Send + Sync,
Func: Fn(F, ArrayView1<F>) -> Array1<F> + Sync,
{
let n_dim = y.len();
let mut jacobian = Array2::<F>::zeros((n_dim, n_dim));
let eps_base = F::from_f64(1e-8).expect("Operation failed") * perturbation_scale;
#[cfg(feature = "parallel_jacobian")]
{
let columns: Vec<(usize, Array1<F>)> = (0..n_dim)
.into_par_iter()
.map(|j| {
let eps = eps_base * (F::one() + y[j].abs()).max(F::one());
let mut y_perturbed = y.clone();
y_perturbed[j] += eps;
let f_perturbed = f(t, y_perturbed.view());
let mut column = Array1::<F>::zeros(n_dim);
for i in 0..n_dim {
column[i] = (f_perturbed[i] - f_current[i]) / eps;
}
(j, column)
})
.collect();
for (j, column) in columns {
for i in 0..n_dim {
jacobian[[i, j]] = column[i];
}
}
}
#[cfg(not(feature = "parallel_jacobian"))]
{
for j in 0..n_dim {
let eps = eps_base * (F::one() + y[j].abs()).max(F::one());
let mut y_perturbed = y.clone();
y_perturbed[j] += eps;
let f_perturbed = f(t, y_perturbed.view());
for i in 0..n_dim {
jacobian[[i, j]] = (f_perturbed[i] - f_current[i]) / eps;
}
}
}
Ok(jacobian)
}
#[allow(dead_code)]
pub fn parallel_sparse_jacobian<F, Func>(
f: &Func,
t: F,
y: &Array1<F>,
f_current: &Array1<F>,
sparsity_pattern: Option<&Array2<bool>>,
perturbation_scale: F,
) -> IntegrateResult<Array2<F>>
where
F: IntegrateFloat + Send + Sync,
Func: Fn(F, ArrayView1<F>) -> Array1<F> + Sync,
{
let n_dim = y.len();
let mut jacobian = Array2::<F>::zeros((n_dim, n_dim));
let (pattern, colors) = if let Some(_pattern) = sparsity_pattern {
(_pattern.clone(), greedy_coloring(_pattern))
} else {
let dense_pattern = Array2::<bool>::from_elem((n_dim, n_dim), true);
let dense_colors = (0..n_dim).collect::<Vec<_>>();
(dense_pattern, dense_colors)
};
let max_color = colors.iter().max().cloned().unwrap_or(0);
let eps_base = F::from_f64(1e-8).expect("Operation failed") * perturbation_scale;
#[cfg(feature = "parallel_jacobian")]
{
let color_results: Vec<_> = (0..=max_color)
.into_par_iter()
.map(|color| {
let columns_with_color: Vec<usize> = colors
.iter()
.enumerate()
.filter_map(|(j, &c)| if c == color { Some(j) } else { None })
.collect();
if columns_with_color.is_empty() {
return Vec::new();
}
let mut y_perturbed = y.clone();
let mut perturbation_sizes = Vec::with_capacity(columns_with_color.len());
for &j in &columns_with_color {
let eps = eps_base * (F::one() + y[j].abs()).max(F::one());
y_perturbed[j] += eps;
perturbation_sizes.push(eps);
}
let f_perturbed = f(t, y_perturbed.view());
let mut color_columns = Vec::with_capacity(columns_with_color.len());
for (idx, &j) in columns_with_color.iter().enumerate() {
let eps = perturbation_sizes[idx];
let mut col_indices = Vec::new();
for i in 0..n_dim {
if pattern[[i, j]] {
col_indices.push(i);
}
}
let mut column_values = Vec::with_capacity(col_indices.len());
for &i in &col_indices {
let df = f_perturbed[i] - f_current[i];
column_values.push((i, j, df / eps));
}
color_columns.push(column_values);
}
color_columns.into_iter().flatten().collect::<Vec<_>>()
})
.collect();
for color_column in color_results {
for (i, j, value) in color_column {
jacobian[[i, j]] = value;
}
}
}
#[cfg(not(feature = "parallel_jacobian"))]
{
for color in 0..=max_color {
let columns_with_color: Vec<usize> = colors
.iter()
.enumerate()
.filter_map(|(j, &c)| if c == color { Some(j) } else { None })
.collect();
if columns_with_color.is_empty() {
continue;
}
let mut y_perturbed = y.clone();
let mut perturbation_sizes = Vec::with_capacity(columns_with_color.len());
for &j in &columns_with_color {
let eps = eps_base * (F::one() + y[j].abs()).max(F::one());
y_perturbed[j] += eps;
perturbation_sizes.push(eps);
}
let f_perturbed = f(t, y_perturbed.view());
for (idx, &j) in columns_with_color.iter().enumerate() {
let eps = perturbation_sizes[idx];
for i in 0..n_dim {
if pattern[[i, j]] {
let df = f_perturbed[i] - f_current[i];
jacobian[[i, j]] = df / eps;
}
}
}
}
}
Ok(jacobian)
}
#[allow(dead_code)]
fn greedy_coloring(_sparsitypattern: &Array2<bool>) -> Vec<usize> {
let (n_rows, n_cols) = _sparsitypattern.dim();
let mut colors = vec![usize::MAX; n_cols];
for j in 0..n_cols {
let mut non_zero_rows = Vec::new();
for i in 0..n_rows {
if _sparsitypattern[[i, j]] {
non_zero_rows.push(i);
}
}
let mut used_colors = Vec::new();
for &row in &non_zero_rows {
for k in 0..j {
if _sparsitypattern[[row, k]] && colors[k] != usize::MAX {
used_colors.push(colors[k]);
}
}
}
let mut color = 0;
while used_colors.contains(&color) {
color += 1;
}
colors[j] = color;
}
colors
}
#[allow(dead_code)]
pub fn should_use_parallel_jacobian(n_dim: usize, is_sparse: bool, numthreads: usize) -> bool {
#[cfg(not(feature = "parallel_jacobian"))]
{
let _ = n_dim;
let _ = is_sparse;
let _ = numthreads;
false }
#[cfg(feature = "parallel_jacobian")]
{
if num_threads() <= 1 {
return false;
}
if n_dim < 20 {
return false;
}
if is_sparse {
if n_dim > 100 {
return true;
}
return false;
}
n_dim >= 20
}
}
pub struct ParallelJacobianStrategy {
pub use_parallel: bool,
pub is_sparse: bool,
pub sparsity_pattern: Option<Array2<bool>>,
pub jacobian: Option<Array2<f64>>,
pub num_threads: usize,
}
impl ParallelJacobianStrategy {
pub fn new(_n_dim: usize, issparse: bool) -> Self {
#[cfg(feature = "parallel_jacobian")]
let (use_parallel, num_threads) = {
let threads = num_threads();
(
should_use_parallel_jacobian(_n_dim, issparse, threads),
threads,
)
};
#[cfg(not(feature = "parallel_jacobian"))]
let (use_parallel, num_threads) = {
let _ = _n_dim;
(false, 1)
};
ParallelJacobianStrategy {
use_parallel,
is_sparse: issparse,
sparsity_pattern: None,
jacobian: None,
num_threads,
}
}
pub fn set_sparsity_pattern(&mut self, pattern: Array2<bool>) {
self.sparsity_pattern = Some(pattern);
self.is_sparse = true;
}
pub fn compute_jacobian<F, Func>(
&mut self,
f: &Func,
t: F,
y: &Array1<F>,
f_current: &Array1<F>,
perturbation_scale: F,
) -> IntegrateResult<Array2<F>>
where
F: IntegrateFloat + Send + Sync,
Func: Fn(F, ArrayView1<F>) -> Array1<F> + Sync,
{
let jacobian = if !self.use_parallel {
crate::ode::utils::common::finite_difference_jacobian(
f,
t,
y,
f_current,
perturbation_scale,
)
} else {
if self.is_sparse && self.sparsity_pattern.is_some() {
parallel_sparse_jacobian(
f,
t,
y,
f_current,
self.sparsity_pattern.as_ref(),
perturbation_scale,
)?
} else {
parallel_finite_difference_jacobian(f, t, y, f_current, perturbation_scale)?
}
};
let jacobian_f64 = jacobian.mapv(|x| x.to_f64().unwrap_or(0.0));
self.jacobian = Some(jacobian_f64);
Ok(jacobian)
}
pub fn is_parallel_available(&self) -> bool {
#[cfg(feature = "parallel_jacobian")]
{
true
}
#[cfg(not(feature = "parallel_jacobian"))]
{
false
}
}
}
#[cfg(test)]
mod tests {
use super::greedy_coloring;
use crate::ode::utils::{parallel_finite_difference_jacobian, parallel_sparse_jacobian};
use scirs2_core::ndarray::{array, Array1, Array2, ArrayView1};
fn test_func(t: f64, y: ArrayView1<f64>) -> Array1<f64> {
array![y[0] * y[0] + y[1], y[0] * y[1] + y[2], y[2] * y[2] - y[0]]
}
fn analytic_jacobian(y: &[f64]) -> Array2<f64> {
array![
[2.0 * y[0], 1.0, 0.0],
[y[1], y[0], 1.0],
[-1.0, 0.0, 2.0 * y[2]]
]
}
#[test]
fn test_parallel_jacobian() {
let y = array![1.0, 2.0, 3.0];
let t = 0.0;
let f_current = test_func(t, y.view());
let numerical_jac = parallel_finite_difference_jacobian(&test_func, t, &y, &f_current, 1.0)
.expect("Operation failed");
let analytic_jac = analytic_jacobian(&[1.0, 2.0, 3.0]);
for i in 0..3 {
for j in 0..3 {
assert!((numerical_jac[[i, j]] - analytic_jac[[i, j]]).abs() < 1e-5);
}
}
}
#[test]
fn test_sparse_jacobian() {
let y = array![1.0, 2.0, 3.0];
let t = 0.0;
let f_current = test_func(t, y.view());
let sparsity_pattern = array![[true, true, false], [true, true, true], [true, false, true]];
let numerical_jac =
parallel_sparse_jacobian(&test_func, t, &y, &f_current, Some(&sparsity_pattern), 1.0)
.expect("Operation failed");
let analytic_jac = analytic_jacobian(&[1.0, 2.0, 3.0]);
for i in 0..3 {
for j in 0..3 {
if sparsity_pattern[[i, j]] {
assert!((numerical_jac[[i, j]] - analytic_jac[[i, j]]).abs() < 1e-5);
} else {
assert_eq!(numerical_jac[[i, j]], 0.0);
}
}
}
}
#[test]
fn test_greedy_coloring() {
let sparsity_pattern = array![
[true, true, false, false],
[true, false, true, false],
[false, true, true, true]
];
let colors = greedy_coloring(&sparsity_pattern);
for i in 0..sparsity_pattern.nrows() {
for j1 in 0..sparsity_pattern.ncols() {
if !sparsity_pattern[[i, j1]] {
continue;
}
for j2 in (j1 + 1)..sparsity_pattern.ncols() {
if sparsity_pattern[[i, j2]] {
assert_ne!(colors[j1], colors[j2]);
}
}
}
}
}
}