scirs2_integrate/autodiff/
sparse.rs

1//! Sparse Jacobian optimization
2//!
3//! This module provides functionality for detecting and exploiting sparsity
4//! patterns in Jacobian matrices to improve computational efficiency.
5
6use crate::common::IntegrateFloat;
7use crate::error::{IntegrateError, IntegrateResult};
8use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
9use scirs2_core::parallel_ops::*;
10use std::collections::{HashMap, HashSet};
11
12/// Represents a sparsity pattern
13#[derive(Debug, Clone)]
14pub struct SparsePattern {
15    /// Non-zero entries as (row, col) pairs
16    pub entries: Vec<(usize, usize)>,
17    /// Number of rows
18    pub nrows: usize,
19    /// Number of columns
20    pub n_cols: usize,
21    /// Row-wise non-zero indices
22    pub row_indices: Vec<Vec<usize>>,
23    /// Column-wise non-zero indices
24    pub col_indices: Vec<Vec<usize>>,
25}
26
27impl SparsePattern {
28    /// Create a new sparse pattern
29    pub fn new(_nrows: usize, ncols: usize) -> Self {
30        SparsePattern {
31            entries: Vec::new(),
32            nrows: _nrows,
33            n_cols: ncols,
34            row_indices: vec![Vec::new(); _nrows],
35            col_indices: vec![Vec::new(); ncols],
36        }
37    }
38
39    /// Add a non-zero entry
40    pub fn add_entry(&mut self, row: usize, col: usize) {
41        if row < self.nrows && col < self.n_cols {
42            self.entries.push((row, col));
43            self.row_indices[row].push(col);
44            self.col_indices[col].push(row);
45        }
46    }
47
48    /// Get the number of non-zeros
49    pub fn nnz(&self) -> usize {
50        self.entries.len()
51    }
52
53    /// Get the sparsity ratio (fraction of zeros)
54    pub fn sparsity(&self) -> f64 {
55        let total = (self.nrows * self.n_cols) as f64;
56        if total > 0.0 {
57            1.0 - (self.nnz() as f64 / total)
58        } else {
59            0.0
60        }
61    }
62
63    /// Check if pattern is sparse enough to benefit from sparse methods
64    pub fn is_sparse(&self, threshold: f64) -> bool {
65        self.sparsity() > threshold
66    }
67
68    /// Compute coloring for efficient Jacobian computation
69    pub fn compute_coloring(&self) -> ColGrouping {
70        // Use improved Welsh-Powell algorithm for better coloring
71        let mut degrees: Vec<(usize, usize)> = Vec::new();
72
73        // Calculate degree of each column (number of structural neighbors)
74        for col in 0..self.n_cols {
75            let mut neighbors = HashSet::new();
76            for &row in &self.col_indices[col] {
77                for &other_col in &self.row_indices[row] {
78                    if other_col != col {
79                        neighbors.insert(other_col);
80                    }
81                }
82            }
83            degrees.push((col, neighbors.len()));
84        }
85
86        // Sort by degree (descending)
87        degrees.sort_by_key(|&(_, deg)| std::cmp::Reverse(deg));
88
89        let mut colors: HashMap<usize, usize> = HashMap::new();
90        let mut max_color = 0;
91
92        // Color vertices in order of decreasing degree
93        for (col_, _) in degrees {
94            let mut used_colors = HashSet::new();
95
96            // Find colors used by adjacent columns
97            for &row in &self.col_indices[col_] {
98                for &other_col in &self.row_indices[row] {
99                    if other_col != col_ {
100                        if let Some(&color) = colors.get(&other_col) {
101                            used_colors.insert(color);
102                        }
103                    }
104                }
105            }
106
107            // Find first available color
108            let mut color = 0;
109            while used_colors.contains(&color) {
110                color += 1;
111            }
112
113            colors.insert(col_, color);
114            max_color = max_color.max(color);
115        }
116
117        // Group columns by color
118        let mut groups = vec![Vec::new(); max_color + 1];
119        for (&col, &color) in &colors {
120            groups[color].push(col);
121        }
122
123        ColGrouping { groups }
124    }
125}
126
127/// Column grouping for efficient Jacobian computation
128pub struct ColGrouping {
129    /// Groups of columns that can be computed together
130    pub groups: Vec<Vec<usize>>,
131}
132
133impl ColGrouping {
134    /// Get the number of groups
135    pub fn n_groups(&self) -> usize {
136        self.groups.len()
137    }
138}
139
140/// Sparse Jacobian representation
141pub struct SparseJacobian<F: IntegrateFloat> {
142    /// The sparsity pattern
143    pub pattern: SparsePattern,
144    /// Non-zero values in row-major order
145    pub values: Vec<F>,
146    /// Mapping from (row, col) to value index
147    pub index_map: HashMap<(usize, usize), usize>,
148}
149
150impl<F: IntegrateFloat> SparseJacobian<F> {
151    /// Create a new sparse Jacobian
152    pub fn new(pattern: SparsePattern) -> Self {
153        let nnz = pattern.nnz();
154        let mut index_map = HashMap::new();
155
156        for (i, &(row, col)) in pattern.entries.iter().enumerate() {
157            index_map.insert((row, col), i);
158        }
159
160        SparseJacobian {
161            pattern,
162            values: vec![F::zero(); nnz],
163            index_map,
164        }
165    }
166
167    /// Set a value in the sparse Jacobian
168    pub fn set(&mut self, row: usize, col: usize, value: F) -> IntegrateResult<()> {
169        if let Some(&idx) = self.index_map.get(&(row, col)) {
170            self.values[idx] = value;
171            Ok(())
172        } else {
173            Err(IntegrateError::IndexError(format!(
174                "Entry ({row}, {col}) not in sparsity pattern"
175            )))
176        }
177    }
178
179    /// Get a value from the sparse Jacobian
180    pub fn get(&self, row: usize, col: usize) -> Option<F> {
181        self.index_map.get(&(row, col)).map(|&idx| self.values[idx])
182    }
183
184    /// Convert to dense matrix
185    pub fn to_dense(&self) -> Array2<F> {
186        let mut dense = Array2::zeros((self.pattern.nrows, self.pattern.n_cols));
187        for (&(row, col), &idx) in &self.index_map {
188            dense[[row, col]] = self.values[idx];
189        }
190        dense
191    }
192
193    /// Multiply by a vector: y = J * x
194    pub fn matvec(&self, x: ArrayView1<F>) -> IntegrateResult<Array1<F>> {
195        if x.len() != self.pattern.n_cols {
196            return Err(IntegrateError::DimensionMismatch(format!(
197                "Expected {} columns, got {}",
198                self.pattern.n_cols,
199                x.len()
200            )));
201        }
202
203        let mut y = Array1::zeros(self.pattern.nrows);
204
205        for (&(row, col), &idx) in &self.index_map {
206            y[row] += self.values[idx] * x[col];
207        }
208
209        Ok(y)
210    }
211
212    /// Transpose multiply by a vector: y = J^T * x
213    pub fn matvec_transpose(&self, x: ArrayView1<F>) -> IntegrateResult<Array1<F>> {
214        if x.len() != self.pattern.nrows {
215            return Err(IntegrateError::DimensionMismatch(format!(
216                "Expected {} rows, got {}",
217                self.pattern.nrows,
218                x.len()
219            )));
220        }
221
222        let mut y = Array1::zeros(self.pattern.n_cols);
223
224        for (&(row, col), &idx) in &self.index_map {
225            y[col] += self.values[idx] * x[row];
226        }
227
228        Ok(y)
229    }
230}
231
232/// Compressed Sparse Row (CSR) format for efficient operations
233pub struct CSRJacobian<F: IntegrateFloat> {
234    /// Number of rows
235    pub nrows: usize,
236    /// Number of columns
237    pub n_cols: usize,
238    /// Row pointers (size nrows + 1)
239    pub row_ptr: Vec<usize>,
240    /// Column indices (size nnz)
241    pub col_idx: Vec<usize>,
242    /// Non-zero values (size nnz)
243    pub values: Vec<F>,
244}
245
246/// Compressed Sparse Column (CSC) format for efficient column operations
247pub struct CSCJacobian<F: IntegrateFloat> {
248    /// Number of rows
249    pub nrows: usize,
250    /// Number of columns
251    pub n_cols: usize,
252    /// Column pointers (size n_cols + 1)
253    pub col_ptr: Vec<usize>,
254    /// Row indices (size nnz)
255    pub row_idx: Vec<usize>,
256    /// Non-zero values (size nnz)
257    pub values: Vec<F>,
258}
259
260impl<F: IntegrateFloat> SparseJacobian<F> {
261    /// Create a new sparse Jacobian from pattern
262    pub fn from_pattern(pattern: SparsePattern) -> Self {
263        let mut index_map = HashMap::new();
264        for (idx, &(row, col)) in pattern.entries.iter().enumerate() {
265            index_map.insert((row, col), idx);
266        }
267
268        SparseJacobian {
269            values: vec![F::zero(); pattern.entries.len()],
270            pattern,
271            index_map,
272        }
273    }
274
275    /// Set a value without error checking
276    pub fn set_unchecked(&mut self, row: usize, col: usize, value: F) {
277        if let Some(&idx) = self.index_map.get(&(row, col)) {
278            self.values[idx] = value;
279        }
280    }
281
282    /// Get a value with default zero
283    pub fn get_or_zero(&self, row: usize, col: usize) -> F {
284        if let Some(&idx) = self.index_map.get(&(row, col)) {
285            self.values[idx]
286        } else {
287            F::zero()
288        }
289    }
290
291    /// Convert to dense matrix (alternative implementation)
292    pub fn to_dense_alt(&self) -> Array2<F> {
293        let mut dense = Array2::zeros((self.pattern.nrows, self.pattern.n_cols));
294        for (idx, &(row, col)) in self.pattern.entries.iter().enumerate() {
295            dense[[row, col]] = self.values[idx];
296        }
297        dense
298    }
299
300    /// Apply to vector (matrix-vector multiplication)
301    pub fn apply(&self, x: ArrayView1<F>) -> IntegrateResult<Array1<F>> {
302        if x.len() != self.pattern.n_cols {
303            return Err(IntegrateError::DimensionMismatch(format!(
304                "Expected {} columns, got {}",
305                self.pattern.n_cols,
306                x.len()
307            )));
308        }
309
310        let mut result = Array1::zeros(self.pattern.nrows);
311        for (idx, &(row, col)) in self.pattern.entries.iter().enumerate() {
312            result[row] += self.values[idx] * x[col];
313        }
314
315        Ok(result)
316    }
317
318    /// Convert to CSR format for efficient row operations
319    pub fn to_csr(&self) -> CSRJacobian<F> {
320        let mut entries: Vec<(usize, usize, F)> = Vec::new();
321        for (idx, &(row, col)) in self.pattern.entries.iter().enumerate() {
322            entries.push((row, col, self.values[idx]));
323        }
324
325        // Sort by row, then column
326        entries.sort_by_key(|&(r, c, _)| (r, c));
327
328        let mut row_ptr = vec![0];
329        let mut col_idx = Vec::new();
330        let mut values = Vec::new();
331
332        let mut currentrow = 0;
333        for (row, col, val) in entries {
334            while currentrow < row {
335                row_ptr.push(col_idx.len());
336                currentrow += 1;
337            }
338            col_idx.push(col);
339            values.push(val);
340        }
341
342        // Fill remaining row pointers
343        while row_ptr.len() <= self.pattern.nrows {
344            row_ptr.push(col_idx.len());
345        }
346
347        CSRJacobian {
348            nrows: self.pattern.nrows,
349            n_cols: self.pattern.n_cols,
350            row_ptr,
351            col_idx,
352            values,
353        }
354    }
355}
356
357/// Detect sparsity pattern by probing with finite differences
358#[allow(dead_code)]
359pub fn detect_sparsity<F, Func>(f: Func, x: ArrayView1<F>, eps: F) -> IntegrateResult<SparsePattern>
360where
361    F: IntegrateFloat + Send + Sync,
362    Func: Fn(ArrayView1<F>) -> IntegrateResult<Array1<F>> + Sync,
363{
364    let n = x.len();
365    let f0 = f(x)?;
366    let m = f0.len();
367
368    let mut pattern = SparsePattern::new(m, n);
369
370    // Parallel probing with batched perturbations
371    let results: Vec<_> = (0..n)
372        .collect::<Vec<_>>()
373        .par_chunks(n / scirs2_core::parallel_ops::num_threads().max(1) + 1)
374        .map(|chunk| {
375            let mut local_entries = Vec::new();
376            for &j in chunk {
377                let mut x_pert = x.to_owned();
378                x_pert[j] += eps;
379                if let Ok(f_pert) = f(x_pert.view()) {
380                    for i in 0..m {
381                        if (f_pert[i] - f0[i]).abs() > F::epsilon() {
382                            local_entries.push((i, j));
383                        }
384                    }
385                }
386            }
387            local_entries
388        })
389        .collect();
390
391    // Merge results
392    for entries in results {
393        for (i, j) in entries {
394            pattern.add_entry(i, j);
395        }
396    }
397
398    Ok(pattern)
399}
400
401impl<F: IntegrateFloat + Send + Sync> CSRJacobian<F> {
402    /// Apply to vector (matrix-vector multiplication) - optimized for CSR
403    pub fn apply(&self, x: ArrayView1<F>) -> IntegrateResult<Array1<F>> {
404        if x.len() != self.n_cols {
405            return Err(IntegrateError::DimensionMismatch(format!(
406                "Expected {} columns, got {}",
407                self.n_cols,
408                x.len()
409            )));
410        }
411
412        let mut result = Array1::zeros(self.nrows);
413
414        // Parallel row-wise computation
415        let chunk_size = (self.nrows / scirs2_core::parallel_ops::num_threads()).max(1);
416        let chunks: Vec<_> = (0..self.nrows)
417            .collect::<Vec<_>>()
418            .par_chunks(chunk_size)
419            .map(|rows| {
420                let mut local_result = Array1::zeros(rows.len());
421                for (local_idx, &row) in rows.iter().enumerate() {
422                    let start = self.row_ptr[row];
423                    let end = self.row_ptr[row + 1];
424                    for idx in start..end {
425                        local_result[local_idx] += self.values[idx] * x[self.col_idx[idx]];
426                    }
427                }
428                (rows[0], local_result)
429            })
430            .collect();
431
432        // Combine results
433        for (startrow, chunk) in chunks {
434            for (i, val) in chunk.iter().enumerate() {
435                result[startrow + i] = *val;
436            }
437        }
438
439        Ok(result)
440    }
441
442    /// Transpose to CSC format
443    pub fn transpose(&self) -> CSCJacobian<F> {
444        let mut entries: Vec<(usize, usize, F)> = Vec::new();
445
446        for row in 0..self.nrows {
447            let start = self.row_ptr[row];
448            let end = self.row_ptr[row + 1];
449            for idx in start..end {
450                entries.push((self.col_idx[idx], row, self.values[idx]));
451            }
452        }
453
454        // Sort by column, then row
455        entries.sort_by_key(|&(c, r, _)| (c, r));
456
457        let mut col_ptr = vec![0];
458        let mut row_idx = Vec::new();
459        let mut values = Vec::new();
460
461        let mut current_col = 0;
462        for (col, row, val) in entries {
463            while current_col < col {
464                col_ptr.push(row_idx.len());
465                current_col += 1;
466            }
467            row_idx.push(row);
468            values.push(val);
469        }
470
471        // Fill remaining column pointers
472        while col_ptr.len() <= self.n_cols {
473            col_ptr.push(row_idx.len());
474        }
475
476        CSCJacobian {
477            nrows: self.n_cols,
478            n_cols: self.nrows,
479            col_ptr,
480            row_idx,
481            values,
482        }
483    }
484}
485
486/// Compress a dense Jacobian using a sparsity pattern
487#[allow(dead_code)]
488pub fn compress_jacobian<F: IntegrateFloat>(
489    dense: ArrayView2<F>,
490    pattern: &SparsePattern,
491) -> SparseJacobian<F> {
492    let mut sparse = SparseJacobian::from_pattern(pattern.clone());
493
494    for (idx, &(row, col)) in pattern.entries.iter().enumerate() {
495        sparse.values[idx] = dense[[row, col]];
496    }
497
498    sparse
499}
500
501/// Compute sparse Jacobian using coloring
502#[allow(dead_code)]
503pub fn colored_jacobian<F, Func>(
504    f: Func,
505    x: ArrayView1<F>,
506    pattern: &SparsePattern,
507    eps: F,
508) -> IntegrateResult<SparseJacobian<F>>
509where
510    F: IntegrateFloat,
511    Func: Fn(ArrayView1<F>) -> IntegrateResult<Array1<F>>,
512{
513    let coloring = pattern.compute_coloring();
514    let f0 = f(x)?;
515    let mut jacobian = SparseJacobian::from_pattern(pattern.clone());
516
517    // Compute Jacobian using column groups
518    for group in &coloring.groups {
519        let mut x_pert = x.to_owned();
520
521        // Perturb all columns in this group
522        for &col in group {
523            x_pert[col] += eps;
524        }
525
526        let f_pert = f(x_pert.view())?;
527
528        // Extract derivatives for this group
529        for &col in group {
530            for &row in &pattern.col_indices[col] {
531                let deriv = (f_pert[row] - f0[row]) / eps;
532                let _ = jacobian.set(row, col, deriv);
533            }
534        }
535    }
536
537    Ok(jacobian)
538}
539
540/// Example: Create a tridiagonal sparsity pattern
541#[allow(dead_code)]
542pub fn example_tridiagonal_pattern(n: usize) -> SparsePattern {
543    let mut pattern = SparsePattern::new(n, n);
544
545    for i in 0..n {
546        pattern.add_entry(i, i); // Diagonal
547        if i > 0 {
548            pattern.add_entry(i, i - 1); // Sub-diagonal
549        }
550        if i < n - 1 {
551            pattern.add_entry(i, i + 1); // Super-diagonal
552        }
553    }
554
555    pattern
556}
557
558/// Sparse Jacobian updater for quasi-Newton methods
559pub struct SparseJacobianUpdater<F: IntegrateFloat> {
560    pattern: SparsePattern,
561    threshold: F,
562}
563
564impl<F: IntegrateFloat> SparseJacobianUpdater<F> {
565    /// Create a new updater
566    pub fn new(pattern: SparsePattern, threshold: F) -> Self {
567        SparseJacobianUpdater { pattern, threshold }
568    }
569
570    /// Update sparse Jacobian using Broyden's method
571    pub fn broyden_update(
572        &self,
573        jac: &mut SparseJacobian<F>,
574        dx: ArrayView1<F>,
575        df: ArrayView1<F>,
576    ) -> IntegrateResult<()> {
577        let jdx = jac.apply(dx)?;
578        let dy = &df - &jdx;
579
580        let dx_norm_sq = dx.dot(&dx);
581        if dx_norm_sq < self.threshold {
582            return Ok(());
583        }
584
585        // Update only non-zero entries
586        for (idx, &(i, j)) in self.pattern.entries.iter().enumerate() {
587            jac.values[idx] += dy[i] * dx[j] / dx_norm_sq;
588        }
589
590        Ok(())
591    }
592}
593
594/// Adaptive sparsity detection with multiple perturbation sizes
595#[allow(dead_code)]
596pub fn detect_sparsity_adaptive<F, Func>(
597    f: Func,
598    x: ArrayView1<F>,
599    eps_range: (F, F),
600    n_samples: usize,
601) -> IntegrateResult<SparsePattern>
602where
603    F: IntegrateFloat + Send + Sync,
604    Func: Fn(ArrayView1<F>) -> IntegrateResult<Array1<F>> + Sync,
605{
606    let n = x.len();
607    let f0 = f(x)?;
608    let m = f0.len();
609
610    let mut accumulated_pattern = SparsePattern::new(m, n);
611    let eps_min = eps_range.0;
612    let eps_max = eps_range.1;
613
614    // Try multiple perturbation sizes
615    for sample in 0..n_samples {
616        let alpha = F::from(sample).unwrap() / F::from(n_samples - 1).unwrap();
617        let eps = eps_min * (F::one() - alpha) + eps_max * alpha;
618
619        let pattern = detect_sparsity(&f, x, eps)?;
620
621        // Merge patterns
622        for &(i, j) in &pattern.entries {
623            accumulated_pattern.add_entry(i, j);
624        }
625    }
626
627    Ok(accumulated_pattern)
628}
629
630/// Block-structured sparsity pattern
631pub struct BlockPattern {
632    /// Block sizes (rows, cols)
633    pub block_sizes: Vec<(usize, usize)>,
634    /// Non-zero blocks as (blockrow, block_col) pairs
635    pub blocks: Vec<(usize, usize)>,
636    /// Total rows and columns
637    pub nrows: usize,
638    pub n_cols: usize,
639}
640
641impl BlockPattern {
642    /// Convert block pattern to regular sparsity pattern
643    pub fn to_sparse_pattern(&self) -> SparsePattern {
644        let mut pattern = SparsePattern::new(self.nrows, self.n_cols);
645
646        let mut row_offset = 0;
647        let mut col_offset = 0;
648
649        for &(blockrow, block_col) in &self.blocks {
650            let (block_height, block_width) = self.block_sizes[blockrow];
651
652            // Add all entries in this block
653            for i in 0..block_height {
654                for j in 0..block_width {
655                    pattern.add_entry(row_offset + i, col_offset + j);
656                }
657            }
658
659            col_offset += block_width;
660            if block_col == self.blocks.len() - 1 {
661                col_offset = 0;
662                row_offset += block_height;
663            }
664        }
665
666        pattern
667    }
668}
669
670/// Hybrid sparse format for mixed dense/sparse blocks
671pub struct HybridJacobian<F: IntegrateFloat> {
672    /// Sparse blocks in CSR format
673    pub sparse_blocks: Vec<CSRJacobian<F>>,
674    /// Dense blocks
675    pub dense_blocks: Vec<Array2<F>>,
676    /// Block layout information
677    pub block_info: BlockPattern,
678}
679
680#[cfg(test)]
681mod tests {
682    use super::*;
683    use crate::{SparseJacobian, SparsePattern};
684    use scirs2_core::ndarray::Array1;
685
686    #[test]
687    fn test_sparse_pattern() {
688        let mut pattern = SparsePattern::new(3, 3);
689        pattern.add_entry(0, 0);
690        pattern.add_entry(1, 1);
691        pattern.add_entry(2, 2);
692        pattern.add_entry(0, 1);
693
694        assert_eq!(pattern.nnz(), 4);
695        assert!(pattern.sparsity() > 0.5);
696    }
697
698    #[test]
699    fn test_coloring() {
700        let pattern = example_tridiagonal_pattern(5);
701        let coloring = pattern.compute_coloring();
702
703        // Tridiagonal matrix should need at most 3 colors
704        assert!(coloring.n_groups() <= 3);
705    }
706
707    #[test]
708    fn test_sparse_jacobian() {
709        let pattern = example_tridiagonal_pattern(3);
710        let mut jac = SparseJacobian::from_pattern(pattern);
711
712        // Set some values
713        let _ = jac.set(0, 0, 2.0);
714        let _ = jac.set(0, 1, -1.0);
715        let _ = jac.set(1, 0, -1.0);
716        let _ = jac.set(1, 1, 2.0);
717        let _ = jac.set(1, 2, -1.0);
718        let _ = jac.set(2, 1, -1.0);
719        let _ = jac.set(2, 2, 2.0);
720
721        // Test matrix-vector multiplication
722        let x = Array1::from_vec(vec![1.0, 2.0, 3.0]);
723        let y = jac.apply(x.view()).unwrap();
724
725        // Should compute [2*1 - 1*2, -1*1 + 2*2 - 1*3, -1*2 + 2*3]
726        assert!((y[0] - 0.0_f64).abs() < 1e-10);
727        assert!((y[1] - 0.0_f64).abs() < 1e-10);
728        assert!((y[2] - 4.0_f64).abs() < 1e-10);
729    }
730
731    #[test]
732    fn test_csr_format() {
733        let pattern = example_tridiagonal_pattern(3);
734        let mut jac = SparseJacobian::from_pattern(pattern);
735
736        // Set values
737        let _ = jac.set(0, 0, 2.0);
738        let _ = jac.set(0, 1, -1.0);
739        let _ = jac.set(1, 0, -1.0);
740        let _ = jac.set(1, 1, 2.0);
741        let _ = jac.set(1, 2, -1.0);
742        let _ = jac.set(2, 1, -1.0);
743        let _ = jac.set(2, 2, 2.0);
744
745        // Convert to CSR
746        let csr = jac.to_csr();
747
748        // Test CSR matrix-vector multiplication
749        let x = Array1::from_vec(vec![1.0, 2.0, 3.0]);
750        let y = csr.apply(x.view()).unwrap();
751
752        assert!((y[0] - 0.0_f64).abs() < 1e-10);
753        assert!((y[1] - 0.0_f64).abs() < 1e-10);
754        assert!((y[2] - 4.0_f64).abs() < 1e-10);
755    }
756}