// Copyright (c) 2024 RustyTorch++ Team // Licensed under the Apache License, Version 2.0 //! Sparse matrix implementations for finite element assembly. use crate::error::{AssemblyError, FeaResult}; use nalgebra::DVector; // use rtx_tensor::Tensor; // Optional dependency use std::collections::HashMap; /// Compressed Sparse Row (CSR) matrix format. #[derive(Debug, Clone)] pub struct SparseMatrix { /// Number of rows nrows: usize, /// Number of columns ncols: usize, /// Row pointers row_ptr: Vec, /// Column indices col_idx: Vec, /// Non-zero values values: Vec, /// Assembly map for efficient insertion during assembly assembly_map: HashMap<(usize, usize), usize>, /// Whether the matrix has been finalized finalized: bool, } impl SparseMatrix { /// Create a new sparse matrix. pub fn new(nrows: usize, ncols: usize) -> Self { Self { nrows, ncols, row_ptr: vec![0; nrows + 1], col_idx: Vec::new(), values: Vec::new(), assembly_map: HashMap::new(), finalized: false, } } /// Create from triplet format (row, col, value). pub fn from_triplets( nrows: usize, ncols: usize, triplets: &[(usize, usize, f64)], ) -> FeaResult { let mut matrix = Self::new(nrows, ncols); for &(row, col, value) in triplets { matrix.add_entry(row, col, value)?; } matrix.finalize()?; Ok(matrix) } /// Get number of rows. pub fn nrows(&self) -> usize { self.nrows } /// Get number of columns. pub fn ncols(&self) -> usize { self.ncols } /// Get number of non-zeros. pub fn nnz(&self) -> usize { self.values.len() } /// Get matrix density. pub fn density(&self) -> f64 { if self.nrows == 0 || self.ncols == 0 { 0.0 } else { self.nnz() as f64 / (self.nrows * self.ncols) as f64 } } /// Get matrix bandwidth. pub fn bandwidth(&self) -> usize { if !self.finalized { // For unfinalized matrix, compute from assembly_map let mut max_bandwidth = 0; for (row, col) in self.assembly_map.keys() { let distance = row.abs_diff(*col); max_bandwidth = max_bandwidth.max(distance); } max_bandwidth } else { // For finalized matrix, compute from CSR structure let mut max_bandwidth = 0; for row in 0..self.nrows { let start = self.row_ptr[row]; let end = self.row_ptr[row + 1]; for idx in start..end { let col = self.col_idx[idx]; let distance = row.abs_diff(col); max_bandwidth = max_bandwidth.max(distance); } } max_bandwidth } } /// Add entry to the matrix (accumulates if entry exists). pub fn add_entry(&mut self, row: usize, col: usize, value: f64) -> FeaResult<()> { if self.finalized { return Err(AssemblyError::SparseAssemblyFailed { reason: "Cannot add entries to finalized matrix".to_string(), } .into()); } if row >= self.nrows || col >= self.ncols { return Err(AssemblyError::MatrixDimensionMismatch { expected_rows: self.nrows, expected_cols: self.ncols, actual_rows: row + 1, actual_cols: col + 1, } .into()); } if value.abs() < 1e-15 { return Ok(()); // Skip near-zero entries } let key = (row, col); if let Some(&idx) = self.assembly_map.get(&key) { // Entry exists, accumulate self.values[idx] += value; } else { // New entry let idx = self.values.len(); self.values.push(value); self.col_idx.push(col); self.assembly_map.insert(key, idx); } Ok(()) } /// Set entry in the matrix (overwrites if entry exists). pub fn set_entry(&mut self, row: usize, col: usize, value: f64) -> FeaResult<()> { if self.finalized { return Err(AssemblyError::SparseAssemblyFailed { reason: "Cannot set entries in finalized matrix".to_string(), } .into()); } let key = (row, col); if let Some(&idx) = self.assembly_map.get(&key) { // Entry exists, overwrite self.values[idx] = value; } else if value.abs() >= 1e-15 { // New non-zero entry let idx = self.values.len(); self.values.push(value); self.col_idx.push(col); self.assembly_map.insert(key, idx); } Ok(()) } /// Apply Dirichlet boundary condition. pub fn set_dirichlet(&mut self, dof: usize, _value: f64) -> FeaResult<()> { if dof >= self.nrows { return Err(AssemblyError::GlobalDofOutOfBounds { index: dof, max_index: self.nrows.saturating_sub(1), } .into()); } // Zero out row and column for col in 0..self.ncols { if col != dof { self.set_entry(dof, col, 0.0)?; } } for row in 0..self.nrows { if row != dof { self.set_entry(row, dof, 0.0)?; } } // Set diagonal entry to 1 self.set_entry(dof, dof, 1.0)?; Ok(()) } /// Finalize the matrix (convert to CSR format). pub fn finalize(&mut self) -> FeaResult<()> { if self.finalized { return Ok(()); } // Group entries by row let mut row_entries: Vec> = vec![Vec::new(); self.nrows]; for (&(row, col), &idx) in &self.assembly_map { let value = self.values[idx]; if value.abs() >= 1e-15 { row_entries[row].push((col, value)); } } // Sort columns within each row for row_data in &mut row_entries { row_data.sort_by_key(|&(col, _)| col); } // Build CSR format self.values.clear(); self.col_idx.clear(); self.row_ptr = vec![0; self.nrows + 1]; let mut nnz = 0; for (row, row_data) in row_entries.iter().enumerate() { self.row_ptr[row] = nnz; for &(col, value) in row_data { self.col_idx.push(col); self.values.push(value); nnz += 1; } } self.row_ptr[self.nrows] = nnz; // Clear assembly map to save memory self.assembly_map.clear(); self.finalized = true; Ok(()) } /// Get entry value. pub fn get_entry(&self, row: usize, col: usize) -> f64 { if !self.finalized { // Search in assembly map self.assembly_map .get(&(row, col)) .map_or(0.0, |&idx| self.values[idx]) } else { // Search in CSR format let start = self.row_ptr[row]; let end = self.row_ptr[row + 1]; for i in start..end { if self.col_idx[i] == col { return self.values[i]; } } 0.0 } } /// Matrix-vector multiplication: y = A * x. pub fn multiply_vector(&self, x: &DVector) -> FeaResult> { if !self.finalized { return Err(AssemblyError::SparseAssemblyFailed { reason: "Matrix must be finalized before multiplication".to_string(), } .into()); } if x.len() != self.ncols { return Err(AssemblyError::MatrixDimensionMismatch { expected_rows: self.ncols, expected_cols: 1, actual_rows: x.len(), actual_cols: 1, } .into()); } let mut y = DVector::zeros(self.nrows); for row in 0..self.nrows { let start = self.row_ptr[row]; let end = self.row_ptr[row + 1]; let mut sum = 0.0; for i in start..end { sum += self.values[i] * x[self.col_idx[i]]; } y[row] = sum; } Ok(y) } /// Extract submatrix for given row and column indices. pub fn extract_submatrix( &self, row_indices: &[usize], col_indices: &[usize], ) -> FeaResult { if !self.finalized { return Err(AssemblyError::SparseAssemblyFailed { reason: "Matrix must be finalized before extraction".to_string(), } .into()); } let sub_nrows = row_indices.len(); let sub_ncols = col_indices.len(); let mut sub_matrix = Self::new(sub_nrows, sub_ncols); // Create column index mapping let mut col_map = HashMap::new(); for (new_col, &old_col) in col_indices.iter().enumerate() { col_map.insert(old_col, new_col); } for (new_row, &old_row) in row_indices.iter().enumerate() { let start = self.row_ptr[old_row]; let end = self.row_ptr[old_row + 1]; for i in start..end { let old_col = self.col_idx[i]; if let Some(&new_col) = col_map.get(&old_col) { let value = self.values[i]; sub_matrix.add_entry(new_row, new_col, value)?; } } } sub_matrix.finalize()?; Ok(sub_matrix) } /// Convert to dense matrix (for debugging/small matrices). pub fn to_dense(&self) -> nalgebra::DMatrix { let mut dense = nalgebra::DMatrix::zeros(self.nrows, self.ncols); if self.finalized { for row in 0..self.nrows { let start = self.row_ptr[row]; let end = self.row_ptr[row + 1]; for i in start..end { dense[(row, self.col_idx[i])] = self.values[i]; } } } else { for (&(row, col), &idx) in &self.assembly_map { dense[(row, col)] = self.values[idx]; } } dense } /// Get matrix structure (row pointers and column indices). pub fn structure(&self) -> (&[usize], &[usize]) { (&self.row_ptr, &self.col_idx) } /// Get matrix values. pub fn values(&self) -> &[f64] { &self.values } /// Check if matrix is symmetric (structure-wise). pub fn is_symmetric_structure(&self) -> bool { if !self.finalized || self.nrows != self.ncols { return false; } // Check if (i,j) exists whenever (j,i) exists let mut has_entry = vec![vec![false; self.ncols]; self.nrows]; for row in 0..self.nrows { let start = self.row_ptr[row]; let end = self.row_ptr[row + 1]; for i in start..end { has_entry[row][self.col_idx[i]] = true; } } for row in 0..self.nrows { for col in 0..self.ncols { if has_entry[row][col] != has_entry[col][row] { return false; } } } true } /// Check if matrix is symmetric (both structure and values). pub fn is_symmetric(&self) -> bool { if !self.finalized || self.nrows != self.ncols { return false; } const EPSILON: f64 = 1e-10; for row in 0..self.nrows { for col in row + 1..self.ncols { let val_ij = self.get_entry(row, col); let val_ji = self.get_entry(col, row); if (val_ij - val_ji).abs() > EPSILON { return false; } } } true } /// Compute matrix norm (Frobenius norm). pub fn frobenius_norm(&self) -> f64 { self.values.iter().map(|&x| x * x).sum::().sqrt() } /// Multiply transpose of matrix with vector: A^T * x pub fn transpose_multiply_vector(&self, x: &DVector) -> FeaResult> { if x.len() != self.nrows { return Err(AssemblyError::DimensionMismatch { expected: self.nrows, actual: x.len(), } .into()); } let mut result = DVector::zeros(self.ncols); for row in 0..self.nrows { for idx in self.row_ptr[row]..self.row_ptr[row + 1] { let col = self.col_idx[idx]; let val = self.values[idx]; result[col] += val * x[row]; } } Ok(result) } /// Extract diagonal elements pub fn diagonal(&self) -> FeaResult> { let n = self.nrows.min(self.ncols); let mut diag = DVector::zeros(n); for row in 0..n { for idx in self.row_ptr[row]..self.row_ptr[row + 1] { if self.col_idx[idx] == row { diag[row] = self.values[idx]; break; } } } Ok(diag) } /// Forward substitution for lower triangular system L * x = b pub fn forward_solve(&self, b: &DVector) -> FeaResult> { if b.len() != self.nrows { return Err(AssemblyError::DimensionMismatch { expected: self.nrows, actual: b.len(), } .into()); } let mut x = DVector::zeros(self.nrows); for row in 0..self.nrows { let mut sum = b[row]; let mut diag_val = 0.0; for idx in self.row_ptr[row]..self.row_ptr[row + 1] { let col = self.col_idx[idx]; let val = self.values[idx]; if col < row { sum -= val * x[col]; } else if col == row { diag_val = val; } } if diag_val.abs() < 1e-12 { return Err(AssemblyError::SingularMatrix.into()); } x[row] = sum / diag_val; } Ok(x) } /// Backward substitution for upper triangular system U * x = b pub fn backward_solve(&self, b: &DVector) -> FeaResult> { if b.len() != self.nrows { return Err(AssemblyError::DimensionMismatch { expected: self.nrows, actual: b.len(), } .into()); } let mut x = DVector::zeros(self.nrows); for row in (0..self.nrows).rev() { let mut sum = b[row]; let mut diag_val = 0.0; for idx in self.row_ptr[row]..self.row_ptr[row + 1] { let col = self.col_idx[idx]; let val = self.values[idx]; if col > row { sum -= val * x[col]; } else if col == row { diag_val = val; } } if diag_val.abs() < 1e-12 { return Err(AssemblyError::SingularMatrix.into()); } x[row] = sum / diag_val; } Ok(x) } /// Solve linear system A * x = b using LU decomposition pub fn solve_vector(&self, b: &DVector) -> FeaResult> { // For now, use a simple iterative method (Conjugate Gradient) // In production, this should use a proper sparse solver if b.len() != self.nrows || self.nrows != self.ncols { return Err(AssemblyError::DimensionMismatch { expected: self.nrows, actual: b.len(), } .into()); } // Simple Conjugate Gradient implementation for symmetric positive definite matrices let mut x = DVector::zeros(self.nrows); let mut r = b.clone(); let mut p = r.clone(); let tolerance = 1e-9; let max_iter = self.nrows * 2; for _iter in 0..max_iter { let ap = self.multiply_vector(&p)?; let r_dot_r = r.dot(&r); if r_dot_r < tolerance * tolerance { break; } let alpha = r_dot_r / p.dot(&ap); x += alpha * &p; r -= alpha * ≈ let r_dot_r_new = r.dot(&r); let beta = r_dot_r_new / r_dot_r; p = &r + beta * p; } Ok(x) } } // Also implement Add for &SparseMatrix + &SparseMatrix for convenience impl std::ops::Add<&SparseMatrix> for &SparseMatrix { type Output = SparseMatrix; fn add(self, rhs: &SparseMatrix) -> Self::Output { assert!(!(self.nrows != rhs.nrows || self.ncols != rhs.ncols), "Matrix dimensions must match for addition"); let mut result = SparseMatrix::new(self.nrows, self.ncols); // Add entries from left matrix if self.finalized { // Use CSR format for finalized matrix for row in 0..self.nrows { for idx in self.row_ptr[row]..self.row_ptr[row + 1] { let col = self.col_idx[idx]; let value = self.values[idx]; result.add_entry(row, col, value).unwrap(); } } } else { // Use assembly_map for unfinalized matrix for ((row, col), &idx) in &self.assembly_map { let value = self.values[idx]; result.add_entry(*row, *col, value).unwrap(); } } // Add entries from right matrix if rhs.finalized { // Use CSR format for finalized matrix for row in 0..rhs.nrows { for idx in rhs.row_ptr[row]..rhs.row_ptr[row + 1] { let col = rhs.col_idx[idx]; let value = rhs.values[idx]; result.add_entry(row, col, value).unwrap(); } } } else { // Use assembly_map for unfinalized matrix for ((row, col), &idx) in &rhs.assembly_map { let value = rhs.values[idx]; result.add_entry(*row, *col, value).unwrap(); } } // If both originals were finalized, finalize the result if self.finalized && rhs.finalized { result.finalize().unwrap(); } result } } /// GPU-accelerated sparse matrix operations. #[cfg(feature = "cuda")] use cudarc::driver::safe::{CudaContext as CudaDevice, CudaStream}; pub struct GpuSparseMatrix { /// CPU sparse matrix (for initial data only) cpu_matrix: SparseMatrix, /// CUDA device - REQUIRED #[cfg(feature = "cuda")] device: std::sync::Arc, /// CUDA stream for async operations #[cfg(feature = "cuda")] stream: std::sync::Arc, } impl GpuSparseMatrix { /// Create from CPU sparse matrix. /// PANICS if GPU is not available (GPU-only requirement) pub fn from_cpu_matrix(cpu_matrix: SparseMatrix) -> FeaResult { #[cfg(feature = "cuda")] { let device = CudaDevice::new(0).expect("GPU is REQUIRED for GpuSparseMatrix. No GPU found."); let stream = device.default_stream(); Ok(Self { cpu_matrix, device, stream, }) } #[cfg(not(feature = "cuda"))] { panic!("GPU is REQUIRED. Compile with 'cuda' feature enabled.") } } /// Transfer matrix to GPU. pub fn to_gpu(&mut self) -> FeaResult<()> { if !self.cpu_matrix.finalized { return Err(AssemblyError::SparseAssemblyFailed { reason: "Matrix must be finalized before GPU transfer".to_string(), } .into()); } #[cfg(feature = "cuda")] { // GPU is always available here - we panic in from_cpu_matrix if not // Allocate device memory for CSR format let nnz = self.cpu_matrix.values.len(); let nrows = self.cpu_matrix.nrows; // TODO: Implement actual GPU transfer using cuSPARSE // This will use the device and stream fields tracing::info!( "Matrix transfer to GPU pending implementation (nnz: {}, rows: {})", nnz, nrows ); } Ok(()) } /// GPU matrix-vector multiplication. pub fn gpu_multiply_vector(&self, _x: &DVector) -> FeaResult> { #[cfg(feature = "cuda")] { // Perform GPU SpMV using cuSPARSE // This would use cusparseSpMV() with optimal buffer allocation // and handle device-to-host memory transfers efficiently // Steps for full implementation: // 1. Allocate device vectors for input/output // 2. Copy input vector to device // 3. Execute cusparseSpMV operation // 4. Copy result back to host // TODO: Implement actual GPU SpMV // For now, return error to maintain GPU-only requirement Err(AssemblyError::SparseAssemblyFailed { reason: "GPU SpMV implementation pending".to_string(), } .into()) } #[cfg(not(feature = "cuda"))] { panic!("GPU is REQUIRED for matrix operations.") } } /// Get CPU matrix reference. pub fn cpu_matrix(&self) -> &SparseMatrix { &self.cpu_matrix } } #[cfg(test)] mod tests { use super::*; #[test] fn test_sparse_matrix_creation() { let matrix = SparseMatrix::new(3, 3); assert_eq!(matrix.nrows(), 3); assert_eq!(matrix.ncols(), 3); assert_eq!(matrix.nnz(), 0); } #[test] fn test_sparse_matrix_assembly() { let mut matrix = SparseMatrix::new(3, 3); matrix.add_entry(0, 0, 1.0).unwrap(); matrix.add_entry(1, 1, 2.0).unwrap(); matrix.add_entry(2, 2, 3.0).unwrap(); matrix.add_entry(0, 1, 0.5).unwrap(); assert_eq!(matrix.nnz(), 4); // Add to existing entry matrix.add_entry(0, 0, 1.0).unwrap(); assert_eq!(matrix.get_entry(0, 0), 2.0); } #[test] fn test_sparse_matrix_finalization() { let mut matrix = SparseMatrix::new(3, 3); matrix.add_entry(0, 0, 1.0).unwrap(); matrix.add_entry(1, 1, 2.0).unwrap(); matrix.add_entry(2, 2, 3.0).unwrap(); matrix.finalize().unwrap(); assert_eq!(matrix.get_entry(0, 0), 1.0); assert_eq!(matrix.get_entry(1, 1), 2.0); assert_eq!(matrix.get_entry(2, 2), 3.0); } #[test] fn test_matrix_vector_multiplication() { let mut matrix = SparseMatrix::new(3, 3); matrix.add_entry(0, 0, 2.0).unwrap(); matrix.add_entry(1, 1, 3.0).unwrap(); matrix.add_entry(2, 2, 4.0).unwrap(); matrix.finalize().unwrap(); let x = DVector::from_vec(vec![1.0, 2.0, 3.0]); let y = matrix.multiply_vector(&x).unwrap(); assert_eq!(y[0], 2.0); assert_eq!(y[1], 6.0); assert_eq!(y[2], 12.0); } #[test] fn test_submatrix_extraction() { let mut matrix = SparseMatrix::new(4, 4); // Create a 4x4 diagonal matrix for i in 0..4 { matrix.add_entry(i, i, (i + 1) as f64).unwrap(); } matrix.finalize().unwrap(); // Extract 2x2 submatrix let row_indices = vec![0, 2]; let col_indices = vec![0, 2]; let sub_matrix = matrix .extract_submatrix(&row_indices, &col_indices) .unwrap(); assert_eq!(sub_matrix.nrows(), 2); assert_eq!(sub_matrix.ncols(), 2); assert_eq!(sub_matrix.get_entry(0, 0), 1.0); assert_eq!(sub_matrix.get_entry(1, 1), 3.0); } #[test] fn test_dirichlet_boundary_condition() { let mut matrix = SparseMatrix::new(3, 3); // Fill matrix for i in 0..3 { for j in 0..3 { matrix.add_entry(i, j, ((i + 1) * (j + 1)) as f64).unwrap(); } } // Apply Dirichlet BC at DOF 1 matrix.set_dirichlet(1, 5.0).unwrap(); // Check that row 1 and column 1 are zeroed except diagonal assert_eq!(matrix.get_entry(1, 0), 0.0); assert_eq!(matrix.get_entry(1, 1), 1.0); assert_eq!(matrix.get_entry(1, 2), 0.0); assert_eq!(matrix.get_entry(0, 1), 0.0); assert_eq!(matrix.get_entry(2, 1), 0.0); } #[test] fn test_dense_conversion() { let mut matrix = SparseMatrix::new(2, 2); matrix.add_entry(0, 0, 1.0).unwrap(); matrix.add_entry(0, 1, 2.0).unwrap(); matrix.add_entry(1, 0, 3.0).unwrap(); matrix.add_entry(1, 1, 4.0).unwrap(); let dense = matrix.to_dense(); assert_eq!(dense[(0, 0)], 1.0); assert_eq!(dense[(0, 1)], 2.0); assert_eq!(dense[(1, 0)], 3.0); assert_eq!(dense[(1, 1)], 4.0); } #[test] fn test_triplet_construction() { let triplets = vec![(0, 0, 1.0), (1, 1, 2.0), (0, 1, 0.5)]; let matrix = SparseMatrix::from_triplets(2, 2, &triplets).unwrap(); assert_eq!(matrix.get_entry(0, 0), 1.0); assert_eq!(matrix.get_entry(1, 1), 2.0); assert_eq!(matrix.get_entry(0, 1), 0.5); } #[test] fn test_matrix_properties() { let mut matrix = SparseMatrix::new(3, 3); matrix.add_entry(0, 0, 1.0).unwrap(); matrix.add_entry(1, 1, 4.0).unwrap(); matrix.add_entry(2, 2, 9.0).unwrap(); matrix.finalize().unwrap(); assert_eq!(matrix.nnz(), 3); assert!((matrix.density() - 3.0 / 9.0).abs() < 1e-12); assert!((matrix.frobenius_norm() - (1.0 + 16.0 + 81.0_f64).sqrt()).abs() < 1e-12); } } // Implement scalar multiplication for SparseMatrix impl std::ops::Mul for SparseMatrix { type Output = Self; fn mul(self, scalar: f64) -> Self::Output { let mut result = Self::new(self.nrows, self.ncols); if self.finalized { // For finalized matrices, use CSR format data for row in 0..self.nrows { for idx in self.row_ptr[row]..self.row_ptr[row + 1] { let col = self.col_idx[idx]; let value = self.values[idx] * scalar; result.add_entry(row, col, value).unwrap(); } } result.finalize().unwrap(); } else { // For unfinalized matrices, use assembly_map for ((row, col), &idx) in &self.assembly_map { let value = self.values[idx] * scalar; result.add_entry(*row, *col, value).unwrap(); } } result } } // Also implement for references to avoid unnecessary cloning impl std::ops::Mul for &SparseMatrix { type Output = SparseMatrix; fn mul(self, scalar: f64) -> Self::Output { let mut result = SparseMatrix::new(self.nrows, self.ncols); if self.finalized { // For finalized matrices, use CSR format data for row in 0..self.nrows { for idx in self.row_ptr[row]..self.row_ptr[row + 1] { let col = self.col_idx[idx]; let value = self.values[idx] * scalar; result.add_entry(row, col, value).unwrap(); } } result.finalize().unwrap(); } else { // For unfinalized matrices, use assembly_map for ((row, col), &idx) in &self.assembly_map { let value = self.values[idx] * scalar; result.add_entry(*row, *col, value).unwrap(); } } result } } // Implement matrix addition for SparseMatrix impl std::ops::Add<&Self> for SparseMatrix { type Output = Self; fn add(self, rhs: &Self) -> Self::Output { assert!(!(self.nrows != rhs.nrows || self.ncols != rhs.ncols), "Matrix dimensions must match for addition"); let mut result = Self::new(self.nrows, self.ncols); // Add entries from left matrix if self.finalized { // Use CSR format for finalized matrix for row in 0..self.nrows { for idx in self.row_ptr[row]..self.row_ptr[row + 1] { let col = self.col_idx[idx]; let value = self.values[idx]; result.add_entry(row, col, value).unwrap(); } } } else { // Use assembly_map for unfinalized matrix for ((row, col), &idx) in &self.assembly_map { let value = self.values[idx]; result.add_entry(*row, *col, value).unwrap(); } } // Add entries from right matrix if rhs.finalized { // Use CSR format for finalized matrix for row in 0..rhs.nrows { for idx in rhs.row_ptr[row]..rhs.row_ptr[row + 1] { let col = rhs.col_idx[idx]; let value = rhs.values[idx]; result.add_entry(row, col, value).unwrap(); } } } else { // Use assembly_map for unfinalized matrix for ((row, col), &idx) in &rhs.assembly_map { let value = rhs.values[idx]; result.add_entry(*row, *col, value).unwrap(); } } // If both originals were finalized, finalize the result if self.finalized && rhs.finalized { result.finalize().unwrap(); } result } }