Skip to main content

diffsol_la/matrix/
mod.rs

1use std::fmt::Debug;
2use std::ops::{Add, AddAssign, Mul, Sub, SubAssign};
3
4use crate::error::LaError;
5use crate::scalar::Scale;
6use crate::{Context, IndexType, Scalar, Vector, VectorIndex, VectorViewMut};
7
8use extract_block::combine;
9use num_traits::{One, Zero};
10use sparsity::{Dense, MatrixSparsity, MatrixSparsityRef};
11
12#[cfg(feature = "cuda")]
13#[allow(deprecated)]
14pub mod cuda;
15
16#[cfg(feature = "cuda-oxide")]
17pub mod cuda_oxide;
18
19#[cfg(feature = "nalgebra")]
20pub mod dense_nalgebra_serial;
21
22#[cfg(feature = "faer")]
23pub mod dense_faer_serial;
24
25#[cfg(feature = "faer")]
26pub mod sparse_faer;
27
28pub mod default_solver;
29pub mod extract_block;
30pub mod sparsity;
31
32/// Common interface for matrix types, providing access to scalar type, context, and dimensions.
33pub trait MatrixCommon: Sized + Debug {
34    type V: Vector<T = Self::T, C = Self::C, Index: VectorIndex<C = Self::C>>;
35    type T: Scalar;
36    type C: Context;
37    type Inner;
38
39    /// Get the number of rows in this matrix.
40    fn nrows(&self) -> IndexType;
41    /// Get the number of columns in this matrix.
42    fn ncols(&self) -> IndexType;
43    /// Get a reference to the inner representation of the matrix.
44    fn inner(&self) -> &Self::Inner;
45}
46
47impl<M> MatrixCommon for &M
48where
49    M: MatrixCommon,
50{
51    type T = M::T;
52    type V = M::V;
53    type C = M::C;
54    type Inner = M::Inner;
55
56    fn nrows(&self) -> IndexType {
57        M::nrows(*self)
58    }
59    fn ncols(&self) -> IndexType {
60        M::ncols(*self)
61    }
62    fn inner(&self) -> &Self::Inner {
63        M::inner(*self)
64    }
65}
66
67impl<M> MatrixCommon for &mut M
68where
69    M: MatrixCommon,
70{
71    type T = M::T;
72    type V = M::V;
73    type C = M::C;
74    type Inner = M::Inner;
75
76    fn ncols(&self) -> IndexType {
77        M::ncols(*self)
78    }
79    fn nrows(&self) -> IndexType {
80        M::nrows(*self)
81    }
82    fn inner(&self) -> &Self::Inner {
83        M::inner(*self)
84    }
85}
86
87/// Operations on matrices by value (addition and subtraction).
88///
89/// This trait defines matrix addition and subtraction when both operands are owned or references.
90pub trait MatrixOpsByValue<Rhs = Self, Output = Self>:
91    MatrixCommon + Add<Rhs, Output = Output> + Sub<Rhs, Output = Output>
92{
93}
94
95impl<M, Rhs, Output> MatrixOpsByValue<Rhs, Output> for M where
96    M: MatrixCommon + Add<Rhs, Output = Output> + Sub<Rhs, Output = Output>
97{
98}
99
100/// In-place operations on matrices (addition and subtraction).
101///
102/// This trait defines in-place matrix addition and subtraction (self += rhs, self -= rhs).
103pub trait MatrixMutOpsByValue<Rhs = Self>: MatrixCommon + AddAssign<Rhs> + SubAssign<Rhs> {}
104
105impl<M, Rhs> MatrixMutOpsByValue<Rhs> for M where M: MatrixCommon + AddAssign<Rhs> + SubAssign<Rhs> {}
106
107/// A trait allowing for references to implement matrix operations
108pub trait MatrixRef<M: MatrixCommon>: Mul<Scale<M::T>, Output = M> {}
109impl<RefT, M: MatrixCommon> MatrixRef<M> for RefT where RefT: Mul<Scale<M::T>, Output = M> {}
110
111/// A base matrix trait supporting both sparse and dense matrices.
112///
113/// This trait provides a complete interface for matrix operations including:
114/// - Matrix creation and memory management
115/// - Matrix-vector and matrix-matrix multiplication
116/// - Element access and modification
117/// - Sparsity information and handling
118/// - Matrix decomposition and combination operations
119/// - Triplet-based construction for sparse matrices
120///
121/// Implementing matrices can be dense or sparse, and may be hosted on CPU or GPU.
122/// Users typically do not need to implement this trait; use provided implementations.
123pub trait Matrix:
124    MatrixCommon + Mul<Scale<Self::T>, Output = Self> + Clone + Send + 'static
125{
126    type Sparsity: MatrixSparsity<Self>;
127    type SparsityRef<'a>: MatrixSparsityRef<'a, Self>
128    where
129        Self: 'a;
130
131    /// Return sparsity information, or `None` if the matrix is dense.
132    fn sparsity(&self) -> Option<Self::SparsityRef<'_>>;
133
134    /// Get the context associated with this matrix (for device placement, memory management, etc.).
135    fn context(&self) -> &Self::C;
136
137    /// Get a mutable reference to the inner representation of the matrix.
138    fn inner_mut(&mut self) -> &mut Self::Inner;
139
140    /// Returns true if this matrix is stored in a sparse format
141    fn is_sparse() -> bool {
142        Self::zeros(1, 1, Default::default()).sparsity().is_some()
143    }
144
145    /// Partition the diagonal indices into two groups: those with zero diagonal elements and those with non-zero diagonal elements.
146    ///
147    /// This is useful for identifying algebraic constraints, which typically have zero diagonal elements in the mass matrix.
148    /// Returns a tuple of (zero_diagonal_indices, non_zero_diagonal_indices).
149    /// For batched matrices the partition is taken from batch 0, so all batches are assumed
150    /// to share the same structure.
151    fn partition_indices_by_zero_diagonal(
152        &self,
153    ) -> (<Self::V as Vector>::Index, <Self::V as Vector>::Index);
154
155    /// Perform a matrix-vector multiplication: y = alpha * self * x + beta * y
156    fn gemv(&self, alpha: Self::T, x: &Self::V, beta: Self::T, y: &mut Self::V);
157
158    /// Copy the contents of `other` into this matrix.
159    fn copy_from(&mut self, other: &Self);
160
161    /// Create a new matrix of shape `nrows` x `ncols` filled with zeros.
162    fn zeros(nrows: IndexType, ncols: IndexType, ctx: Self::C) -> Self;
163
164    /// Create a new matrix from a sparsity pattern. Non-zero elements are not initialized.
165    fn new_from_sparsity(
166        nrows: IndexType,
167        ncols: IndexType,
168        sparsity: Option<Self::Sparsity>,
169        ctx: Self::C,
170    ) -> Self;
171
172    /// Create a new diagonal matrix from a vector holding the diagonal elements.
173    fn from_diagonal(v: &Self::V) -> Self;
174
175    /// Set the values of column `j` to be equal to the values in `v`.
176    ///
177    /// For sparse matrices, only the existing non-zero elements are updated.
178    fn set_column(&mut self, j: IndexType, v: &Self::V);
179
180    /// Add a column of this matrix to a vector: v += self[:, j]
181    fn add_column_to_vector(&self, j: IndexType, v: &mut Self::V);
182
183    /// Add every column of this matrix to the matching batch lane of `v`: lane `b * ncols() + j`
184    /// receives column `j` of this matrix's batch `b`.
185    fn add_columns_to_batched_vector(&self, v: &mut Self::V);
186
187    /// Assign the values in the `data` vector to this matrix at the indices in `dst_indices`
188    /// from the indices in `src_indices`.
189    ///
190    /// For dense matrices, the index is the data index in column-major order.
191    /// For sparse matrices, the index is the index into the data array.
192    fn set_data_with_indices(
193        &mut self,
194        dst_indices: &<Self::V as Vector>::Index,
195        src_indices: &<Self::V as Vector>::Index,
196        data: &Self::V,
197    );
198
199    /// Gather values from another matrix at specified indices into this matrix.
200    ///
201    /// For sparse matrices: the index `idx_i` in `indices` is an index into the data array for `other`,
202    /// and is copied to the index `idx_i` in the data array for this matrix.
203    /// For dense matrices: the index is the data index in column-major order.
204    fn gather(&mut self, other: &Self, indices: &<Self::V as Vector>::Index);
205
206    /// Split this matrix into four submatrices based on algebraic constraint indices.
207    ///
208    /// Partitions the matrix into blocks:
209    /// ```text
210    /// M = [UL, UR]
211    ///     [LL, LR]
212    /// ```
213    /// where:
214    /// - UL contains rows and columns NOT in `algebraic_indices`
215    /// - UR contains rows NOT in `algebraic_indices` and columns in `algebraic_indices`
216    /// - LL contains rows in `algebraic_indices` and columns NOT in `algebraic_indices`
217    /// - LR contains rows and columns in `algebraic_indices`
218    ///
219    /// Returns an array of tuples, where each tuple contains a submatrix and the indices that were used to create it.
220    /// These indices can be used with `gather()` to update the submatrix.
221    fn split(
222        &self,
223        algebraic_indices: &<Self::V as Vector>::Index,
224    ) -> [(Self, <Self::V as Vector>::Index); 4] {
225        match self.sparsity() {
226            Some(sp) => sp.split(algebraic_indices).map(|(sp, src_indices)| {
227                let mut m = Self::new_from_sparsity(
228                    sp.nrows(),
229                    sp.ncols(),
230                    Some(sp),
231                    self.context().clone(),
232                );
233                m.gather(self, &src_indices);
234                (m, src_indices)
235            }),
236            None => Dense::<Self>::new(self.nrows(), self.ncols())
237                .split(algebraic_indices)
238                .map(|(sp, src_indices)| {
239                    let mut m = Self::new_from_sparsity(
240                        sp.nrows(),
241                        sp.ncols(),
242                        None,
243                        self.context().clone(),
244                    );
245                    m.gather(self, &src_indices);
246                    (m, src_indices)
247                }),
248        }
249    }
250
251    /// Combine four submatrices back into a single matrix based on algebraic constraint indices.
252    ///
253    /// Inverse operation of `split()`. Takes submatrices `ul`, `ur`, `ll`, `lr` and combines them
254    /// back into the original matrix structure.
255    fn combine(
256        ul: &Self,
257        ur: &Self,
258        ll: &Self,
259        lr: &Self,
260        algebraic_indices: &<Self::V as Vector>::Index,
261    ) -> Self {
262        combine(ul, ur, ll, lr, algebraic_indices)
263    }
264
265    /// Perform the assignment: self = x + beta * y where x and y are matrices and beta is a scalar.
266    ///
267    /// Note: Panics if the sparsity patterns of self, x, and y do not match.
268    /// The sparsity of self must be the union of the sparsity of x and y.
269    fn scale_add_and_assign(&mut self, x: &Self, beta: Self::T, y: &Self);
270
271    /// Iterate over structural positions and values of the matrix.
272    ///
273    /// Returns a tuple:
274    /// - First iterator: `(row, col)` pairs for each non-zero element (length `nnz`)
275    /// - Second iterator: values (length `nnz * nbatch`), laid out batch-contiguously:
276    ///   `[batch0_val0..batch0_valN, batch1_val0..batch1_valN, ...]`
277    fn triplet_iter(
278        &self,
279    ) -> (
280        impl Iterator<Item = (IndexType, IndexType)> + '_,
281        impl Iterator<Item = Self::T> + '_,
282    );
283
284    /// Create a new matrix from structural indices and values.
285    ///
286    /// - `indices`: `(row, col)` pairs for each non-zero element (length `nnz`)
287    /// - `values`: values laid out batch-contiguously (length `nnz * ctx.nbatch()`):
288    ///   `[batch0_val0..batch0_valN, batch1_val0..batch1_valN, ...]`
289    fn try_from_triplets(
290        nrows: IndexType,
291        ncols: IndexType,
292        indices: Vec<(IndexType, IndexType)>,
293        values: Vec<Self::T>,
294        ctx: Self::C,
295    ) -> Result<Self, LaError>;
296}
297
298/// Largest column range the small-coefficient kernels accept — [`DenseMatrix::mul_cols_by`] and
299/// [`DenseMatrix::gemv_cols`].
300///
301/// Both take their coefficients from the host: the serial backends stage them in a fixed stack
302/// buffer and the CUDA backend passes them to the kernel by value, so neither allocates — which
303/// needs a bound. Two things drive it: the BDF difference table is at most `MAX_ORDER + 1 = 6`
304/// columns wide, and the widest built-in Runge-Kutta tableau (`tsit45`) has 7 stages. 8 covers
305/// both.
306///
307/// Must match `MAX_SMALL_COLS` in `src/cuda_kernels/cuda_kernels_common.h`, which sizes the
308/// kernels' by-value coefficient arrays; nothing checks that across the language boundary.
309pub const MAX_SMALL_COLS: usize = 8;
310
311/// A dense column-major matrix with efficient column access operations.
312///
313/// This trait represents matrices stored in column-major order, where accessing matrix columns
314/// is efficient. It supports:
315/// - Column operations (access, modification, and the fused column kernels below)
316/// - Element access and modification
317/// - Matrix resizing
318///
319/// The column-major layout makes operations on individual or ranges of columns very efficient.
320pub trait DenseMatrix:
321    Matrix + for<'b> MatrixOpsByValue<&'b Self, Self> + for<'b> MatrixMutOpsByValue<&'b Self>
322{
323    /// Update a table of backward differences held in columns `0..=order+2` of
324    /// this matrix, given the newly computed highest-order correction `d`.
325    ///
326    /// Equivalent to:
327    /// ```text
328    /// self[:, order+2] = d - self[:, order+1]
329    /// for i in (0..=order+1).rev() { self[:, i] += self[:, i+1] }
330    /// ```
331    /// where each addition uses the just-updated value of column `i+1` (a
332    /// right-to-left cumulative scan).
333    fn update_backward_diff(&mut self, order: IndexType, d: &Self::V);
334
335    /// Transpose of [Self::update_backward_diff] with respect to `(self, d)`.
336    ///
337    /// Given cotangents `C` of the updated columns `0..=order+2`, overwrite them with the
338    /// cotangents of the columns before the update, and write the cotangent of `d` into `d_bar`.
339    /// Equivalent to:
340    /// ```text
341    /// d_bar = C[:, 0] + C[:, 1] + ... + C[:, order+2]
342    /// for i in 1..=order { C[:, i] += C[:, i-1] }   // left-to-right cumulative scan
343    /// C[:, order+1] = -C[:, order+2]
344    /// C[:, order+2] = 0
345    /// ```
346    ///
347    /// The default implementation is one [Self::gemv_cols] and one [Self::mul_cols_by], so it
348    /// needs no backend-specific code. Panics if `order + 3` exceeds [`MAX_SMALL_COLS`].
349    fn update_backward_diff_transpose(&mut self, order: IndexType, d_bar: &mut Self::V) {
350        let n = order + 3;
351        assert!(n <= MAX_SMALL_COLS, "order out of bounds");
352        let ones = [Self::T::one(); MAX_SMALL_COLS];
353        self.gemv_cols(0, n, Self::T::one(), &ones, Self::T::zero(), d_bar);
354
355        // the remaining updates are C <- C * t, with t column-major and n x n
356        let mut t = [Self::T::zero(); MAX_SMALL_COLS * MAX_SMALL_COLS];
357        for j in 0..=order {
358            for i in 0..=j {
359                t[j * n + i] = Self::T::one();
360            }
361        }
362        t[(order + 1) * n + order + 2] = -Self::T::one();
363        self.mul_cols_by(n, &t[..n * n]);
364    }
365
366    /// Set every entry of every batch to `value`, in place.
367    ///
368    /// The default copies one column of `value`s into each column, so it needs no
369    /// backend-specific code; the host backends override it with their own fill.
370    fn fill(&mut self, value: Self::T) {
371        let col = Self::V::from_element(self.nrows(), value, self.context().clone());
372        for j in 0..self.ncols() {
373            self.column_mut(j).copy_from(&col);
374        }
375    }
376
377    /// Right-multiply a range of columns by the transpose of a small matrix, in place:
378    ///
379    /// ```text
380    /// self[:, 0..ncols] = self[:, 0..ncols] * rhsᵀ
381    /// ```
382    ///
383    /// `rhs` is laid out as in [Self::mul_cols_by]. The default implementation transposes it on
384    /// the host and calls [Self::mul_cols_by]. Panics if `ncols` exceeds [`MAX_SMALL_COLS`].
385    fn mul_cols_by_transpose(&mut self, ncols: IndexType, rhs: &[Self::T]) {
386        assert!(ncols <= MAX_SMALL_COLS, "ncols out of bounds");
387        assert_eq!(rhs.len(), ncols * ncols, "rhs must be ncols x ncols");
388        let mut rhs_t = [Self::T::zero(); MAX_SMALL_COLS * MAX_SMALL_COLS];
389        for j in 0..ncols {
390            for i in 0..ncols {
391                rhs_t[j * ncols + i] = rhs[i * ncols + j];
392            }
393        }
394        self.mul_cols_by(ncols, &rhs_t[..ncols * ncols]);
395    }
396
397    /// Right-multiply a range of columns by a small matrix, in place:
398    ///
399    /// ```text
400    /// self[:, 0..ncols] = self[:, 0..ncols] * rhs
401    /// ```
402    ///
403    /// `rhs` is an `ncols x ncols` block held column-major in a host slice of exactly
404    /// `ncols * ncols` values — it is small (the BDF step-size update uses at most `6 x 6`).
405    ///
406    /// Panics if `ncols` exceeds [`MAX_SMALL_COLS`], since the serial backends stage the old
407    /// column values in a fixed-size buffer rather than allocating.
408    fn mul_cols_by(&mut self, ncols: IndexType, rhs: &[Self::T]);
409
410    /// Matrix-vector multiply over a range of columns, with host-side coefficients:
411    ///
412    /// ```text
413    /// y = alpha * self[:, start..end] * x + beta * y
414    /// ```
415    ///
416    /// `x` is a host slice, not a vector, because `x.len()` is the *width of the column range*
417    /// rather than a dimension of the problem: every caller in the solvers passes a short
418    /// coefficient list computed from host scalars — a Runge-Kutta tableau row, the `b`/`d`
419    /// vectors, interpolation weights, BDF's psi weights. Keeping them on the host is what lets
420    /// those callers build them in a stack array instead of allocating a vector per call.
421    ///
422    /// Use [`Matrix::gemv`] instead when `x` is a genuine operand of length `ncols` — a
423    /// Jacobian- or mass-times-vector product.
424    ///
425    /// Only the leading `end - start` columns take part; the rest of the matrix is not read.
426    /// `x.len()` must be at least `end - start`; only its leading `end - start` entries are
427    /// read, as with a BLAS `gemv` given a longer vector and `incx = 1`. `beta == 0` means `y`
428    /// is not read, so it may hold uninitialised values. An empty range contributes nothing,
429    /// leaving `y = beta * y`.
430    ///
431    /// The coefficients are shared by every batch — unlike [`Matrix::gemv`], this cannot apply
432    /// a different `x` per batch. Panics if `end - start` exceeds [`MAX_SMALL_COLS`].
433    fn gemv_cols(
434        &self,
435        start: IndexType,
436        end: IndexType,
437        alpha: Self::T,
438        x: &[Self::T],
439        beta: Self::T,
440        y: &mut Self::V,
441    );
442
443    /// Rank-one update over a range of columns, with host-side coefficients: BLAS `ger`
444    /// ("general rank-1") restricted to `start..end`, and the transpose of [`Self::gemv_cols`].
445    ///
446    /// ```text
447    /// self[:, start..end] += alpha * y * xᵀ,   i.e.   self[:, j] += alpha * x[j - start] * y
448    /// ```
449    ///
450    /// Where [`Self::gemv_cols`] sums the columns into a vector, this adds a vector into each
451    /// column: if `y = alpha * A[:, start..end] * x`, the cotangent of `A` gains
452    /// `alpha * y_bar * xᵀ`, which is `A_bar.ger_cols(start, end, alpha, x, &y_bar)`.
453    ///
454    /// As for [`Self::gemv_cols`], `x` is a host slice of at least `end - start` coefficients,
455    /// shared by every batch, and only the columns in the range are touched. `y` has this
456    /// matrix's rows and lanes.
457    fn ger_cols(
458        &mut self,
459        start: IndexType,
460        end: IndexType,
461        alpha: Self::T,
462        x: &[Self::T],
463        y: &Self::V,
464    ) {
465        assert!(
466            x.len() >= end.saturating_sub(start),
467            "ger_cols needs a coefficient per column in the range"
468        );
469        for j in start..end {
470            self.column_mut(j)
471                .axpy(alpha * x[j - start], y, Self::T::one());
472        }
473    }
474
475    /// Get an immutable vector view of column `i`.
476    fn column(&self, i: IndexType) -> <Self::V as Vector>::View<'_>;
477
478    /// Get a mutable vector view of column `i`.
479    fn column_mut(&mut self, i: IndexType) -> <Self::V as Vector>::ViewMut<'_>;
480
481    /// Set the value at the given row and column indices.
482    ///
483    /// Scalar access is single-system, as it is for [`Vector::set_index`]: panics unless
484    /// `nbatch == 1`. So does `IndexMut`.
485    fn set_index(&mut self, i: IndexType, j: IndexType, value: Self::T);
486
487    /// Set the value at the given row and column indices, in a single batch.
488    ///
489    /// The batch-aware counterpart to [`Self::set_index`]: `(i, j)` alone is ambiguous once
490    /// `nbatch > 1`, so the batch is named explicitly. Mirrors [`Vector::get_batch_mut`].
491    fn set_index_batch(&mut self, batch: IndexType, i: IndexType, j: IndexType, value: Self::T);
492
493    /// Get the value at the given row and column indices.
494    ///
495    /// Scalar access is single-system, as it is for [`Vector::get_index`]: panics unless
496    /// `nbatch == 1`. So does `Index`.
497    fn get_index(&self, i: IndexType, j: IndexType) -> Self::T;
498
499    /// Resize the number of columns in the matrix, preserving existing data.
500    ///
501    /// New elements (if added) are uninitialized. If the number of columns decreases, trailing columns are discarded.
502    fn resize_cols(&mut self, ncols: IndexType);
503
504    /// Create a new matrix from a vector of values in column-major order.
505    ///
506    /// The values are assumed to be stored in column-major order (first column, then second column, etc.).
507    fn from_vec(nrows: IndexType, ncols: IndexType, data: Vec<Self::T>, ctx: Self::C) -> Self;
508}
509
510#[cfg(test)]
511#[allow(clippy::items_after_test_module)]
512pub(crate) mod tests {
513    use std::ops::{Add, Mul, Sub};
514
515    use super::{
516        DenseMatrix, Matrix, MatrixCommon, MatrixSparsity, MatrixSparsityRef, MAX_SMALL_COLS,
517    };
518    use crate::scalar::Scale;
519    use crate::{scalar::IndexType, Context, Vector, VectorIndex, VectorView, VectorViewMut};
520    use num_traits::{FromPrimitive, One, Zero};
521
522    fn f<M: Matrix>(x: f64) -> M::T {
523        M::T::from_f64(x).unwrap()
524    }
525
526    /// Read one element of one batch. `get_index` is single-system, so a batched matrix is read a
527    /// column at a time.
528    fn at<M: DenseMatrix>(a: &M, batch: usize, i: IndexType, j: IndexType) -> M::T {
529        a.column(j).into_owned().get_batch(batch).get_index(i)
530    }
531
532    pub(crate) fn triplet_values<M: Matrix>(m: &M) -> Vec<M::T> {
533        let (_, vals) = m.triplet_iter();
534        vals.collect()
535    }
536
537    fn triplet_indices<M: Matrix>(m: &M) -> Vec<(IndexType, IndexType)> {
538        let (idx, _) = m.triplet_iter();
539        idx.collect()
540    }
541
542    pub fn test_partition_indices_by_zero_diagonal<M: Matrix>() {
543        let indices = vec![(0, 0), (1, 1), (3, 3)];
544        let values = vec![M::T::one(), M::T::from_f64(2.0).unwrap(), M::T::one()];
545        let m = M::try_from_triplets(4, 4, indices, values, Default::default()).unwrap();
546        let (zero_diagonal_indices, non_zero_diagonal_indices) =
547            m.partition_indices_by_zero_diagonal();
548        assert_eq!(zero_diagonal_indices.clone_as_vec(), vec![2]);
549        assert_eq!(non_zero_diagonal_indices.clone_as_vec(), vec![0, 1, 3]);
550
551        let indices = vec![(0, 0), (1, 1), (2, 2), (3, 3)];
552        let values = vec![
553            M::T::one(),
554            M::T::from_f64(2.0).unwrap(),
555            M::T::zero(),
556            M::T::one(),
557        ];
558        let m = M::try_from_triplets(4, 4, indices, values, Default::default()).unwrap();
559        let (zero_diagonal_indices, non_zero_diagonal_indices) =
560            m.partition_indices_by_zero_diagonal();
561        assert_eq!(zero_diagonal_indices.clone_as_vec(), vec![2]);
562        assert_eq!(non_zero_diagonal_indices.clone_as_vec(), vec![0, 1, 3]);
563
564        let indices = vec![(0, 0), (1, 1), (2, 2), (3, 3)];
565        let values = vec![
566            M::T::one(),
567            M::T::from_f64(2.0).unwrap(),
568            M::T::from_f64(3.0).unwrap(),
569            M::T::one(),
570        ];
571        let m = M::try_from_triplets(4, 4, indices, values, Default::default()).unwrap();
572        let (zero_diagonal_indices, non_zero_diagonal_indices) =
573            m.partition_indices_by_zero_diagonal();
574        assert_eq!(
575            zero_diagonal_indices.clone_as_vec(),
576            Vec::<IndexType>::new()
577        );
578        assert_eq!(non_zero_diagonal_indices.clone_as_vec(), vec![0, 1, 2, 3]);
579
580        // column 2 has a single stored entry below the diagonal (row 3) and none at (2, 2):
581        // the sparse backend's early-exit ("row index has passed the diagonal") branch fires.
582        let indices = vec![(3, 2)];
583        let values = vec![M::T::one()];
584        let m = M::try_from_triplets(4, 4, indices, values, Default::default()).unwrap();
585        let (zero_diagonal_indices, non_zero_diagonal_indices) =
586            m.partition_indices_by_zero_diagonal();
587        assert_eq!(zero_diagonal_indices.clone_as_vec(), vec![0, 1, 2, 3]);
588        assert_eq!(
589            non_zero_diagonal_indices.clone_as_vec(),
590            Vec::<IndexType>::new()
591        );
592    }
593
594    // --- Matrix-generic tests (work with both dense and sparse) ---
595
596    pub fn test_zeros<M: Matrix>() {
597        let a = M::zeros(2, 3, Default::default());
598        assert_eq!(a.nrows(), 2);
599        assert_eq!(a.ncols(), 3);
600        let vals = triplet_values(&a);
601        assert!(vals.is_empty() || vals.iter().all(|v| v.is_zero()));
602        assert_eq!(M::is_sparse(), a.sparsity().is_some());
603    }
604
605    /// `M::Sparsity`/`M::SparsityRef<'_>` trait methods (`nrows`/`ncols`/`is_sparse`/`indices`/
606    /// `union`/`new_diagonal`/`get_index`/`as_ref`/`to_owned`/`split`): only `is_sparse` was ever
607    /// called generically before, via `test_zeros`, so none of the sparsity pattern machinery
608    /// itself (used by both the `Dense` stand-in for dense backends and the real sparse pattern)
609    /// was exercised.
610    pub fn test_sparsity<M: Matrix>() {
611        let a = M::Sparsity::try_from_indices(2, 2, vec![(0, 0), (1, 1)]).unwrap();
612        assert_eq!(MatrixSparsity::<M>::nrows(&a), 2);
613        assert_eq!(MatrixSparsity::<M>::ncols(&a), 2);
614        assert_eq!(
615            M::is_sparse(),
616            <M::Sparsity as MatrixSparsity<M>>::is_sparse()
617        );
618        let _ = a.indices();
619        let diag = M::Sparsity::new_diagonal(3);
620        assert_eq!(MatrixSparsity::<M>::nrows(&diag), 3);
621        assert_eq!(MatrixSparsity::<M>::ncols(&diag), 3);
622
623        let b = M::Sparsity::try_from_indices(2, 2, vec![(0, 1)]).unwrap();
624        let unioned = a.clone().union(b.as_ref()).unwrap();
625        assert_eq!(MatrixSparsity::<M>::nrows(&unioned), 2);
626        assert_eq!(MatrixSparsity::<M>::ncols(&unioned), 2);
627
628        let idx = a.get_index(&[(0, 0), (1, 1)], Default::default());
629        assert_eq!(idx.len(), 2);
630
631        let a_ref = a.as_ref();
632        assert_eq!(MatrixSparsityRef::<M>::nrows(&a_ref), 2);
633        assert_eq!(MatrixSparsityRef::<M>::ncols(&a_ref), 2);
634        assert_eq!(
635            M::is_sparse(),
636            <M::SparsityRef<'_> as MatrixSparsityRef<M>>::is_sparse()
637        );
638        let _ = a_ref.indices();
639        let owned = a_ref.to_owned();
640        assert_eq!(MatrixSparsity::<M>::nrows(&owned), 2);
641
642        let algebraic = <M::V as Vector>::Index::from_vec(vec![1], Default::default());
643        let [(ul, _), (_, _), (_, _), (lr, _)] = a_ref.split(&algebraic);
644        assert_eq!(MatrixSparsity::<M>::nrows(&ul), 1);
645        assert_eq!(MatrixSparsity::<M>::nrows(&lr), 1);
646    }
647
648    pub fn test_matrix_common_by_ref<M: Matrix>() {
649        fn via_common<T: MatrixCommon>(m: T) -> (IndexType, IndexType) {
650            let dims = (m.nrows(), m.ncols());
651            let _ = m.inner();
652            dims
653        }
654        let mut a = M::zeros(2, 3, Default::default());
655        assert_eq!(via_common(&a), (2, 3));
656        assert_eq!(via_common(&mut a), (2, 3));
657    }
658
659    pub fn test_from_diagonal<M: Matrix>() {
660        let v = M::V::from_vec(
661            vec![f::<M>(2.0), f::<M>(3.0), f::<M>(5.0)],
662            Default::default(),
663        );
664        let a = M::from_diagonal(&v);
665        assert_eq!(a.nrows(), 3);
666        assert_eq!(a.ncols(), 3);
667        let idx = triplet_indices(&a);
668        let vals = triplet_values(&a);
669        // diagonal matrix triplet_iter returns only the diagonal nnz entries
670        for &(i, j) in &idx {
671            let pos = idx.iter().position(|&x| x == (i, j)).unwrap();
672            if i == j {
673                assert!(
674                    vals[pos] != M::T::zero(),
675                    "diagonal entry should be non-zero"
676                );
677            } else {
678                assert!(vals[pos].is_zero(), "off-diagonal entry should be zero");
679            }
680        }
681    }
682
683    pub fn test_from_diagonal_dense<M: DenseMatrix>() {
684        let v = M::V::from_vec(
685            vec![f::<M>(2.0), f::<M>(3.0), f::<M>(5.0)],
686            Default::default(),
687        );
688        let a = M::from_diagonal(&v);
689        assert_eq!(a.nrows(), 3);
690        assert_eq!(a.ncols(), 3);
691        assert_eq!(a.get_index(0, 0), f::<M>(2.0));
692        assert_eq!(a.get_index(1, 1), f::<M>(3.0));
693        assert_eq!(a.get_index(2, 2), f::<M>(5.0));
694        assert_eq!(a.get_index(0, 1), f::<M>(0.0));
695        assert_eq!(a.get_index(1, 0), f::<M>(0.0));
696    }
697
698    pub fn test_gemv<M: Matrix>() {
699        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
700        let values = vec![f::<M>(1.0), f::<M>(3.0), f::<M>(2.0), f::<M>(4.0)];
701        let a = M::try_from_triplets(2, 2, indices, values, Default::default()).unwrap();
702        let x = M::V::from_vec(vec![f::<M>(1.0), f::<M>(2.0)], Default::default());
703        let mut y = M::V::zeros(2, Default::default());
704        a.gemv(f::<M>(1.0), &x, f::<M>(0.0), &mut y);
705        assert_eq!(y.clone_as_vec(), vec![f::<M>(5.0), f::<M>(11.0)]);
706
707        // beta = 1.0: accumulate onto the pre-existing value of y
708        a.gemv(f::<M>(1.0), &x, f::<M>(1.0), &mut y);
709        assert_eq!(y.clone_as_vec(), vec![f::<M>(10.0), f::<M>(22.0)]);
710    }
711
712    pub fn test_set_column<M: Matrix>() {
713        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
714        let values = vec![f::<M>(0.0), f::<M>(0.0), f::<M>(0.0), f::<M>(0.0)];
715        let mut a = M::try_from_triplets(2, 2, indices, values, Default::default()).unwrap();
716        let v = M::V::from_vec(vec![f::<M>(7.0), f::<M>(8.0)], Default::default());
717        a.set_column(1, &v);
718        let idx = triplet_indices(&a);
719        let vals = triplet_values(&a);
720        assert_eq!(idx, vec![(0, 0), (1, 0), (0, 1), (1, 1)]);
721        assert_eq!(
722            vals,
723            vec![f::<M>(0.0), f::<M>(0.0), f::<M>(7.0), f::<M>(8.0)]
724        );
725    }
726
727    pub fn test_copy_from<M: Matrix>() {
728        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
729        let values = vec![f::<M>(1.0), f::<M>(2.0), f::<M>(3.0), f::<M>(4.0)];
730        let a = M::try_from_triplets(2, 2, indices, values, Default::default()).unwrap();
731        let mut b = M::zeros(2, 2, Default::default());
732        b.copy_from(&a);
733        let vals = triplet_values(&b);
734        assert_eq!(
735            vals,
736            vec![f::<M>(1.0), f::<M>(2.0), f::<M>(3.0), f::<M>(4.0)]
737        );
738    }
739
740    pub fn test_scale_add_and_assign<M: Matrix>() {
741        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
742        let x_vals = vec![f::<M>(1.0), f::<M>(2.0), f::<M>(3.0), f::<M>(4.0)];
743        let y_vals = vec![f::<M>(10.0), f::<M>(20.0), f::<M>(30.0), f::<M>(40.0)];
744        let x = M::try_from_triplets(2, 2, indices.clone(), x_vals, Default::default()).unwrap();
745        let y = M::try_from_triplets(2, 2, indices, y_vals, Default::default()).unwrap();
746        let mut result = M::zeros(2, 2, Default::default());
747        result.copy_from(&x);
748        result.scale_add_and_assign(&x, f::<M>(2.0), &y);
749        let vals = triplet_values(&result);
750        assert_eq!(
751            vals,
752            vec![f::<M>(21.0), f::<M>(42.0), f::<M>(63.0), f::<M>(84.0)]
753        );
754    }
755
756    // --- DenseMatrix-specific tests ---
757
758    /// `Dense<M>`'s `try_from_indices`/`union` error branches: dense matrices ignore the actual
759    /// index pattern (their `Sparsity` is just a bounding box), so only a zero dimension or a
760    /// shape mismatch can make these fail — inputs the sparse backend wouldn't reject the same way.
761    pub fn test_sparsity_dense_errors<M: DenseMatrix>() {
762        assert!(M::Sparsity::try_from_indices(0, 3, vec![]).is_err());
763        let a = M::Sparsity::try_from_indices(2, 2, vec![]).unwrap();
764        let b = M::Sparsity::try_from_indices(3, 3, vec![]).unwrap();
765        assert!(a.union(b.as_ref()).is_err());
766    }
767
768    /// Order 1: 4 columns (`0..=order+2`), column 3 starts as garbage (overwritten).
769    /// Expected values are the right-to-left cumulative scan replayed by hand:
770    ///   col3 = d - col2
771    ///   col2 += col3 (updated); col1 += col2 (updated); col0 += col1 (updated)
772    pub fn test_update_backward_diff<M: DenseMatrix>() {
773        let order = 1;
774        let mut a = M::from_vec(
775            2,
776            4,
777            vec![
778                f::<M>(1.0),
779                f::<M>(10.0),
780                f::<M>(2.0),
781                f::<M>(20.0),
782                f::<M>(3.0),
783                f::<M>(30.0),
784                f::<M>(0.0),
785                f::<M>(0.0),
786            ],
787            Default::default(),
788        );
789        let d = M::V::from_vec(vec![f::<M>(100.0), f::<M>(1000.0)], Default::default());
790
791        a.update_backward_diff(order, &d);
792
793        assert_eq!(a.get_index(0, 3), f::<M>(97.0));
794        assert_eq!(a.get_index(1, 3), f::<M>(970.0));
795        assert_eq!(a.get_index(0, 2), f::<M>(100.0));
796        assert_eq!(a.get_index(1, 2), f::<M>(1000.0));
797        assert_eq!(a.get_index(0, 1), f::<M>(102.0));
798        assert_eq!(a.get_index(1, 1), f::<M>(1020.0));
799        assert_eq!(a.get_index(0, 0), f::<M>(103.0));
800        assert_eq!(a.get_index(1, 0), f::<M>(1030.0));
801    }
802
803    /// A pseudo-random `nrows x ncols` matrix, each column from [`Vector::rand`] with its own
804    /// seed.
805    fn rand_mat<M: DenseMatrix>(nrows: usize, ncols: usize, seed: f64) -> M {
806        let mut m = M::zeros(nrows, ncols, Default::default());
807        for j in 0..ncols {
808            let col = M::V::rand(nrows, seed * (j + 1) as f64, Default::default());
809            m.column_mut(j).copy_from(&col);
810        }
811        m
812    }
813
814    /// `<a, b>` over the entries of columns `cols`, in every batch.
815    fn mat_dot<M: DenseMatrix>(a: &M, b: &M, cols: std::ops::Range<usize>) -> M::T {
816        cols.fold(M::T::zero(), |acc, j| {
817            acc + a.column(j).into_owned().dot(&b.column(j).into_owned())
818        })
819    }
820
821    /// `a` and `b` agree exactly in columns `cols`, in every batch.
822    fn assert_cols_eq<M: DenseMatrix>(a: &M, b: &M, cols: std::ops::Range<usize>) {
823        assert_eq!(a.nrows(), b.nrows());
824        for j in cols {
825            a.column(j)
826                .into_owned()
827                .assert_eq_st(&b.column(j).into_owned(), M::T::zero());
828        }
829    }
830
831    /// `a` and `b` agree exactly, in every batch.
832    fn assert_mat_eq<M: DenseMatrix>(a: &M, b: &M) {
833        assert_eq!(a.ncols(), b.ncols());
834        assert_cols_eq(a, b, 0..a.ncols());
835    }
836
837    fn assert_close<M: Matrix>(a: M::T, b: M::T) {
838        use num_traits::Signed;
839        let scale = if a.abs() > b.abs() { a.abs() } else { b.abs() };
840        let tol = f::<M>(1e-4) * (M::T::one() + scale);
841        assert!((a - b).abs() <= tol, "{a} != {b}");
842    }
843
844    /// Dot test of [`DenseMatrix::update_backward_diff_transpose`] against
845    /// [`DenseMatrix::update_backward_diff`]: `<C, U(D, d)> = <C_D, D> + <d_bar, d>`, with
846    /// `(C_D, d_bar) = Uᵀ(C)`, for every order the BDF solver uses. Also checks the transpose
847    /// leaves the columns above `order + 2` alone.
848    pub fn test_update_backward_diff_transpose<M: DenseMatrix>() {
849        let nrows = 3;
850        let ncols = MAX_SMALL_COLS;
851        for order in 1..=5 {
852            let d = M::V::rand(nrows, 0.7, Default::default());
853            let d_mat = rand_mat::<M>(nrows, ncols, 1.3);
854            let c0 = rand_mat::<M>(nrows, ncols, 2.9);
855
856            let mut u = d_mat.clone();
857            u.update_backward_diff(order, &d);
858            let lhs = mat_dot(&c0, &u, 0..ncols);
859
860            let mut c = c0.clone();
861            let mut d_bar = M::V::zeros(nrows, Default::default());
862            c.update_backward_diff_transpose(order, &mut d_bar);
863            let rhs = mat_dot(&c, &d_mat, 0..ncols) + d_bar.dot(&d);
864            assert_close::<M>(lhs, rhs);
865
866            assert_cols_eq(&c, &c0, order + 3..ncols);
867        }
868    }
869
870    /// [`DenseMatrix::mul_cols_by_transpose`] matches [`DenseMatrix::mul_cols_by`] with an
871    /// explicitly transposed `rhs`, and passes the dot test `<C, D R> = <C Rᵀ, D>`.
872    pub fn test_mul_cols_by_transpose<M: DenseMatrix>() {
873        let nrows = 3;
874        let ncols = 5;
875        let ntot = 7;
876        let r = M::V::rand(ncols * ncols, 0.37, Default::default()).clone_as_vec();
877        let mut r_t = r.clone();
878        for j in 0..ncols {
879            for i in 0..ncols {
880                r_t[j * ncols + i] = r[i * ncols + j];
881            }
882        }
883        let d = rand_mat::<M>(nrows, ntot, 1.1);
884        let c = rand_mat::<M>(nrows, ntot, 2.3);
885
886        let mut a = d.clone();
887        let mut b = d.clone();
888        a.mul_cols_by_transpose(ncols, &r);
889        b.mul_cols_by(ncols, &r_t);
890        assert_mat_eq(&a, &b);
891
892        let mut dr = d.clone();
893        dr.mul_cols_by(ncols, &r);
894        let mut cr = c.clone();
895        cr.mul_cols_by_transpose(ncols, &r);
896        // only the leading ncols columns are transformed; the dot test holds there and the
897        // trailing columns pass through unchanged on both sides
898        assert_close::<M>(mat_dot(&c, &dr, 0..ncols), mat_dot(&cr, &d, 0..ncols));
899        assert_cols_eq(&cr, &c, ncols..ntot);
900    }
901
902    pub fn test_ger_cols<M: DenseMatrix>() {
903        // columns: [1, 10], [2, 20], [3, 30], [4, 40]
904        let mk = || {
905            M::from_vec(
906                2,
907                4,
908                (1..=4)
909                    .flat_map(|j| [f::<M>(j as f64), f::<M>(10.0 * j as f64)])
910                    .collect(),
911                Default::default(),
912            )
913        };
914        let y = M::V::from_vec(vec![f::<M>(1.0), f::<M>(-1.0)], Default::default());
915
916        // columns 1 and 2 gain 2 * [3, 5] * y; x lines up with the start of the range, and
917        // the columns outside it are left alone
918        let mut a = mk();
919        a.ger_cols(1, 3, f::<M>(2.0), &[f::<M>(3.0), f::<M>(5.0)], &y);
920        let expected = M::from_vec(
921            2,
922            4,
923            [1.0, 10.0, 8.0, 14.0, 13.0, 20.0, 4.0, 40.0]
924                .into_iter()
925                .map(f::<M>)
926                .collect(),
927            Default::default(),
928        );
929        assert_mat_eq(&a, &expected);
930
931        // an empty range is a no-op
932        let mut a = mk();
933        a.ger_cols(2, 2, f::<M>(2.0), &[], &y);
934        assert_mat_eq(&a, &mk());
935    }
936
937    /// Dot test of [`DenseMatrix::ger_cols`] against [`DenseMatrix::gemv_cols`], whose
938    /// transpose in the matrix it is: `<alpha y xᵀ, A> = <y, alpha A x>` over the column range.
939    pub fn test_ger_cols_transpose<M: DenseMatrix>() {
940        let (nrows, ncols, start, end) = (3, 6, 1, 5);
941        let a = rand_mat::<M>(nrows, ncols, 0.9);
942        let y = M::V::rand(nrows, 1.7, Default::default());
943        let x = M::V::rand(end - start, 2.3, Default::default()).clone_as_vec();
944        let alpha = f::<M>(-0.6);
945
946        let mut update = M::zeros(nrows, ncols, Default::default());
947        update.ger_cols(start, end, alpha, &x, &y);
948        let mut ax = M::V::zeros(nrows, Default::default());
949        a.gemv_cols(start, end, alpha, &x, f::<M>(0.0), &mut ax);
950        assert_close::<M>(mat_dot(&update, &a, 0..ncols), y.dot(&ax));
951    }
952
953    pub fn test_gemv_cols<M: DenseMatrix>() {
954        // columns: [1, 10], [2, 20], [3, 30], [4, 40]
955        let a = M::from_vec(
956            2,
957            4,
958            (1..=4)
959                .flat_map(|j| [f::<M>(j as f64), f::<M>(10.0 * j as f64)])
960                .collect(),
961            Default::default(),
962        );
963        let ones = |n: usize| vec![f::<M>(1.0); n];
964        let mut y = M::V::from_vec(vec![f::<M>(-1.0), f::<M>(-1.0)], Default::default());
965
966        // unweighted sum of columns 0..3, overwriting y
967        a.gemv_cols(0, 3, f::<M>(1.0), &ones(3), f::<M>(0.0), &mut y);
968        assert_eq!(y.clone_as_vec(), vec![f::<M>(6.0), f::<M>(60.0)]);
969
970        // weighted sum of every column
971        let w4 = vec![f::<M>(1.0), f::<M>(2.0), f::<M>(3.0), f::<M>(4.0)];
972        a.gemv_cols(0, 4, f::<M>(1.0), &w4, f::<M>(0.0), &mut y);
973        assert_eq!(y.clone_as_vec(), vec![f::<M>(30.0), f::<M>(300.0)]);
974
975        // x lines up with the start of the range, not with column zero
976        let w23 = vec![f::<M>(2.0), f::<M>(3.0)];
977        a.gemv_cols(2, 4, f::<M>(1.0), &w23, f::<M>(0.0), &mut y);
978        assert_eq!(y.clone_as_vec(), vec![f::<M>(18.0), f::<M>(180.0)]);
979
980        // alpha scales the whole product
981        a.gemv_cols(2, 4, f::<M>(2.0), &w23, f::<M>(0.0), &mut y);
982        assert_eq!(y.clone_as_vec(), vec![f::<M>(36.0), f::<M>(360.0)]);
983
984        // beta = 1 accumulates onto y instead of overwriting it
985        let half = vec![f::<M>(0.5)];
986        a.gemv_cols(1, 2, f::<M>(1.0), &half, f::<M>(0.0), &mut y);
987        assert_eq!(y.clone_as_vec(), vec![f::<M>(1.0), f::<M>(10.0)]);
988        a.gemv_cols(1, 2, f::<M>(1.0), &half, f::<M>(1.0), &mut y);
989        assert_eq!(y.clone_as_vec(), vec![f::<M>(2.0), f::<M>(20.0)]);
990
991        // a general beta scales the old y before accumulating
992        a.gemv_cols(1, 2, f::<M>(1.0), &half, f::<M>(3.0), &mut y);
993        assert_eq!(y.clone_as_vec(), vec![f::<M>(7.0), f::<M>(70.0)]);
994
995        // an empty range contributes nothing, leaving y = beta * y
996        a.gemv_cols(1, 1, f::<M>(1.0), &ones(0), f::<M>(2.0), &mut y);
997        assert_eq!(y.clone_as_vec(), vec![f::<M>(14.0), f::<M>(140.0)]);
998        a.gemv_cols(1, 1, f::<M>(1.0), &ones(0), f::<M>(1.0), &mut y);
999        assert_eq!(y.clone_as_vec(), vec![f::<M>(14.0), f::<M>(140.0)]);
1000        a.gemv_cols(1, 1, f::<M>(1.0), &ones(0), f::<M>(0.0), &mut y);
1001        assert_eq!(y.clone_as_vec(), vec![f::<M>(0.0), f::<M>(0.0)]);
1002    }
1003
1004    /// A column range wider than the 8-weight chunk the retired `weighted_column_sum` CUDA
1005    /// kernel could take in one launch — that boundary was never covered by a test.
1006    /// Entry `(i, j)` of batch `b`. `Matrix::get_index` only ever reads batch 0, and a
1007    /// column view carries every batch, so this goes through the column.
1008    fn mat_at<M: DenseMatrix>(m: &M, b: usize, i: usize, j: usize) -> M::T {
1009        m.column(j).into_owned().clone_as_vec()[b * m.nrows() + i]
1010    }
1011
1012    pub fn test_mul_cols_by<M: DenseMatrix>() {
1013        // 2x4, columns [1,10], [2,20], [3,30], [4,40]
1014        let mk = || {
1015            M::from_vec(
1016                2,
1017                4,
1018                (1..=4)
1019                    .flat_map(|j| [f::<M>(j as f64), f::<M>(10.0 * j as f64)])
1020                    .collect(),
1021                Default::default(),
1022            )
1023        };
1024        let untouched = mk();
1025
1026        // identity must leave the matrix exactly as it was: the sharpest check that the
1027        // in-place update never reads a value it has already overwritten
1028        let mut a = mk();
1029        a.mul_cols_by(2, &[f::<M>(1.0), f::<M>(0.0), f::<M>(0.0), f::<M>(1.0)]);
1030        assert_mat_eq(&a, &untouched);
1031
1032        // an empty range is a no-op
1033        let mut a = mk();
1034        a.mul_cols_by(0, &[]);
1035        assert_mat_eq(&a, &untouched);
1036
1037        // a single column is just scaled, and the rest are left alone
1038        let mut a = mk();
1039        a.mul_cols_by(1, &[f::<M>(3.0)]);
1040        let expected = M::from_vec(
1041            2,
1042            4,
1043            vec![
1044                f::<M>(3.0),
1045                f::<M>(30.0),
1046                f::<M>(2.0),
1047                f::<M>(20.0),
1048                f::<M>(3.0),
1049                f::<M>(30.0),
1050                f::<M>(4.0),
1051                f::<M>(40.0),
1052            ],
1053            Default::default(),
1054        );
1055        assert_mat_eq(&a, &expected);
1056
1057        // rhs = [[1, 2], [3, 4]] column-major, applied to the first two columns only:
1058        //   new[:, 0] = 1 * [1,10] + 3 * [2,20] = [7, 70]
1059        //   new[:, 1] = 2 * [1,10] + 4 * [2,20] = [10, 100]
1060        // columns 2 and 3 must survive untouched -- the BDF difference table keeps live data
1061        // above the updated range and reads it back in `update_backward_diff`
1062        let mut a = mk();
1063        a.mul_cols_by(2, &[f::<M>(1.0), f::<M>(3.0), f::<M>(2.0), f::<M>(4.0)]);
1064        let expected = M::from_vec(
1065            2,
1066            4,
1067            vec![
1068                f::<M>(7.0),
1069                f::<M>(70.0),
1070                f::<M>(10.0),
1071                f::<M>(100.0),
1072                f::<M>(3.0),
1073                f::<M>(30.0),
1074                f::<M>(4.0),
1075                f::<M>(40.0),
1076            ],
1077            Default::default(),
1078        );
1079        assert_mat_eq(&a, &expected);
1080
1081        // the full width the solvers use (MAX_ORDER + 1), via an identity
1082        let ncols = 6;
1083        let mut wide = M::from_vec(
1084            2,
1085            ncols,
1086            (1..=ncols)
1087                .flat_map(|j| [f::<M>(j as f64), f::<M>(10.0 * j as f64)])
1088                .collect(),
1089            Default::default(),
1090        );
1091        let wide_before = wide.clone();
1092        let mut id = vec![f::<M>(0.0); ncols * ncols];
1093        for j in 0..ncols {
1094            id[j * ncols + j] = f::<M>(1.0);
1095        }
1096        wide.mul_cols_by(ncols, &id);
1097        assert_mat_eq(&wide, &wide_before);
1098
1099        // more rows than fit one staging tile, so the row-blocked loop runs several times
1100        let nrows = 700;
1101        let mut tall = M::from_vec(
1102            nrows,
1103            2,
1104            (0..2 * nrows).map(|i| f::<M>(i as f64)).collect(),
1105            Default::default(),
1106        );
1107        tall.mul_cols_by(2, &[f::<M>(1.0), f::<M>(3.0), f::<M>(2.0), f::<M>(4.0)]);
1108        // the original columns are 0..nrows and nrows..2 nrows
1109        let c0 = |i: usize| f::<M>(i as f64);
1110        let c1 = |i: usize| f::<M>((nrows + i) as f64);
1111        let expected = M::from_vec(
1112            nrows,
1113            2,
1114            (0..nrows)
1115                .map(|i| c0(i) + f::<M>(3.0) * c1(i))
1116                .chain((0..nrows).map(|i| f::<M>(2.0) * c0(i) + f::<M>(4.0) * c1(i)))
1117                .collect(),
1118            Default::default(),
1119        );
1120        assert_mat_eq(&tall, &expected);
1121    }
1122
1123    /// [`DenseMatrix::fill`] sets every entry of every batch, and keeps the shape.
1124    pub fn test_fill<M: DenseMatrix>(ctx: M::C) {
1125        let (nrows, ncols) = (3, 4);
1126        let mut m = M::zeros(nrows, ncols, ctx.clone());
1127        for j in 0..ncols {
1128            let col = M::V::rand(nrows, (j + 1) as f64, ctx.clone());
1129            m.column_mut(j).copy_from(&col);
1130        }
1131        m.fill(f::<M>(2.5));
1132        assert_eq!((m.nrows(), m.ncols()), (nrows, ncols));
1133        let expected = M::V::from_element(nrows, f::<M>(2.5), ctx);
1134        for j in 0..ncols {
1135            m.column(j)
1136                .into_owned()
1137                .assert_eq_st(&expected, f::<M>(0.0));
1138        }
1139    }
1140
1141    /// Same product as [`test_mul_cols_by`] on 2 independent batches: `rhs` is shared by every
1142    /// batch, the columns are not.
1143    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1144    pub fn test_batched_mul_cols_by<M: DenseMatrix>(ctx: M::C) {
1145        assert_eq!(ctx.nbatch(), 2);
1146        // batch0 columns [1,10], [2,20]; batch1 columns [3,30], [4,40]
1147        let mut a = M::from_vec(
1148            2,
1149            2,
1150            (1..=4)
1151                .flat_map(|j| [f::<M>(j as f64), f::<M>(10.0 * j as f64)])
1152                .collect(),
1153            ctx.clone(),
1154        );
1155        a.mul_cols_by(2, &[f::<M>(1.0), f::<M>(3.0), f::<M>(2.0), f::<M>(4.0)]);
1156        // batch0: [1,10] + 3*[2,20] = [7,70];   2*[1,10] + 4*[2,20] = [10,100]
1157        // batch1: [3,30] + 3*[4,40] = [15,150]; 2*[3,30] + 4*[4,40] = [22,220]
1158        let expected = M::from_vec(
1159            2,
1160            2,
1161            [7.0, 70.0, 10.0, 100.0, 15.0, 150.0, 22.0, 220.0]
1162                .into_iter()
1163                .map(f::<M>)
1164                .collect(),
1165            ctx,
1166        );
1167        assert_mat_eq(&a, &expected);
1168    }
1169
1170    /// The widest column range `gemv_cols` accepts, which is also the size of the by-value
1171    /// coefficient array the CUDA kernel takes.
1172    pub fn test_gemv_cols_many_columns<M: DenseMatrix>() {
1173        let ncols = MAX_SMALL_COLS;
1174        // column j is [j + 1, 10 * (j + 1)]
1175        let a = M::from_vec(
1176            2,
1177            ncols,
1178            (1..=ncols)
1179                .flat_map(|j| [f::<M>(j as f64), f::<M>(10.0 * j as f64)])
1180                .collect(),
1181            Default::default(),
1182        );
1183        let x = vec![f::<M>(1.0); ncols];
1184        let mut y = M::V::from_vec(vec![f::<M>(-1.0), f::<M>(-1.0)], Default::default());
1185        a.gemv_cols(0, ncols, f::<M>(1.0), &x, f::<M>(0.0), &mut y);
1186        // sum_{j=1}^{ncols} j
1187        let total = f::<M>((ncols * (ncols + 1) / 2) as f64);
1188        assert_eq!(y.clone_as_vec(), vec![total, f::<M>(10.0) * total]);
1189    }
1190
1191    /// [`DenseMatrix::ger_cols`] on 2 batches: the coefficients are shared, `y` is per batch.
1192    pub fn test_batched_ger_cols<M: DenseMatrix>(ctx: M::C) {
1193        assert_eq!(ctx.nbatch(), 2);
1194        // batch 0 columns: [1, 10], [2, 20]; batch 1 columns: [3, 30], [4, 40]
1195        let mut a = M::from_vec(
1196            2,
1197            2,
1198            (1..=4)
1199                .flat_map(|j| [f::<M>(j as f64), f::<M>(10.0 * j as f64)])
1200                .collect(),
1201            ctx.clone(),
1202        );
1203        // y is [1, 0] in batch 0 and [0, 1] in batch 1
1204        let y = M::V::from_vec(
1205            vec![f::<M>(1.0), f::<M>(0.0), f::<M>(0.0), f::<M>(1.0)],
1206            ctx.clone(),
1207        );
1208        a.ger_cols(0, 2, f::<M>(1.0), &[f::<M>(2.0), f::<M>(3.0)], &y);
1209        let expected = M::from_vec(
1210            2,
1211            2,
1212            [3.0, 10.0, 5.0, 20.0, 3.0, 32.0, 4.0, 43.0]
1213                .into_iter()
1214                .map(f::<M>)
1215                .collect(),
1216            ctx,
1217        );
1218        assert_mat_eq(&a, &expected);
1219    }
1220
1221    /// Same products as [`test_gemv_cols`], run on 2 independent batches: `x` is unbatched and
1222    /// therefore shared by every batch, the columns are not.
1223    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1224    pub fn test_batched_gemv_cols_m<M: DenseMatrix>(ctx: M::C) {
1225        assert_eq!(ctx.nbatch(), 2);
1226
1227        // batch 0 columns: [1, 10], [2, 20]; batch 1 columns: [3, 30], [4, 40]
1228        let a = M::from_vec(
1229            2,
1230            2,
1231            (1..=4)
1232                .flat_map(|j| [f::<M>(j as f64), f::<M>(10.0 * j as f64)])
1233                .collect(),
1234            ctx.clone(),
1235        );
1236        let mut y = M::V::from_vec(
1237            vec![f::<M>(-1.0), f::<M>(-1.0), f::<M>(-1.0), f::<M>(-1.0)],
1238            ctx,
1239        );
1240
1241        let ones = vec![f::<M>(1.0), f::<M>(1.0)];
1242        a.gemv_cols(0, 2, f::<M>(1.0), &ones, f::<M>(0.0), &mut y);
1243        assert_eq!(
1244            y.clone_as_vec(),
1245            vec![f::<M>(3.0), f::<M>(30.0), f::<M>(7.0), f::<M>(70.0)]
1246        );
1247
1248        let w23 = vec![f::<M>(2.0), f::<M>(3.0)];
1249        a.gemv_cols(0, 2, f::<M>(1.0), &w23, f::<M>(0.0), &mut y);
1250        assert_eq!(
1251            y.clone_as_vec(),
1252            vec![f::<M>(8.0), f::<M>(80.0), f::<M>(18.0), f::<M>(180.0)]
1253        );
1254
1255        let half = vec![f::<M>(0.5)];
1256        a.gemv_cols(1, 2, f::<M>(1.0), &half, f::<M>(0.0), &mut y);
1257        assert_eq!(
1258            y.clone_as_vec(),
1259            vec![f::<M>(1.0), f::<M>(10.0), f::<M>(2.0), f::<M>(20.0)]
1260        );
1261
1262        // beta = 1 accumulates independently in each batch
1263        a.gemv_cols(1, 2, f::<M>(1.0), &half, f::<M>(1.0), &mut y);
1264        assert_eq!(
1265            y.clone_as_vec(),
1266            vec![f::<M>(2.0), f::<M>(20.0), f::<M>(4.0), f::<M>(40.0)]
1267        );
1268    }
1269
1270    pub fn test_resize_cols<M: DenseMatrix>() {
1271        let mut a = M::zeros(2, 2, Default::default());
1272        a.set_index(0, 0, M::T::one());
1273        a.set_index(0, 1, M::T::from_f64(2.0).unwrap());
1274        a.set_index(1, 0, M::T::from_f64(3.0).unwrap());
1275        a.set_index(1, 1, M::T::from_f64(4.0).unwrap());
1276
1277        // resizing to the current column count is a no-op
1278        a.resize_cols(2);
1279        assert_eq!(a.ncols(), 2);
1280        assert_eq!(a.get_index(0, 0), M::T::one());
1281
1282        a.resize_cols(3);
1283        assert_eq!(a.ncols(), 3);
1284        assert_eq!(a.nrows(), 2);
1285        assert_eq!(a.get_index(0, 0), M::T::one());
1286        assert_eq!(a.get_index(0, 1), M::T::from_f64(2.0).unwrap());
1287        assert_eq!(a.get_index(1, 0), M::T::from_f64(3.0).unwrap());
1288        assert_eq!(a.get_index(1, 1), M::T::from_f64(4.0).unwrap());
1289
1290        a.set_index(0, 2, M::T::from_f64(5.0).unwrap());
1291        a.set_index(1, 2, M::T::from_f64(6.0).unwrap());
1292        assert_eq!(a.get_index(0, 2), M::T::from_f64(5.0).unwrap());
1293        assert_eq!(a.get_index(1, 2), M::T::from_f64(6.0).unwrap());
1294
1295        a.resize_cols(2);
1296        assert_eq!(a.ncols(), 2);
1297        assert_eq!(a.nrows(), 2);
1298        assert_eq!(a.get_index(0, 0), M::T::one());
1299        assert_eq!(a.get_index(0, 1), M::T::from_f64(2.0).unwrap());
1300        assert_eq!(a.get_index(1, 0), M::T::from_f64(3.0).unwrap());
1301        assert_eq!(a.get_index(1, 1), M::T::from_f64(4.0).unwrap());
1302    }
1303
1304    pub fn test_from_vec<M: DenseMatrix>() {
1305        let a = M::from_vec(
1306            2,
1307            2,
1308            vec![f::<M>(1.0), f::<M>(3.0), f::<M>(2.0), f::<M>(4.0)],
1309            Default::default(),
1310        );
1311        assert_eq!(a.nrows(), 2);
1312        assert_eq!(a.ncols(), 2);
1313        assert_eq!(a.get_index(0, 0), f::<M>(1.0));
1314        assert_eq!(a.get_index(1, 0), f::<M>(3.0));
1315        assert_eq!(a.get_index(0, 1), f::<M>(2.0));
1316        assert_eq!(a.get_index(1, 1), f::<M>(4.0));
1317    }
1318
1319    pub fn test_column_view<M: DenseMatrix>() {
1320        let a = M::from_vec(
1321            2,
1322            2,
1323            vec![f::<M>(1.0), f::<M>(3.0), f::<M>(2.0), f::<M>(4.0)],
1324            Default::default(),
1325        );
1326        let col = a.column(1);
1327        use crate::VectorView;
1328        assert_eq!(col.get_index(0), f::<M>(2.0));
1329        assert_eq!(col.get_index(1), f::<M>(4.0));
1330    }
1331
1332    // --- Batched Matrix-generic tests ---
1333
1334    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1335    pub fn test_batched_zeros_m<M: Matrix>(ctx: M::C) {
1336        assert_eq!(ctx.nbatch(), 2);
1337        let a = M::zeros(2, 3, ctx);
1338        assert_eq!(a.nrows(), 2);
1339        assert_eq!(a.ncols(), 3);
1340        let vals = triplet_values(&a);
1341        assert!(vals.is_empty() || vals.iter().all(|v| v.is_zero()));
1342    }
1343
1344    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1345    pub fn test_batched_gemv_m<M: Matrix>(ctx: M::C) {
1346        assert_eq!(ctx.nbatch(), 2);
1347        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1348        let values = vec![
1349            f::<M>(1.0),
1350            f::<M>(3.0),
1351            f::<M>(2.0),
1352            f::<M>(4.0), // batch 0
1353            f::<M>(5.0),
1354            f::<M>(7.0),
1355            f::<M>(6.0),
1356            f::<M>(8.0), // batch 1
1357        ];
1358        let a = M::try_from_triplets(2, 2, indices, values, ctx.clone()).unwrap();
1359        let x = M::V::from_vec(
1360            vec![f::<M>(1.0), f::<M>(2.0), f::<M>(1.0), f::<M>(1.0)],
1361            ctx.clone(),
1362        );
1363        let mut y = M::V::zeros(2, ctx);
1364        a.gemv(f::<M>(1.0), &x, f::<M>(0.0), &mut y);
1365        assert_eq!(
1366            y.clone_as_vec(),
1367            vec![f::<M>(5.0), f::<M>(11.0), f::<M>(11.0), f::<M>(15.0)]
1368        );
1369    }
1370
1371    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1372    pub fn test_batched_gemv_broadcast_x_m<M: Matrix>(ctx: M::C) {
1373        assert_eq!(ctx.nbatch(), 2);
1374        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1375        let values = vec![
1376            f::<M>(1.0),
1377            f::<M>(3.0),
1378            f::<M>(2.0),
1379            f::<M>(4.0),
1380            f::<M>(5.0),
1381            f::<M>(7.0),
1382            f::<M>(6.0),
1383            f::<M>(8.0),
1384        ];
1385        let a = M::try_from_triplets(2, 2, indices, values, ctx.clone()).unwrap();
1386        let x = M::V::from_vec(vec![f::<M>(1.0), f::<M>(2.0)], Default::default());
1387        let mut y = M::V::zeros(2, ctx);
1388        a.gemv(f::<M>(1.0), &x, f::<M>(0.0), &mut y);
1389        assert_eq!(
1390            y.clone_as_vec(),
1391            vec![f::<M>(5.0), f::<M>(11.0), f::<M>(17.0), f::<M>(23.0)]
1392        );
1393    }
1394
1395    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1396    pub fn test_batched_gemv_broadcast_mat_m<M: Matrix>(ctx: M::C) {
1397        assert_eq!(ctx.nbatch(), 2);
1398        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1399        let values = vec![f::<M>(1.0), f::<M>(3.0), f::<M>(2.0), f::<M>(4.0)];
1400        let a =
1401            M::try_from_triplets(2, 2, indices, values, ctx.clone_with_nbatch(1).unwrap()).unwrap();
1402        let x = M::V::from_vec(
1403            vec![f::<M>(1.0), f::<M>(2.0), f::<M>(3.0), f::<M>(4.0)],
1404            ctx.clone(),
1405        );
1406        let mut y = M::V::zeros(2, ctx);
1407        a.gemv(f::<M>(1.0), &x, f::<M>(0.0), &mut y);
1408        assert_eq!(
1409            y.clone_as_vec(),
1410            vec![f::<M>(5.0), f::<M>(11.0), f::<M>(11.0), f::<M>(25.0)]
1411        );
1412    }
1413
1414    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1415    pub fn test_batched_from_diagonal_m<M: Matrix>(ctx: M::C) {
1416        assert_eq!(ctx.nbatch(), 2);
1417        let v = M::V::from_vec(
1418            vec![f::<M>(2.0), f::<M>(3.0), f::<M>(4.0), f::<M>(5.0)],
1419            ctx,
1420        );
1421        let a = M::from_diagonal(&v);
1422        assert_eq!(a.nrows(), 2);
1423        assert_eq!(a.ncols(), 2);
1424        let idx = triplet_indices(&a);
1425        let vals = triplet_values(&a);
1426        for &(i, j) in &idx {
1427            let pos = idx.iter().position(|&x| x == (i, j)).unwrap();
1428            if i == j {
1429                assert!(
1430                    vals[pos] != M::T::zero(),
1431                    "diagonal entry should be non-zero"
1432                );
1433            } else {
1434                assert!(vals[pos].is_zero(), "off-diagonal entry should be zero");
1435            }
1436        }
1437    }
1438
1439    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1440    pub fn test_batched_copy_from_m<M: Matrix>(ctx: M::C) {
1441        assert_eq!(ctx.nbatch(), 2);
1442        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1443        let values = vec![
1444            f::<M>(1.0),
1445            f::<M>(2.0),
1446            f::<M>(3.0),
1447            f::<M>(4.0),
1448            f::<M>(5.0),
1449            f::<M>(6.0),
1450            f::<M>(7.0),
1451            f::<M>(8.0),
1452        ];
1453        let a = M::try_from_triplets(2, 2, indices, values, ctx.clone()).unwrap();
1454        let mut b = M::zeros(2, 2, ctx);
1455        b.copy_from(&a);
1456        let vals = triplet_values(&b);
1457        assert_eq!(
1458            vals,
1459            vec![
1460                f::<M>(1.0),
1461                f::<M>(2.0),
1462                f::<M>(3.0),
1463                f::<M>(4.0),
1464                f::<M>(5.0),
1465                f::<M>(6.0),
1466                f::<M>(7.0),
1467                f::<M>(8.0),
1468            ]
1469        );
1470    }
1471
1472    /// `copy_from` with mismatched batch counts: the source broadcasts over the destination's
1473    /// batches, exercising the per-batch loop instead of the equal-nbatch whole-matrix copy.
1474    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1475    pub fn test_batched_copy_from_broadcast_m<M: Matrix>(ctx: M::C) {
1476        assert_eq!(ctx.nbatch(), 2);
1477        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1478        let values = vec![f::<M>(1.0), f::<M>(2.0), f::<M>(3.0), f::<M>(4.0)];
1479        let a = M::try_from_triplets(2, 2, indices, values, M::C::default()).unwrap();
1480        let mut b = M::zeros(2, 2, ctx);
1481        b.copy_from(&a);
1482        let vals = triplet_values(&b);
1483        assert_eq!(
1484            vals,
1485            vec![
1486                f::<M>(1.0),
1487                f::<M>(2.0),
1488                f::<M>(3.0),
1489                f::<M>(4.0),
1490                f::<M>(1.0),
1491                f::<M>(2.0),
1492                f::<M>(3.0),
1493                f::<M>(4.0),
1494            ]
1495        );
1496    }
1497
1498    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1499    pub fn test_batched_set_column_m<M: Matrix>(ctx: M::C) {
1500        assert_eq!(ctx.nbatch(), 2);
1501        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1502        let values = vec![
1503            f::<M>(0.0),
1504            f::<M>(0.0),
1505            f::<M>(0.0),
1506            f::<M>(0.0),
1507            f::<M>(0.0),
1508            f::<M>(0.0),
1509            f::<M>(0.0),
1510            f::<M>(0.0),
1511        ];
1512        let mut a = M::try_from_triplets(2, 2, indices, values, ctx.clone()).unwrap();
1513        let v = M::V::from_vec(
1514            vec![f::<M>(5.0), f::<M>(6.0), f::<M>(7.0), f::<M>(8.0)],
1515            ctx,
1516        );
1517        a.set_column(0, &v);
1518        let vals = triplet_values(&a);
1519        assert_eq!(
1520            vals,
1521            vec![
1522                f::<M>(5.0),
1523                f::<M>(6.0),
1524                f::<M>(0.0),
1525                f::<M>(0.0),
1526                f::<M>(7.0),
1527                f::<M>(8.0),
1528                f::<M>(0.0),
1529                f::<M>(0.0),
1530            ]
1531        );
1532    }
1533
1534    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1535    pub fn test_batched_scale_add_and_assign_m<M: Matrix>(ctx: M::C) {
1536        assert_eq!(ctx.nbatch(), 2);
1537        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1538        let x_vals = vec![
1539            f::<M>(1.0),
1540            f::<M>(2.0),
1541            f::<M>(3.0),
1542            f::<M>(4.0),
1543            f::<M>(5.0),
1544            f::<M>(6.0),
1545            f::<M>(7.0),
1546            f::<M>(8.0),
1547        ];
1548        let y_vals = vec![
1549            f::<M>(10.0),
1550            f::<M>(20.0),
1551            f::<M>(30.0),
1552            f::<M>(40.0),
1553            f::<M>(50.0),
1554            f::<M>(60.0),
1555            f::<M>(70.0),
1556            f::<M>(80.0),
1557        ];
1558        let x = M::try_from_triplets(2, 2, indices.clone(), x_vals, ctx.clone()).unwrap();
1559        let y = M::try_from_triplets(2, 2, indices, y_vals, ctx.clone()).unwrap();
1560        let mut result = M::zeros(2, 2, ctx);
1561        result.copy_from(&x);
1562        result.scale_add_and_assign(&x, f::<M>(2.0), &y);
1563        let vals = triplet_values(&result);
1564        assert_eq!(
1565            vals,
1566            vec![
1567                f::<M>(21.0),
1568                f::<M>(42.0),
1569                f::<M>(63.0),
1570                f::<M>(84.0),
1571                f::<M>(105.0),
1572                f::<M>(126.0),
1573                f::<M>(147.0),
1574                f::<M>(168.0),
1575            ]
1576        );
1577    }
1578
1579    /// An owned right-hand side may be written into, which reverses the operands of `sub`.
1580    ///
1581    /// Only wired up for the backends that implement the owned-rhs combinations; move the call
1582    /// into `generate_dense_matrix_tests_nonbatched!` once they all do.
1583    pub fn test_owned_rhs_m<M>()
1584    where
1585        M: DenseMatrix,
1586        for<'a> &'a M: Add<M, Output = M> + Sub<M, Output = M>,
1587    {
1588        let values = vec![f::<M>(1.0), f::<M>(3.0), f::<M>(2.0), f::<M>(4.0)];
1589        let a = M::from_vec(
1590            2,
1591            2,
1592            vec![f::<M>(10.0), f::<M>(30.0), f::<M>(20.0), f::<M>(40.0)],
1593            Default::default(),
1594        );
1595
1596        let b = M::from_vec(2, 2, values.clone(), Default::default());
1597        let c = &a - b;
1598        assert_eq!(c.get_index(0, 0), f::<M>(9.0));
1599        assert_eq!(c.get_index(1, 1), f::<M>(36.0));
1600
1601        let b = M::from_vec(2, 2, values, Default::default());
1602        let c = &a + b;
1603        assert_eq!(c.get_index(0, 0), f::<M>(11.0));
1604        assert_eq!(c.get_index(1, 1), f::<M>(44.0));
1605    }
1606
1607    /// Owned right-hand side with broadcasting in both directions: the right-hand side can
1608    /// only be written into when it already has the result's batch count.
1609    pub fn test_batched_owned_rhs_broadcast_m<M>(ctx: M::C)
1610    where
1611        M: Matrix + DenseMatrix,
1612        for<'a> &'a M: Sub<M, Output = M>,
1613    {
1614        assert_eq!(ctx.nbatch(), 2);
1615        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1616        let one_batch = vec![f::<M>(1.0), f::<M>(2.0), f::<M>(3.0), f::<M>(4.0)];
1617        let two_batches = vec![
1618            f::<M>(10.0),
1619            f::<M>(20.0),
1620            f::<M>(30.0),
1621            f::<M>(40.0),
1622            f::<M>(50.0),
1623            f::<M>(60.0),
1624            f::<M>(70.0),
1625            f::<M>(80.0),
1626        ];
1627
1628        // lhs broadcasts over the batches of rhs, which already spans the result
1629        let a = M::try_from_triplets(2, 2, indices.clone(), one_batch.clone(), M::C::default())
1630            .unwrap();
1631        let b =
1632            M::try_from_triplets(2, 2, indices.clone(), two_batches.clone(), ctx.clone()).unwrap();
1633        let c = &a - b;
1634        assert_eq!(c.context().nbatch(), 2);
1635        assert_eq!(
1636            triplet_values(&c),
1637            vec![
1638                f::<M>(-9.0),
1639                f::<M>(-18.0),
1640                f::<M>(-27.0),
1641                f::<M>(-36.0),
1642                f::<M>(-49.0),
1643                f::<M>(-58.0),
1644                f::<M>(-67.0),
1645                f::<M>(-76.0),
1646            ]
1647        );
1648    }
1649
1650    /// The owned right-hand side governs, so a *wider* borrowed left-hand side is an error.
1651    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1652    pub fn test_batched_owned_rhs_narrow_m<M>(ctx: M::C)
1653    where
1654        M: Matrix + DenseMatrix,
1655        for<'a> &'a M: Sub<M, Output = M>,
1656    {
1657        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1658        let wide = (1..=8).map(|i| f::<M>(i as f64)).collect::<Vec<_>>();
1659        let narrow = (1..=4).map(|i| f::<M>(i as f64)).collect::<Vec<_>>();
1660        let a = M::try_from_triplets(2, 2, indices.clone(), wide, ctx).unwrap();
1661        let b = M::try_from_triplets(2, 2, indices, narrow, M::C::default()).unwrap();
1662        let _ = &a - b;
1663    }
1664
1665    /// The owned left-hand side governs and is written into.
1666    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1667    pub fn test_batched_owned_lhs_broadcast_m<M>(ctx: M::C)
1668    where
1669        M: Matrix + for<'a> Sub<&'a M, Output = M>,
1670    {
1671        assert_eq!(ctx.nbatch(), 2);
1672        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1673        let one_batch = vec![f::<M>(1.0), f::<M>(2.0), f::<M>(3.0), f::<M>(4.0)];
1674        let two_batches = vec![
1675            f::<M>(10.0),
1676            f::<M>(20.0),
1677            f::<M>(30.0),
1678            f::<M>(40.0),
1679            f::<M>(50.0),
1680            f::<M>(60.0),
1681            f::<M>(70.0),
1682            f::<M>(80.0),
1683        ];
1684
1685        // rhs broadcasts over the batches of lhs
1686        let a =
1687            M::try_from_triplets(2, 2, indices.clone(), two_batches.clone(), ctx.clone()).unwrap();
1688        let b = M::try_from_triplets(2, 2, indices.clone(), one_batch.clone(), M::C::default())
1689            .unwrap();
1690        let c = a - &b;
1691        assert_eq!(c.context().nbatch(), 2);
1692        assert_eq!(
1693            triplet_values(&c),
1694            vec![
1695                f::<M>(9.0),
1696                f::<M>(18.0),
1697                f::<M>(27.0),
1698                f::<M>(36.0),
1699                f::<M>(49.0),
1700                f::<M>(58.0),
1701                f::<M>(67.0),
1702                f::<M>(76.0),
1703            ]
1704        );
1705    }
1706
1707    /// The owned left-hand side governs, so a *wider* right-hand side is an error.
1708    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1709    pub fn test_batched_owned_lhs_narrow_m<M>(ctx: M::C)
1710    where
1711        M: Matrix + for<'a> Sub<&'a M, Output = M>,
1712    {
1713        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1714        let narrow = (1..=4).map(|i| f::<M>(i as f64)).collect::<Vec<_>>();
1715        let wide = (1..=8).map(|i| f::<M>(10.0 * i as f64)).collect::<Vec<_>>();
1716        let a = M::try_from_triplets(2, 2, indices.clone(), narrow, M::C::default()).unwrap();
1717        let b = M::try_from_triplets(2, 2, indices, wide, ctx).unwrap();
1718        let _ = a - &b;
1719    }
1720
1721    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1722    pub fn test_batched_scale_add_and_assign_broadcast_m<M: Matrix>(ctx: M::C) {
1723        assert_eq!(ctx.nbatch(), 2);
1724        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1725        let x_vals = vec![f::<M>(1.0), f::<M>(2.0), f::<M>(3.0), f::<M>(4.0)];
1726        let y_vals = vec![f::<M>(10.0), f::<M>(20.0), f::<M>(30.0), f::<M>(40.0)];
1727        // x and y have nbatch == 1, so they broadcast over both batches of result
1728        let x =
1729            M::try_from_triplets(2, 2, indices.clone(), x_vals.clone(), M::C::default()).unwrap();
1730        let y = M::try_from_triplets(2, 2, indices.clone(), y_vals, M::C::default()).unwrap();
1731        // result carries the union sparsity (required for sparse matrices) in both batches
1732        let mut result_vals = x_vals.clone();
1733        result_vals.extend(x_vals);
1734        let mut result = M::try_from_triplets(2, 2, indices, result_vals, ctx).unwrap();
1735        result.scale_add_and_assign(&x, f::<M>(2.0), &y);
1736        let vals = triplet_values(&result);
1737        assert_eq!(
1738            vals,
1739            vec![
1740                f::<M>(21.0),
1741                f::<M>(42.0),
1742                f::<M>(63.0),
1743                f::<M>(84.0),
1744                f::<M>(21.0),
1745                f::<M>(42.0),
1746                f::<M>(63.0),
1747                f::<M>(84.0),
1748            ]
1749        );
1750    }
1751
1752    /// Scaling through a reference is a separate `impl` from the owned one, and it walks the
1753    /// batches itself.
1754    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1755    pub fn test_batched_mul_scalar_ref_m<M>(ctx: M::C)
1756    where
1757        M: Matrix,
1758        for<'a> &'a M: Mul<Scale<M::T>, Output = M>,
1759    {
1760        assert_eq!(ctx.nbatch(), 2);
1761        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1762        let values = (1..=8).map(|i| f::<M>(i as f64)).collect::<Vec<_>>();
1763        let a = M::try_from_triplets(2, 2, indices, values, ctx).unwrap();
1764        let result = &a * Scale(f::<M>(2.0));
1765        assert_eq!(result.context().nbatch(), 2);
1766        assert_eq!(
1767            triplet_values(&result),
1768            (1..=8).map(|i| f::<M>(2.0 * i as f64)).collect::<Vec<_>>()
1769        );
1770    }
1771
1772    /// `+=` with a narrower right-hand side: the equal-batch whole-matrix path cannot be
1773    /// used, so this is the only test of the per-batch assign loop.
1774    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1775    pub fn test_batched_add_assign_broadcast_m<M: DenseMatrix>(ctx: M::C) {
1776        assert_eq!(ctx.nbatch(), 2);
1777        let mut a = M::from_vec(
1778            2,
1779            2,
1780            (1..=8).map(|i| f::<M>(i as f64)).collect(),
1781            ctx.clone(),
1782        );
1783        let b = M::from_vec(
1784            2,
1785            2,
1786            (1..=4).map(|i| f::<M>(10.0 * i as f64)).collect(),
1787            M::C::default(),
1788        );
1789        a += &b;
1790        // batch 0: 1..4 + 10..40, batch 1: 5..8 + 10..40
1791        let expected = (1..=4)
1792            .map(|i| f::<M>(i as f64 + 10.0 * i as f64))
1793            .chain((5..=8).map(|i| f::<M>(i as f64 + 10.0 * (i - 4) as f64)))
1794            .collect::<Vec<_>>();
1795        assert_eq!(triplet_values(&a), expected);
1796    }
1797
1798    /// Owned left-hand side broadcast into a fresh allocation by `+` (the `-` direction is
1799    /// covered by [`test_batched_owned_lhs_broadcast_m`]).
1800    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1801    pub fn test_batched_owned_lhs_add_broadcast_m<M>(ctx: M::C)
1802    where
1803        M: Matrix + for<'a> Add<&'a M, Output = M>,
1804    {
1805        assert_eq!(ctx.nbatch(), 2);
1806        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1807        let narrow = M::try_from_triplets(
1808            2,
1809            2,
1810            indices.clone(),
1811            (1..=4).map(|i| f::<M>(i as f64)).collect(),
1812            M::C::default(),
1813        )
1814        .unwrap();
1815        let wide = M::try_from_triplets(
1816            2,
1817            2,
1818            indices,
1819            (1..=8).map(|i| f::<M>(10.0 * i as f64)).collect(),
1820            ctx,
1821        )
1822        .unwrap();
1823        // the owned left-hand side governs, so it is the wide one and `narrow` broadcasts in
1824        let c = wide + &narrow;
1825        assert_eq!(c.context().nbatch(), 2);
1826        let expected = (1..=4)
1827            .map(|i| f::<M>(10.0 * i as f64 + i as f64))
1828            .chain((5..=8).map(|i| f::<M>(10.0 * i as f64 + (i - 4) as f64)))
1829            .collect::<Vec<_>>();
1830        assert_eq!(triplet_values(&c), expected);
1831    }
1832
1833    /// `gemv` with `beta` neither 0 nor 1, which is the only path that rescales `y` before
1834    /// accumulating.
1835    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1836    pub fn test_batched_gemv_beta_m<M: Matrix>(ctx: M::C) {
1837        assert_eq!(ctx.nbatch(), 2);
1838        // batch 0 = I, batch 1 = 2I
1839        let indices = vec![(0, 0), (1, 1)];
1840        let values = vec![f::<M>(1.0), f::<M>(1.0), f::<M>(2.0), f::<M>(2.0)];
1841        let a = M::try_from_triplets(2, 2, indices, values, ctx.clone()).unwrap();
1842        let x = M::V::from_vec(
1843            vec![f::<M>(1.0), f::<M>(2.0), f::<M>(3.0), f::<M>(4.0)],
1844            ctx.clone(),
1845        );
1846        let mut y = M::V::from_vec(
1847            vec![f::<M>(1.0), f::<M>(1.0), f::<M>(1.0), f::<M>(1.0)],
1848            ctx,
1849        );
1850        // y = 1 * A * x + 2 * y
1851        a.gemv(f::<M>(1.0), &x, f::<M>(2.0), &mut y);
1852        assert_eq!(
1853            y.clone_as_vec(),
1854            vec![f::<M>(3.0), f::<M>(4.0), f::<M>(8.0), f::<M>(10.0)]
1855        );
1856    }
1857
1858    // --- Grouped broadcast tests: `B` batches feeding `B * P` batches ---
1859    //
1860    // Each group holds a distinct value, so a cyclic mapping (source order `[0, 1, 0, 1]`)
1861    // fails these — grouped broadcasting reads `[0, 0, 1, 1]`.
1862
1863    /// `nbatch = 2` widened to 4, so each source batch covers two contiguous destinations.
1864    fn ctx4<M: Matrix>(ctx2: &M::C) -> M::C {
1865        assert_eq!(ctx2.nbatch(), 2);
1866        ctx2.clone_with_nbatch(4).unwrap()
1867    }
1868
1869    /// `y` carries 4 batches while the matrix carries 2, so each factor of the matrix serves
1870    /// two contiguous right-hand sides.
1871    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1872    pub fn test_grouped_gemv_m<M: Matrix>(ctx2: M::C) {
1873        let wide = ctx4::<M>(&ctx2);
1874        // batch 0 = I, batch 1 = 2I
1875        let indices = vec![(0, 0), (1, 1)];
1876        let values = vec![f::<M>(1.0), f::<M>(1.0), f::<M>(2.0), f::<M>(2.0)];
1877        let a = M::try_from_triplets(2, 2, indices, values, ctx2).unwrap();
1878        let x = M::V::from_vec((1..=8).map(|i| f::<M>(i as f64)).collect(), wide.clone());
1879        let mut y = M::V::zeros(2, wide);
1880        a.gemv(f::<M>(1.0), &x, f::<M>(0.0), &mut y);
1881        assert_eq!(
1882            y.clone_as_vec(),
1883            vec![
1884                f::<M>(1.0),
1885                f::<M>(2.0),
1886                f::<M>(3.0),
1887                f::<M>(4.0),
1888                f::<M>(10.0),
1889                f::<M>(12.0),
1890                f::<M>(14.0),
1891                f::<M>(16.0),
1892            ]
1893        );
1894    }
1895
1896    /// The mirror of [`test_grouped_gemv_m`]: the matrix holds one lane per right-hand side
1897    /// while `x` carries 2, so each `x` lane serves two contiguous results — which still
1898    /// multiply different matrix lanes.
1899    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1900    pub fn test_grouped_gemv_x_m<M: Matrix>(ctx2: M::C) {
1901        let wide = ctx4::<M>(&ctx2);
1902        // batch b = (b + 1) I
1903        let indices = vec![(0, 0), (1, 1)];
1904        let values = (1..=4).flat_map(|i| [f::<M>(i as f64); 2]).collect();
1905        let a = M::try_from_triplets(2, 2, indices, values, wide.clone()).unwrap();
1906        let x = M::V::from_vec((1..=4).map(|i| f::<M>(i as f64)).collect(), ctx2);
1907        let mut y = M::V::zeros(2, wide);
1908        a.gemv(f::<M>(1.0), &x, f::<M>(0.0), &mut y);
1909        assert_eq!(
1910            y.clone_as_vec(),
1911            vec![
1912                f::<M>(1.0),
1913                f::<M>(2.0),
1914                f::<M>(2.0),
1915                f::<M>(4.0),
1916                f::<M>(9.0),
1917                f::<M>(12.0),
1918                f::<M>(12.0),
1919                f::<M>(16.0),
1920            ]
1921        );
1922    }
1923
1924    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1925    pub fn test_grouped_copy_from_m<M: Matrix>(ctx2: M::C) {
1926        let wide = ctx4::<M>(&ctx2);
1927        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1928        let values = (1..=8).map(|i| f::<M>(i as f64)).collect::<Vec<_>>();
1929        let a = M::try_from_triplets(2, 2, indices, values, ctx2).unwrap();
1930        let mut b = M::zeros(2, 2, wide);
1931        b.copy_from(&a);
1932        let group0 = (1..=4).map(|i| f::<M>(i as f64));
1933        let group1 = (5..=8).map(|i| f::<M>(i as f64));
1934        let expected = group0
1935            .clone()
1936            .chain(group0)
1937            .chain(group1.clone())
1938            .chain(group1)
1939            .collect::<Vec<_>>();
1940        assert_eq!(triplet_values(&b), expected);
1941    }
1942
1943    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1944    pub fn test_grouped_scale_add_and_assign_m<M: Matrix>(ctx2: M::C) {
1945        let wide = ctx4::<M>(&ctx2);
1946        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1947        let x_vals = (1..=8).map(|i| f::<M>(i as f64)).collect::<Vec<_>>();
1948        let y_vals = (1..=8).map(|i| f::<M>(10.0 * i as f64)).collect::<Vec<_>>();
1949        let x = M::try_from_triplets(2, 2, indices.clone(), x_vals, ctx2.clone()).unwrap();
1950        let y = M::try_from_triplets(2, 2, indices.clone(), y_vals, ctx2).unwrap();
1951        // the destination needs the union sparsity in every batch (required for sparse)
1952        let dst_vals = (0..16).map(|_| f::<M>(0.0)).collect::<Vec<_>>();
1953        let mut result = M::try_from_triplets(2, 2, indices, dst_vals, wide).unwrap();
1954        result.scale_add_and_assign(&x, f::<M>(2.0), &y);
1955        // x_i + 2 * y_i = 21 * i, with each source batch covering two contiguous batches
1956        let group0 = (1..=4).map(|i| f::<M>(21.0 * i as f64));
1957        let group1 = (5..=8).map(|i| f::<M>(21.0 * i as f64));
1958        let expected = group0
1959            .clone()
1960            .chain(group0)
1961            .chain(group1.clone())
1962            .chain(group1)
1963            .collect::<Vec<_>>();
1964        assert_eq!(triplet_values(&result), expected);
1965    }
1966
1967    /// The grouped flavour of [`test_batched_add_assign_broadcast_m`].
1968    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1969    pub fn test_grouped_add_assign_m<M: DenseMatrix>(ctx2: M::C) {
1970        let wide = ctx4::<M>(&ctx2);
1971        let mut a = M::from_vec(2, 2, (0..16).map(|_| f::<M>(0.0)).collect(), wide);
1972        let b = M::from_vec(2, 2, (1..=8).map(|i| f::<M>(i as f64)).collect(), ctx2);
1973        a += &b;
1974        let group0 = (1..=4).map(|i| f::<M>(i as f64));
1975        let group1 = (5..=8).map(|i| f::<M>(i as f64));
1976        let expected = group0
1977            .clone()
1978            .chain(group0)
1979            .chain(group1.clone())
1980            .chain(group1)
1981            .collect::<Vec<_>>();
1982        assert_eq!(triplet_values(&a), expected);
1983    }
1984
1985    /// The grouped flavour of [`test_batched_owned_lhs_add_broadcast_m`].
1986    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
1987    pub fn test_grouped_owned_lhs_add_m<M>(ctx2: M::C)
1988    where
1989        M: Matrix + for<'a> Add<&'a M, Output = M>,
1990    {
1991        let wide_ctx = ctx4::<M>(&ctx2);
1992        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
1993        let narrow = M::try_from_triplets(
1994            2,
1995            2,
1996            indices.clone(),
1997            (1..=8).map(|i| f::<M>(i as f64)).collect(),
1998            ctx2,
1999        )
2000        .unwrap();
2001        let wide = M::try_from_triplets(
2002            2,
2003            2,
2004            indices,
2005            (0..16).map(|_| f::<M>(0.0)).collect(),
2006            wide_ctx,
2007        )
2008        .unwrap();
2009        // the owned left-hand side governs, so the wide matrix is the owned one and the
2010        // narrow one broadcasts into its groups
2011        let c = wide + &narrow;
2012        assert_eq!(c.context().nbatch(), 4);
2013        let group0 = (1..=4).map(|i| f::<M>(i as f64));
2014        let group1 = (5..=8).map(|i| f::<M>(i as f64));
2015        let expected = group0
2016            .clone()
2017            .chain(group0)
2018            .chain(group1.clone())
2019            .chain(group1)
2020            .collect::<Vec<_>>();
2021        assert_eq!(triplet_values(&c), expected);
2022    }
2023
2024    /// The grouped flavour of [`test_batched_owned_lhs_narrow_m`].
2025    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2026    pub fn test_grouped_owned_lhs_narrow_m<M>(ctx2: M::C)
2027    where
2028        M: Matrix + for<'a> Add<&'a M, Output = M>,
2029    {
2030        let wide_ctx = ctx4::<M>(&ctx2);
2031        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
2032        let narrow = M::try_from_triplets(
2033            2,
2034            2,
2035            indices.clone(),
2036            (1..=8).map(|i| f::<M>(i as f64)).collect(),
2037            ctx2,
2038        )
2039        .unwrap();
2040        let wide = M::try_from_triplets(
2041            2,
2042            2,
2043            indices,
2044            (0..16).map(|_| f::<M>(0.0)).collect(),
2045            wide_ctx,
2046        )
2047        .unwrap();
2048        let _ = narrow + &wide;
2049    }
2050
2051    // --- Narrow-destination tests: a wider source would silently drop batches ---
2052
2053    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2054    pub fn test_narrow_dest_gemv_m<M: Matrix>(ctx2: M::C) {
2055        let wide = ctx4::<M>(&ctx2);
2056        let indices = vec![(0, 0), (1, 1)];
2057        let values = (1..=8).map(|i| f::<M>(i as f64)).collect::<Vec<_>>();
2058        let a = M::try_from_triplets(2, 2, indices, values, wide.clone()).unwrap();
2059        let x = M::V::zeros(2, wide);
2060        let mut y = M::V::zeros(2, ctx2);
2061        a.gemv(f::<M>(1.0), &x, f::<M>(0.0), &mut y);
2062    }
2063
2064    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2065    pub fn test_narrow_dest_copy_from_m<M: Matrix>(ctx2: M::C) {
2066        let wide = ctx4::<M>(&ctx2);
2067        let indices = vec![(0, 0), (1, 1)];
2068        let values = (1..=8).map(|i| f::<M>(i as f64)).collect::<Vec<_>>();
2069        let a = M::try_from_triplets(2, 2, indices, values, wide).unwrap();
2070        let mut b = M::zeros(2, 2, ctx2);
2071        b.copy_from(&a);
2072    }
2073
2074    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2075    pub fn test_narrow_dest_scale_add_m<M: Matrix>(ctx2: M::C) {
2076        let wide = ctx4::<M>(&ctx2);
2077        let indices = vec![(0, 0), (1, 1)];
2078        let vals = (1..=8).map(|i| f::<M>(i as f64)).collect::<Vec<_>>();
2079        let x = M::try_from_triplets(2, 2, indices.clone(), vals.clone(), wide.clone()).unwrap();
2080        let y = M::try_from_triplets(2, 2, indices.clone(), vals, wide).unwrap();
2081        let dst = (1..=4).map(|i| f::<M>(i as f64)).collect::<Vec<_>>();
2082        let mut result = M::try_from_triplets(2, 2, indices, dst, ctx2).unwrap();
2083        result.scale_add_and_assign(&x, f::<M>(2.0), &y);
2084    }
2085
2086    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2087    pub fn test_narrow_dest_gather_m<M: Matrix>(ctx2: M::C) {
2088        let wide = ctx4::<M>(&ctx2);
2089        let indices = vec![(0, 0), (1, 1)];
2090        let values = (1..=8).map(|i| f::<M>(i as f64)).collect::<Vec<_>>();
2091        let src = M::try_from_triplets(2, 2, indices.clone(), values, wide).unwrap();
2092        let zeros = (0..4).map(|_| f::<M>(0.0)).collect::<Vec<_>>();
2093        let mut dst = M::try_from_triplets(2, 2, indices, zeros, ctx2).unwrap();
2094        let gather_indices = <M::V as Vector>::Index::from_vec(vec![0, 1], Default::default());
2095        dst.gather(&src, &gather_indices);
2096    }
2097
2098    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2099    pub fn test_narrow_dest_set_data_with_indices_m<M: Matrix>(ctx2: M::C) {
2100        let wide = ctx4::<M>(&ctx2);
2101        let indices = vec![(0, 0), (1, 1)];
2102        let zeros = (0..4).map(|_| f::<M>(0.0)).collect::<Vec<_>>();
2103        let mut mat = M::try_from_triplets(2, 2, indices, zeros, ctx2).unwrap();
2104        let dst_indices = <M::V as Vector>::Index::from_vec(vec![0, 1], Default::default());
2105        let src_indices = <M::V as Vector>::Index::from_vec(vec![0, 1], Default::default());
2106        let data = M::V::from_vec((1..=8).map(|i| f::<M>(i as f64)).collect(), wide);
2107        mat.set_data_with_indices(&dst_indices, &src_indices, &data);
2108    }
2109
2110    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2111    pub fn test_narrow_dest_add_assign_m<M: DenseMatrix>(ctx2: M::C) {
2112        let wide = ctx4::<M>(&ctx2);
2113        let src = M::from_vec(2, 2, (0..16).map(|_| f::<M>(1.0)).collect(), wide);
2114        let mut dst = M::zeros(2, 2, ctx2);
2115        dst += &src;
2116    }
2117
2118    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2119    pub fn test_narrow_dest_gemv_cols_m<M: DenseMatrix>(ctx2: M::C) {
2120        let wide = ctx4::<M>(&ctx2);
2121        let a = M::from_vec(2, 2, (0..16).map(|_| f::<M>(1.0)).collect(), wide);
2122        let mut y = M::V::zeros(2, ctx2);
2123        let ones = vec![f::<M>(1.0), f::<M>(1.0)];
2124        a.gemv_cols(0, 2, f::<M>(1.0), &ones, f::<M>(0.0), &mut y);
2125    }
2126
2127    /// `gemv_cols` shares its coefficients across batches, so only the matrix broadcasts.
2128    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2129    pub fn test_grouped_gemv_cols_m<M: DenseMatrix>(ctx2: M::C) {
2130        let wide = ctx4::<M>(&ctx2);
2131        // batch 0 columns: [1, 1], [0, 0]; batch 1 columns: [2, 2], [0, 0]
2132        let a = M::from_vec(
2133            2,
2134            2,
2135            vec![
2136                f::<M>(1.0),
2137                f::<M>(1.0),
2138                f::<M>(0.0),
2139                f::<M>(0.0),
2140                f::<M>(2.0),
2141                f::<M>(2.0),
2142                f::<M>(0.0),
2143                f::<M>(0.0),
2144            ],
2145            ctx2,
2146        );
2147        let mut y = M::V::zeros(2, wide);
2148        let ones = vec![f::<M>(1.0), f::<M>(1.0)];
2149        a.gemv_cols(0, 2, f::<M>(1.0), &ones, f::<M>(0.0), &mut y);
2150        assert_eq!(
2151            y.clone_as_vec(),
2152            vec![
2153                f::<M>(1.0),
2154                f::<M>(1.0),
2155                f::<M>(1.0),
2156                f::<M>(1.0),
2157                f::<M>(2.0),
2158                f::<M>(2.0),
2159                f::<M>(2.0),
2160                f::<M>(2.0),
2161            ]
2162        );
2163    }
2164
2165    // --- Batched DenseMatrix-specific tests ---
2166
2167    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2168    pub fn test_batched_set_index_batch<M: DenseMatrix>(ctx: M::C) {
2169        assert_eq!(ctx.nbatch(), 2);
2170        // 2x2 matrix, nbatch=2: every (batch, row, col) gets a value that identifies it
2171        let mut a = M::zeros(2, 2, ctx);
2172        let val = |b: usize, i: usize, j: usize| f::<M>((100 * b + 10 * i + j) as f64);
2173        for b in 0..2 {
2174            for i in 0..2 {
2175                for j in 0..2 {
2176                    a.set_index_batch(b, i, j, val(b, i, j));
2177                }
2178            }
2179        }
2180        for b in 0..2 {
2181            for i in 0..2 {
2182                for j in 0..2 {
2183                    assert_eq!(mat_at(&a, b, i, j), val(b, i, j), "batch {b}, ({i}, {j})");
2184                }
2185            }
2186        }
2187    }
2188
2189    pub fn test_batched_from_vec<M: DenseMatrix>(ctx: M::C) {
2190        assert_eq!(ctx.nbatch(), 2);
2191        // 2x2 matrix, nbatch=2: physical is 2x4
2192        // batch0: col0=[1,3], col1=[2,4]; batch1: col0=[5,7], col1=[6,8]
2193        let a = M::from_vec(
2194            2,
2195            2,
2196            vec![
2197                f::<M>(1.0),
2198                f::<M>(3.0),
2199                f::<M>(2.0),
2200                f::<M>(4.0),
2201                f::<M>(5.0),
2202                f::<M>(7.0),
2203                f::<M>(6.0),
2204                f::<M>(8.0),
2205            ],
2206            ctx,
2207        );
2208        assert_eq!(a.nrows(), 2);
2209        assert_eq!(a.ncols(), 2);
2210        assert_eq!(at(&a, 0, 0, 0), f::<M>(1.0));
2211        assert_eq!(at(&a, 0, 1, 0), f::<M>(3.0));
2212        assert_eq!(at(&a, 0, 0, 1), f::<M>(2.0));
2213        assert_eq!(at(&a, 0, 1, 1), f::<M>(4.0));
2214        assert_eq!(at(&a, 1, 0, 0), f::<M>(5.0));
2215        assert_eq!(at(&a, 1, 1, 0), f::<M>(7.0));
2216        assert_eq!(at(&a, 1, 0, 1), f::<M>(6.0));
2217        assert_eq!(at(&a, 1, 1, 1), f::<M>(8.0));
2218    }
2219
2220    // --- Broadcasting tests ---
2221
2222    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2223    pub fn test_batched_gemv_o_broadcast_x<M: DenseMatrix>(ctx: M::C) {
2224        assert_eq!(ctx.nbatch(), 2);
2225        // 2x3 diff matrix, nbatch=2
2226        let diff = M::from_vec(
2227            2,
2228            3,
2229            vec![
2230                f::<M>(1.0),
2231                f::<M>(4.0),
2232                f::<M>(2.0),
2233                f::<M>(5.0),
2234                f::<M>(3.0),
2235                f::<M>(6.0),
2236                f::<M>(7.0),
2237                f::<M>(10.0),
2238                f::<M>(8.0),
2239                f::<M>(11.0),
2240                f::<M>(9.0),
2241                f::<M>(12.0),
2242            ],
2243            ctx.clone(),
2244        );
2245        // x with nbatch=1, length=2 (broadcast)
2246        let x = vec![f::<M>(1.0), f::<M>(1.0)];
2247        let mut y = M::V::zeros(2, ctx);
2248        diff.gemv_cols(0, 2, f::<M>(1.0), &x, f::<M>(0.0), &mut y);
2249        // batch0: [[1,2],[4,5]] * [1,1] = [3, 9]
2250        // batch1: [[7,8],[10,11]] * [1,1] = [15, 21]
2251        assert_eq!(
2252            y.clone_as_vec(),
2253            vec![f::<M>(3.0), f::<M>(9.0), f::<M>(15.0), f::<M>(21.0)]
2254        );
2255    }
2256
2257    // --- Incompatible batch tests ---
2258
2259    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2260    pub fn test_batched_gemv_incompatible<M: DenseMatrix>(ctx2: M::C, ctx3: M::C) {
2261        assert_eq!(ctx2.nbatch(), 2);
2262        assert_eq!(ctx3.nbatch(), 3);
2263        let a = M::zeros(2, 2, ctx2.clone());
2264        let x = M::V::zeros(2, ctx3);
2265        let mut y = M::V::zeros(2, ctx2);
2266        a.gemv(f::<M>(1.0), &x, f::<M>(0.0), &mut y);
2267    }
2268
2269    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2270    pub fn test_batched_resize_cols<M: DenseMatrix>(ctx: M::C) {
2271        assert_eq!(ctx.nbatch(), 2);
2272        // 2x2, nbatch=2: batch0=[[1,2],[3,4]], batch1=[[5,6],[7,8]]
2273        let mut a = M::from_vec(
2274            2,
2275            2,
2276            vec![
2277                f::<M>(1.0),
2278                f::<M>(3.0),
2279                f::<M>(2.0),
2280                f::<M>(4.0),
2281                f::<M>(5.0),
2282                f::<M>(7.0),
2283                f::<M>(6.0),
2284                f::<M>(8.0),
2285            ],
2286            ctx.clone(),
2287        );
2288        // grow to 3 columns
2289        a.resize_cols(3);
2290        assert_eq!(a.ncols(), 3);
2291        assert_eq!(a.nrows(), 2);
2292        // existing data preserved per batch
2293        assert_eq!(at(&a, 0, 0, 0), f::<M>(1.0));
2294        assert_eq!(at(&a, 0, 1, 0), f::<M>(3.0));
2295        assert_eq!(at(&a, 0, 0, 1), f::<M>(2.0));
2296        assert_eq!(at(&a, 0, 1, 1), f::<M>(4.0));
2297        assert_eq!(at(&a, 1, 0, 0), f::<M>(5.0));
2298        assert_eq!(at(&a, 1, 1, 0), f::<M>(7.0));
2299        assert_eq!(at(&a, 1, 0, 1), f::<M>(6.0));
2300        assert_eq!(at(&a, 1, 1, 1), f::<M>(8.0));
2301        // new column is zero in every batch
2302        assert_eq!(at(&a, 0, 0, 2), f::<M>(0.0));
2303        assert_eq!(at(&a, 0, 1, 2), f::<M>(0.0));
2304        assert_eq!(at(&a, 1, 0, 2), f::<M>(0.0));
2305        assert_eq!(at(&a, 1, 1, 2), f::<M>(0.0));
2306        // verify via gemv that batch 1 data is intact
2307        let x = M::V::from_vec(
2308            vec![
2309                f::<M>(1.0),
2310                f::<M>(0.0),
2311                f::<M>(0.0),
2312                f::<M>(1.0),
2313                f::<M>(0.0),
2314                f::<M>(0.0),
2315            ],
2316            ctx.clone(),
2317        );
2318        let mut y = M::V::zeros(2, ctx.clone());
2319        a.gemv(f::<M>(1.0), &x, f::<M>(0.0), &mut y);
2320        // batch0: col0=[1,3], x=[1,0,0] → [1,3]
2321        // batch1: col0=[5,7], x=[1,0,0] → [5,7]
2322        assert_eq!(
2323            y.clone_as_vec(),
2324            vec![f::<M>(1.0), f::<M>(3.0), f::<M>(5.0), f::<M>(7.0)]
2325        );
2326
2327        // shrink to 1 column
2328        a.resize_cols(1);
2329        assert_eq!(a.ncols(), 1);
2330        assert_eq!(at(&a, 0, 0, 0), f::<M>(1.0));
2331        assert_eq!(at(&a, 0, 1, 0), f::<M>(3.0));
2332        assert_eq!(at(&a, 1, 0, 0), f::<M>(5.0));
2333        assert_eq!(at(&a, 1, 1, 0), f::<M>(7.0));
2334        // verify batch1 col0 via gemv
2335        let x2 = M::V::from_vec(vec![f::<M>(1.0), f::<M>(1.0)], ctx.clone());
2336        let mut y2 = M::V::zeros(2, ctx);
2337        a.gemv(f::<M>(1.0), &x2, f::<M>(0.0), &mut y2);
2338        // batch0: [[1],[3]] * [1] = [1,3], batch1: [[5],[7]] * [1] = [5,7]
2339        assert_eq!(
2340            y2.clone_as_vec(),
2341            vec![f::<M>(1.0), f::<M>(3.0), f::<M>(5.0), f::<M>(7.0)]
2342        );
2343    }
2344
2345    // --- New unbatched Matrix-generic tests ---
2346
2347    pub fn test_mul_scalar<M: Matrix>() {
2348        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
2349        let values = vec![f::<M>(1.0), f::<M>(3.0), f::<M>(2.0), f::<M>(4.0)];
2350        let a = M::try_from_triplets(2, 2, indices, values, Default::default()).unwrap();
2351        let result = a * Scale(f::<M>(2.0));
2352        let (_, vals) = result.triplet_iter();
2353        let vals: Vec<_> = vals.collect();
2354        assert_eq!(
2355            vals,
2356            vec![f::<M>(2.0), f::<M>(6.0), f::<M>(4.0), f::<M>(8.0)]
2357        );
2358    }
2359
2360    pub fn test_add_column_to_vector<M: Matrix>() {
2361        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
2362        let values = vec![f::<M>(1.0), f::<M>(2.0), f::<M>(3.0), f::<M>(4.0)];
2363        let mat = M::try_from_triplets(2, 2, indices, values, Default::default()).unwrap();
2364        let mut v = M::V::zeros(2, Default::default());
2365        mat.add_column_to_vector(1, &mut v);
2366        assert_eq!(v.clone_as_vec(), vec![f::<M>(3.0), f::<M>(4.0)]);
2367    }
2368
2369    // --- New unbatched DenseMatrix-specific tests ---
2370
2371    pub fn test_add<M: DenseMatrix>() {
2372        let a = M::from_vec(
2373            2,
2374            2,
2375            vec![f::<M>(1.0), f::<M>(3.0), f::<M>(2.0), f::<M>(4.0)],
2376            Default::default(),
2377        );
2378        let b = M::from_vec(
2379            2,
2380            2,
2381            vec![f::<M>(5.0), f::<M>(7.0), f::<M>(6.0), f::<M>(8.0)],
2382            Default::default(),
2383        );
2384        let result = a + &b;
2385        assert_eq!(result.get_index(0, 0), f::<M>(6.0));
2386        assert_eq!(result.get_index(1, 1), f::<M>(12.0));
2387    }
2388
2389    pub fn test_sub<M: DenseMatrix>() {
2390        let a = M::from_vec(
2391            2,
2392            2,
2393            vec![f::<M>(5.0), f::<M>(7.0), f::<M>(6.0), f::<M>(8.0)],
2394            Default::default(),
2395        );
2396        let b = M::from_vec(
2397            2,
2398            2,
2399            vec![f::<M>(1.0), f::<M>(3.0), f::<M>(2.0), f::<M>(4.0)],
2400            Default::default(),
2401        );
2402        let result = a - &b;
2403        assert_eq!(result.get_index(0, 0), f::<M>(4.0));
2404        assert_eq!(result.get_index(1, 1), f::<M>(4.0));
2405    }
2406
2407    pub fn test_add_assign<M: DenseMatrix>() {
2408        let mut a = M::from_vec(
2409            2,
2410            2,
2411            vec![f::<M>(1.0), f::<M>(3.0), f::<M>(2.0), f::<M>(4.0)],
2412            Default::default(),
2413        );
2414        let b = M::from_vec(
2415            2,
2416            2,
2417            vec![f::<M>(5.0), f::<M>(7.0), f::<M>(6.0), f::<M>(8.0)],
2418            Default::default(),
2419        );
2420        a += &b;
2421        assert_eq!(a.get_index(0, 0), f::<M>(6.0));
2422        assert_eq!(a.get_index(1, 1), f::<M>(12.0));
2423    }
2424
2425    pub fn test_sub_assign<M: DenseMatrix>() {
2426        let mut a = M::from_vec(
2427            2,
2428            2,
2429            vec![f::<M>(5.0), f::<M>(7.0), f::<M>(6.0), f::<M>(8.0)],
2430            Default::default(),
2431        );
2432        let b = M::from_vec(
2433            2,
2434            2,
2435            vec![f::<M>(1.0), f::<M>(3.0), f::<M>(2.0), f::<M>(4.0)],
2436            Default::default(),
2437        );
2438        a -= &b;
2439        assert_eq!(a.get_index(0, 0), f::<M>(4.0));
2440        assert_eq!(a.get_index(1, 1), f::<M>(4.0));
2441    }
2442
2443    /// `MatrixCommon::inner_mut` on an owned matrix; `test_matrix_common_by_ref` only reaches
2444    /// the generic `&M`/`&mut M` blanket impls.
2445    pub fn test_inner_mut<M: DenseMatrix>() {
2446        let mut a = M::zeros(2, 2, Default::default());
2447        let _ = a.inner_mut();
2448    }
2449
2450    /// `column_mut` on an unbatched (nbatch == 1) matrix: existing coverage only calls it through
2451    /// batched helpers, which take a different code path than the single-batch fast path.
2452    pub fn test_column_mut<M: DenseMatrix>() {
2453        let mut a = M::from_vec(
2454            2,
2455            2,
2456            vec![f::<M>(1.0), f::<M>(3.0), f::<M>(2.0), f::<M>(4.0)],
2457            Default::default(),
2458        );
2459        a.column_mut(1).set_index(0, f::<M>(20.0));
2460        assert_eq!(a.get_index(0, 1), f::<M>(20.0));
2461        assert_eq!(a.get_index(1, 1), f::<M>(4.0));
2462    }
2463
2464    pub fn test_gather<M: DenseMatrix>() {
2465        let mat1 = M::from_vec(
2466            3,
2467            3,
2468            vec![
2469                f::<M>(1.0),
2470                f::<M>(2.0),
2471                f::<M>(3.0),
2472                f::<M>(4.0),
2473                f::<M>(5.0),
2474                f::<M>(6.0),
2475                f::<M>(7.0),
2476                f::<M>(8.0),
2477                f::<M>(9.0),
2478            ],
2479            Default::default(),
2480        );
2481        let mut mat2 = M::zeros(2, 2, Default::default());
2482        let indices = <M::V as Vector>::Index::from_vec(vec![0, 1, 3, 4], Default::default());
2483        mat2.gather(&mat1, &indices);
2484        assert_eq!(mat2.get_index(0, 0), f::<M>(1.0));
2485        assert_eq!(mat2.get_index(1, 0), f::<M>(2.0));
2486        assert_eq!(mat2.get_index(0, 1), f::<M>(4.0));
2487        assert_eq!(mat2.get_index(1, 1), f::<M>(5.0));
2488    }
2489
2490    pub fn test_set_data_with_indices<M: DenseMatrix>() {
2491        let mut mat = M::zeros(2, 2, Default::default());
2492        let dst_indices = <M::V as Vector>::Index::from_vec(vec![0, 3], Default::default());
2493        let src_indices = <M::V as Vector>::Index::from_vec(vec![0, 1], Default::default());
2494        let data = M::V::from_vec(vec![f::<M>(5.0), f::<M>(6.0)], Default::default());
2495        mat.set_data_with_indices(&dst_indices, &src_indices, &data);
2496        assert_eq!(mat.get_index(0, 0), f::<M>(5.0));
2497        assert_eq!(mat.get_index(1, 1), f::<M>(6.0));
2498    }
2499
2500    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2501    pub fn test_batched_combine<M: DenseMatrix>(ctx: M::C) {
2502        assert_eq!(ctx.nbatch(), 2);
2503        #[rustfmt::skip]
2504        let data: Vec<M::T> = vec![
2505            // batch 0: 4x4 column-major (cols 0-3)
2506            f::<M>(1.0), f::<M>(2.0), f::<M>(3.0), f::<M>(4.0),
2507            f::<M>(5.0), f::<M>(6.0), f::<M>(7.0), f::<M>(8.0),
2508            f::<M>(9.0), f::<M>(10.0), f::<M>(11.0), f::<M>(12.0),
2509            f::<M>(13.0), f::<M>(14.0), f::<M>(15.0), f::<M>(16.0),
2510            // batch 1: 4x4 column-major (cols 0-3)
2511            f::<M>(101.0), f::<M>(102.0), f::<M>(103.0), f::<M>(104.0),
2512            f::<M>(105.0), f::<M>(106.0), f::<M>(107.0), f::<M>(108.0),
2513            f::<M>(109.0), f::<M>(110.0), f::<M>(111.0), f::<M>(112.0),
2514            f::<M>(113.0), f::<M>(114.0), f::<M>(115.0), f::<M>(116.0),
2515        ];
2516        let m = M::from_vec(4, 4, data, ctx.clone());
2517
2518        let alg_indices = <M::V as Vector>::Index::from_vec(vec![1, 3], Default::default());
2519        let [(ul, _), (ur, _), (ll, _), (lr, _)] = m.split(&alg_indices);
2520
2521        let recombined = M::combine(&ul, &ur, &ll, &lr, &alg_indices);
2522
2523        let (_orig_idx, orig_vals) = m.triplet_iter();
2524        let (_recom_idx, recom_vals) = recombined.triplet_iter();
2525        let orig_vals: Vec<_> = orig_vals.collect();
2526        let recom_vals: Vec<_> = recom_vals.collect();
2527        assert_eq!(orig_vals, recom_vals);
2528    }
2529
2530    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2531    pub fn test_batched_add_column_to_vector_m<M: Matrix>(ctx: M::C) {
2532        assert_eq!(ctx.nbatch(), 2);
2533        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
2534        let values = vec![
2535            f::<M>(1.0),
2536            f::<M>(2.0),
2537            f::<M>(3.0),
2538            f::<M>(4.0),
2539            f::<M>(5.0),
2540            f::<M>(6.0),
2541            f::<M>(7.0),
2542            f::<M>(8.0),
2543        ];
2544        let mat = M::try_from_triplets(2, 2, indices, values, ctx.clone()).unwrap();
2545        let mut v = M::V::zeros(2, ctx);
2546        mat.add_column_to_vector(1, &mut v);
2547        assert_eq!(
2548            v.clone_as_vec(),
2549            vec![f::<M>(3.0), f::<M>(4.0), f::<M>(7.0), f::<M>(8.0)]
2550        );
2551    }
2552
2553    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2554    pub fn test_batched_add_columns_to_batched_vector_m<M: Matrix>(ctx: M::C) {
2555        assert_eq!(ctx.nbatch(), 2);
2556        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
2557        let values = vec![
2558            f::<M>(1.0),
2559            f::<M>(2.0),
2560            f::<M>(3.0),
2561            f::<M>(4.0),
2562            f::<M>(5.0),
2563            f::<M>(6.0),
2564            f::<M>(7.0),
2565            f::<M>(8.0),
2566        ];
2567        let mat = M::try_from_triplets(2, 2, indices, values, ctx.clone()).unwrap();
2568        // one lane per (batch, column): lane `b * ncols + j` gets column `j` of batch `b`
2569        let ctx4 = ctx.clone_with_nbatch(4).unwrap();
2570        let mut v = M::V::zeros(2, ctx4);
2571        mat.add_columns_to_batched_vector(&mut v);
2572        assert_eq!(
2573            v.clone_as_vec(),
2574            vec![
2575                f::<M>(1.0),
2576                f::<M>(2.0),
2577                f::<M>(3.0),
2578                f::<M>(4.0),
2579                f::<M>(5.0),
2580                f::<M>(6.0),
2581                f::<M>(7.0),
2582                f::<M>(8.0)
2583            ]
2584        );
2585        // it accumulates, like its single-column sibling
2586        mat.add_columns_to_batched_vector(&mut v);
2587        assert_eq!(v.clone_as_vec()[0], f::<M>(2.0));
2588        assert_eq!(v.clone_as_vec()[7], f::<M>(16.0));
2589    }
2590
2591    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2592    pub fn test_batched_set_data_with_indices_m<M: Matrix>(ctx: M::C) {
2593        assert_eq!(ctx.nbatch(), 2);
2594        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
2595        let zero_values = vec![
2596            f::<M>(0.0),
2597            f::<M>(0.0),
2598            f::<M>(0.0),
2599            f::<M>(0.0),
2600            f::<M>(0.0),
2601            f::<M>(0.0),
2602            f::<M>(0.0),
2603            f::<M>(0.0),
2604        ];
2605        let mut mat = M::try_from_triplets(2, 2, indices, zero_values, ctx.clone()).unwrap();
2606        let dst_indices = <M::V as Vector>::Index::from_vec(vec![0, 3], Default::default());
2607        let src_indices = <M::V as Vector>::Index::from_vec(vec![0, 1], Default::default());
2608        let data = M::V::from_vec(
2609            vec![f::<M>(5.0), f::<M>(6.0), f::<M>(50.0), f::<M>(60.0)],
2610            ctx,
2611        );
2612        mat.set_data_with_indices(&dst_indices, &src_indices, &data);
2613        let (_, vals) = mat.triplet_iter();
2614        let vals: Vec<_> = vals.collect();
2615        assert_eq!(
2616            vals,
2617            vec![
2618                f::<M>(5.0),
2619                f::<M>(0.0),
2620                f::<M>(0.0),
2621                f::<M>(6.0),
2622                f::<M>(50.0),
2623                f::<M>(0.0),
2624                f::<M>(0.0),
2625                f::<M>(60.0),
2626            ]
2627        );
2628    }
2629
2630    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2631    pub fn test_batched_gather_m<M: Matrix>(ctx: M::C) {
2632        assert_eq!(ctx.nbatch(), 2);
2633        let indices: Vec<(IndexType, IndexType)> =
2634            (0..3).flat_map(|j| (0..3).map(move |i| (i, j))).collect();
2635        let values = vec![
2636            f::<M>(1.0),
2637            f::<M>(2.0),
2638            f::<M>(3.0),
2639            f::<M>(4.0),
2640            f::<M>(5.0),
2641            f::<M>(6.0),
2642            f::<M>(7.0),
2643            f::<M>(8.0),
2644            f::<M>(9.0),
2645            f::<M>(10.0),
2646            f::<M>(20.0),
2647            f::<M>(30.0),
2648            f::<M>(40.0),
2649            f::<M>(50.0),
2650            f::<M>(60.0),
2651            f::<M>(70.0),
2652            f::<M>(80.0),
2653            f::<M>(90.0),
2654        ];
2655        let mat1 = M::try_from_triplets(3, 3, indices, values, ctx.clone()).unwrap();
2656        let dest_indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
2657        let zero_values = vec![
2658            f::<M>(0.0),
2659            f::<M>(0.0),
2660            f::<M>(0.0),
2661            f::<M>(0.0),
2662            f::<M>(0.0),
2663            f::<M>(0.0),
2664            f::<M>(0.0),
2665            f::<M>(0.0),
2666        ];
2667        let mut mat2 = M::try_from_triplets(2, 2, dest_indices, zero_values, ctx).unwrap();
2668        let gather_indices =
2669            <M::V as Vector>::Index::from_vec(vec![0, 1, 3, 4], Default::default());
2670        mat2.gather(&mat1, &gather_indices);
2671        let (_, vals) = mat2.triplet_iter();
2672        let vals: Vec<_> = vals.collect();
2673        assert_eq!(
2674            vals,
2675            vec![
2676                f::<M>(1.0),
2677                f::<M>(2.0),
2678                f::<M>(4.0),
2679                f::<M>(5.0),
2680                f::<M>(10.0),
2681                f::<M>(20.0),
2682                f::<M>(40.0),
2683                f::<M>(50.0),
2684            ]
2685        );
2686    }
2687
2688    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2689    pub fn test_batched_mul_scalar_m<M: Matrix>(ctx: M::C) {
2690        assert_eq!(ctx.nbatch(), 2);
2691        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
2692        let values = vec![
2693            f::<M>(1.0),
2694            f::<M>(3.0),
2695            f::<M>(2.0),
2696            f::<M>(4.0),
2697            f::<M>(5.0),
2698            f::<M>(7.0),
2699            f::<M>(6.0),
2700            f::<M>(8.0),
2701        ];
2702        let a = M::try_from_triplets(2, 2, indices, values, ctx.clone()).unwrap();
2703        let result = a * Scale(f::<M>(2.0));
2704        let (_, vals) = result.triplet_iter();
2705        let vals: Vec<_> = vals.collect();
2706        assert_eq!(
2707            vals,
2708            vec![
2709                f::<M>(2.0),
2710                f::<M>(6.0),
2711                f::<M>(4.0),
2712                f::<M>(8.0),
2713                f::<M>(10.0),
2714                f::<M>(14.0),
2715                f::<M>(12.0),
2716                f::<M>(16.0),
2717            ]
2718        );
2719    }
2720
2721    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2722    pub fn test_batched_partition_indices<M: Matrix>(ctx: M::C) {
2723        assert_eq!(ctx.nbatch(), 2);
2724        let zero_val = M::T::zero();
2725        let one_val = f::<M>(1.0);
2726        let two_val = f::<M>(2.0);
2727        let indices = vec![(0, 0), (1, 1), (2, 2)];
2728        let values = vec![one_val, zero_val, one_val, two_val, zero_val, two_val];
2729        let m = M::try_from_triplets(3, 3, indices, values, ctx).unwrap();
2730        let (zero_idx, nonzero_idx) = m.partition_indices_by_zero_diagonal();
2731        assert_eq!(zero_idx.clone_as_vec(), vec![1]);
2732        assert_eq!(nonzero_idx.clone_as_vec(), vec![0, 2]);
2733    }
2734
2735    /// Same order-1 scan as [`test_update_backward_diff`], run on 2 independent batches with
2736    /// distinct data, verified via `column(..).clone_as_vec()` so both batches are checked
2737    /// (`get_index` only ever reads batch 0).
2738    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2739    pub fn test_batched_update_backward_diff<M: DenseMatrix>(ctx: M::C) {
2740        assert_eq!(ctx.nbatch(), 2);
2741        let order = 1;
2742        let mut a = M::from_vec(
2743            2,
2744            4,
2745            vec![
2746                // batch 0
2747                f::<M>(1.0),
2748                f::<M>(10.0),
2749                f::<M>(2.0),
2750                f::<M>(20.0),
2751                f::<M>(3.0),
2752                f::<M>(30.0),
2753                f::<M>(0.0),
2754                f::<M>(0.0),
2755                // batch 1
2756                f::<M>(5.0),
2757                f::<M>(50.0),
2758                f::<M>(6.0),
2759                f::<M>(60.0),
2760                f::<M>(7.0),
2761                f::<M>(70.0),
2762                f::<M>(0.0),
2763                f::<M>(0.0),
2764            ],
2765            ctx.clone(),
2766        );
2767        let d = M::V::from_vec(
2768            vec![f::<M>(100.0), f::<M>(1000.0), f::<M>(200.0), f::<M>(2000.0)],
2769            ctx,
2770        );
2771
2772        a.update_backward_diff(order, &d);
2773
2774        assert_eq!(
2775            a.column(3).into_owned().clone_as_vec(),
2776            vec![f::<M>(97.0), f::<M>(970.0), f::<M>(193.0), f::<M>(1930.0)]
2777        );
2778        assert_eq!(
2779            a.column(2).into_owned().clone_as_vec(),
2780            vec![f::<M>(100.0), f::<M>(1000.0), f::<M>(200.0), f::<M>(2000.0)]
2781        );
2782        assert_eq!(
2783            a.column(1).into_owned().clone_as_vec(),
2784            vec![f::<M>(102.0), f::<M>(1020.0), f::<M>(206.0), f::<M>(2060.0)]
2785        );
2786        assert_eq!(
2787            a.column(0).into_owned().clone_as_vec(),
2788            vec![f::<M>(103.0), f::<M>(1030.0), f::<M>(211.0), f::<M>(2110.0)]
2789        );
2790    }
2791
2792    #[cfg_attr(not(feature = "cuda"), allow(dead_code))]
2793    pub fn test_batched_from_diagonal_dense<M: DenseMatrix>(ctx: M::C) {
2794        assert_eq!(ctx.nbatch(), 2);
2795        let v = M::V::from_vec(
2796            vec![f::<M>(2.0), f::<M>(3.0), f::<M>(4.0), f::<M>(5.0)],
2797            ctx,
2798        );
2799        let a = M::from_diagonal(&v);
2800        assert_eq!(a.nrows(), 2);
2801        assert_eq!(a.ncols(), 2);
2802        assert_eq!(at(&a, 0, 0, 0), f::<M>(2.0));
2803        assert_eq!(at(&a, 0, 1, 1), f::<M>(3.0));
2804        assert_eq!(at(&a, 0, 0, 1), f::<M>(0.0));
2805        assert_eq!(at(&a, 0, 1, 0), f::<M>(0.0));
2806        assert_eq!(at(&a, 1, 0, 0), f::<M>(4.0));
2807        assert_eq!(at(&a, 1, 1, 1), f::<M>(5.0));
2808        assert_eq!(at(&a, 1, 0, 1), f::<M>(0.0));
2809        assert_eq!(at(&a, 1, 1, 0), f::<M>(0.0));
2810    }
2811
2812    pub fn test_try_from_triplets_wrong_length<M: Matrix>() {
2813        let indices = vec![(0, 0), (1, 0), (0, 1), (1, 1)];
2814        // one value too few: triggers the length assertion inside try_from_triplets
2815        let values = vec![f::<M>(1.0), f::<M>(2.0), f::<M>(3.0)];
2816        let _ = M::try_from_triplets(2, 2, indices, values, Default::default());
2817    }
2818
2819    // --- Batched view-mut tests ---
2820}
2821
2822#[cfg(test)]
2823macro_rules! generate_matrix_tests_nonbatched {
2824    ($suffix:ident, $M:ty) => {
2825        paste::paste! {
2826            #[test]
2827            fn [<test_zeros_ $suffix>]() {
2828                $crate::matrix::tests::test_zeros::<$M>();
2829            }
2830            #[test]
2831            fn [<test_sparsity_ $suffix>]() {
2832                $crate::matrix::tests::test_sparsity::<$M>();
2833            }
2834            #[test]
2835            fn [<test_matrix_common_by_ref_ $suffix>]() {
2836                $crate::matrix::tests::test_matrix_common_by_ref::<$M>();
2837            }
2838            #[test]
2839            fn [<test_from_diagonal_ $suffix>]() {
2840                $crate::matrix::tests::test_from_diagonal::<$M>();
2841            }
2842            #[test]
2843            fn [<test_gemv_ $suffix>]() {
2844                $crate::matrix::tests::test_gemv::<$M>();
2845            }
2846            #[test]
2847            fn [<test_set_column_ $suffix>]() {
2848                $crate::matrix::tests::test_set_column::<$M>();
2849            }
2850            #[test]
2851            fn [<test_copy_from_ $suffix>]() {
2852                $crate::matrix::tests::test_copy_from::<$M>();
2853            }
2854            #[test]
2855            fn [<test_scale_add_and_assign_ $suffix>]() {
2856                $crate::matrix::tests::test_scale_add_and_assign::<$M>();
2857            }
2858            #[test]
2859            fn [<test_partition_indices_ $suffix>]() {
2860                $crate::matrix::tests::test_partition_indices_by_zero_diagonal::<$M>();
2861            }
2862            #[test]
2863            fn [<test_mul_scalar_ $suffix>]() {
2864                $crate::matrix::tests::test_mul_scalar::<$M>();
2865            }
2866            #[test]
2867            fn [<test_add_column_to_vector_ $suffix>]() {
2868                $crate::matrix::tests::test_add_column_to_vector::<$M>();
2869            }
2870            #[test]
2871            #[should_panic]
2872            fn [<test_try_from_triplets_wrong_length_ $suffix>]() {
2873                $crate::matrix::tests::test_try_from_triplets_wrong_length::<$M>();
2874            }
2875        }
2876    };
2877}
2878
2879#[cfg(test)]
2880#[cfg_attr(not(feature = "cuda"), allow(unused_macros))]
2881macro_rules! generate_matrix_tests_batched {
2882    ($suffix:ident, $M:ty, $ctx1:expr, $ctx2:expr) => {
2883        paste::paste! {
2884            #[test]
2885            fn [<test_batched_add_column_to_vector_ $suffix>]() {
2886                $crate::matrix::tests::test_batched_add_column_to_vector_m::<$M>($ctx2);
2887            }
2888            #[test]
2889            fn [<test_batched_add_columns_to_batched_vector_ $suffix>]() {
2890                $crate::matrix::tests::test_batched_add_columns_to_batched_vector_m::<$M>($ctx2);
2891            }
2892            #[test]
2893            fn [<test_batched_set_data_with_indices_ $suffix>]() {
2894                $crate::matrix::tests::test_batched_set_data_with_indices_m::<$M>($ctx2);
2895            }
2896            #[test]
2897            fn [<test_batched_gather_ $suffix>]() {
2898                $crate::matrix::tests::test_batched_gather_m::<$M>($ctx2);
2899            }
2900            #[test]
2901            fn [<test_batched_mul_scalar_ $suffix>]() {
2902                $crate::matrix::tests::test_batched_mul_scalar_m::<$M>($ctx2);
2903            }
2904            #[test]
2905            fn [<test_batched_partition_indices_ $suffix>]() {
2906                $crate::matrix::tests::test_batched_partition_indices::<$M>($ctx2);
2907            }
2908            #[test]
2909            fn [<test_batched_zeros_ $suffix>]() {
2910                $crate::matrix::tests::test_batched_zeros_m::<$M>($ctx2);
2911            }
2912            #[test]
2913            fn [<test_batched_gemv_ $suffix>]() {
2914                $crate::matrix::tests::test_batched_gemv_m::<$M>($ctx2);
2915            }
2916            #[test]
2917            fn [<test_batched_gemv_broadcast_x_ $suffix>]() {
2918                $crate::matrix::tests::test_batched_gemv_broadcast_x_m::<$M>($ctx2);
2919            }
2920            #[test]
2921            fn [<test_batched_gemv_broadcast_mat_ $suffix>]() {
2922                $crate::matrix::tests::test_batched_gemv_broadcast_mat_m::<$M>($ctx2);
2923            }
2924            #[test]
2925            fn [<test_batched_from_diagonal_ $suffix>]() {
2926                $crate::matrix::tests::test_batched_from_diagonal_m::<$M>($ctx2);
2927            }
2928            #[test]
2929            fn [<test_batched_copy_from_ $suffix>]() {
2930                $crate::matrix::tests::test_batched_copy_from_m::<$M>($ctx2);
2931            }
2932            #[test]
2933            fn [<test_batched_copy_from_broadcast_ $suffix>]() {
2934                $crate::matrix::tests::test_batched_copy_from_broadcast_m::<$M>($ctx2);
2935            }
2936            #[test]
2937            fn [<test_batched_set_column_ $suffix>]() {
2938                $crate::matrix::tests::test_batched_set_column_m::<$M>($ctx2);
2939            }
2940            #[test]
2941            fn [<test_batched_scale_add_ $suffix>]() {
2942                $crate::matrix::tests::test_batched_scale_add_and_assign_m::<$M>($ctx2);
2943            }
2944            #[test]
2945            fn [<test_batched_owned_lhs_broadcast_ $suffix>]() {
2946                $crate::matrix::tests::test_batched_owned_lhs_broadcast_m::<$M>($ctx2);
2947            }
2948            #[test]
2949            fn [<test_batched_scale_add_broadcast_ $suffix>]() {
2950                $crate::matrix::tests::test_batched_scale_add_and_assign_broadcast_m::<$M>($ctx2);
2951            }
2952
2953            #[test]
2954            fn [<test_batched_mul_scalar_ref_ $suffix>]() {
2955                $crate::matrix::tests::test_batched_mul_scalar_ref_m::<$M>($ctx2);
2956            }
2957            #[test]
2958            fn [<test_batched_owned_lhs_add_broadcast_ $suffix>]() {
2959                $crate::matrix::tests::test_batched_owned_lhs_add_broadcast_m::<$M>($ctx2);
2960            }
2961            #[test]
2962            fn [<test_batched_gemv_beta_ $suffix>]() {
2963                $crate::matrix::tests::test_batched_gemv_beta_m::<$M>($ctx2);
2964            }
2965
2966            // --- Governing-operand tests: the operand that governs must be the wider one ---
2967            #[test]
2968            #[should_panic(expected = "incompatible nbatch")]
2969            fn [<test_batched_owned_lhs_narrow_ $suffix>]() {
2970                $crate::matrix::tests::test_batched_owned_lhs_narrow_m::<$M>($ctx2);
2971            }
2972            #[test]
2973            #[should_panic(expected = "incompatible nbatch")]
2974            fn [<test_grouped_owned_lhs_narrow_ $suffix>]() {
2975                $crate::matrix::tests::test_grouped_owned_lhs_narrow_m::<$M>($ctx2);
2976            }
2977
2978            // --- Narrow-destination tests (source wider than destination panics) ---
2979            #[test]
2980            #[should_panic(expected = "incompatible nbatch")]
2981            fn [<test_narrow_dest_gemv_ $suffix>]() {
2982                $crate::matrix::tests::test_narrow_dest_gemv_m::<$M>($ctx2);
2983            }
2984            #[test]
2985            #[should_panic(expected = "incompatible nbatch")]
2986            fn [<test_narrow_dest_copy_from_ $suffix>]() {
2987                $crate::matrix::tests::test_narrow_dest_copy_from_m::<$M>($ctx2);
2988            }
2989            #[test]
2990            #[should_panic(expected = "incompatible nbatch")]
2991            fn [<test_narrow_dest_scale_add_ $suffix>]() {
2992                $crate::matrix::tests::test_narrow_dest_scale_add_m::<$M>($ctx2);
2993            }
2994            #[test]
2995            #[should_panic(expected = "incompatible nbatch")]
2996            fn [<test_narrow_dest_gather_ $suffix>]() {
2997                $crate::matrix::tests::test_narrow_dest_gather_m::<$M>($ctx2);
2998            }
2999            #[test]
3000            #[should_panic(expected = "incompatible nbatch")]
3001            fn [<test_narrow_dest_set_data_with_indices_ $suffix>]() {
3002                $crate::matrix::tests::test_narrow_dest_set_data_with_indices_m::<$M>($ctx2);
3003            }
3004
3005            // --- Grouped broadcast tests (B -> B * P, using $ctx2 widened to 4) ---
3006            #[test]
3007            fn [<test_grouped_gemv_ $suffix>]() {
3008                $crate::matrix::tests::test_grouped_gemv_m::<$M>($ctx2);
3009            }
3010            #[test]
3011            fn [<test_grouped_gemv_x_ $suffix>]() {
3012                $crate::matrix::tests::test_grouped_gemv_x_m::<$M>($ctx2);
3013            }
3014            #[test]
3015            fn [<test_grouped_copy_from_ $suffix>]() {
3016                $crate::matrix::tests::test_grouped_copy_from_m::<$M>($ctx2);
3017            }
3018            #[test]
3019            fn [<test_grouped_owned_lhs_add_ $suffix>]() {
3020                $crate::matrix::tests::test_grouped_owned_lhs_add_m::<$M>($ctx2);
3021            }
3022            #[test]
3023            fn [<test_grouped_scale_add_ $suffix>]() {
3024                $crate::matrix::tests::test_grouped_scale_add_and_assign_m::<$M>($ctx2);
3025            }
3026        }
3027    };
3028}
3029
3030#[cfg(test)]
3031macro_rules! generate_dense_matrix_tests_nonbatched {
3032    ($suffix:ident, $M:ty) => {
3033        paste::paste! {
3034            #[test]
3035            fn [<test_from_vec_ $suffix>]() {
3036                $crate::matrix::tests::test_from_vec::<$M>();
3037            }
3038            #[test]
3039            fn [<test_from_diagonal_dense_ $suffix>]() {
3040                $crate::matrix::tests::test_from_diagonal_dense::<$M>();
3041            }
3042            #[test]
3043            fn [<test_column_view_ $suffix>]() {
3044                $crate::matrix::tests::test_column_view::<$M>();
3045            }
3046            #[test]
3047            fn [<test_sparsity_dense_errors_ $suffix>]() {
3048                $crate::matrix::tests::test_sparsity_dense_errors::<$M>();
3049            }
3050            #[test]
3051            fn [<test_update_backward_diff_ $suffix>]() {
3052                $crate::matrix::tests::test_update_backward_diff::<$M>();
3053            }
3054            #[test]
3055            fn [<test_update_backward_diff_transpose_ $suffix>]() {
3056                $crate::matrix::tests::test_update_backward_diff_transpose::<$M>();
3057            }
3058            #[test]
3059            fn [<test_mul_cols_by_transpose_ $suffix>]() {
3060                $crate::matrix::tests::test_mul_cols_by_transpose::<$M>();
3061            }
3062            #[test]
3063            fn [<test_gemv_cols_ $suffix>]() {
3064                $crate::matrix::tests::test_gemv_cols::<$M>();
3065            }
3066            #[test]
3067            fn [<test_ger_cols_ $suffix>]() {
3068                $crate::matrix::tests::test_ger_cols::<$M>();
3069            }
3070            #[test]
3071            fn [<test_ger_cols_transpose_ $suffix>]() {
3072                $crate::matrix::tests::test_ger_cols_transpose::<$M>();
3073            }
3074
3075            #[test]
3076            fn [<test_gemv_cols_many_columns_ $suffix>]() {
3077                $crate::matrix::tests::test_gemv_cols_many_columns::<$M>();
3078            }
3079
3080            #[test]
3081            fn [<test_mul_cols_by_ $suffix>]() {
3082                $crate::matrix::tests::test_mul_cols_by::<$M>();
3083            }
3084            #[test]
3085            fn [<test_fill_ $suffix>]() {
3086                $crate::matrix::tests::test_fill::<$M>(Default::default());
3087            }
3088            #[test]
3089            fn [<test_resize_cols_ $suffix>]() {
3090                $crate::matrix::tests::test_resize_cols::<$M>();
3091            }
3092            #[test]
3093            fn [<test_add_ $suffix>]() {
3094                $crate::matrix::tests::test_add::<$M>();
3095            }
3096            #[test]
3097            fn [<test_sub_ $suffix>]() {
3098                $crate::matrix::tests::test_sub::<$M>();
3099            }
3100            #[test]
3101            fn [<test_inner_mut_ $suffix>]() {
3102                $crate::matrix::tests::test_inner_mut::<$M>();
3103            }
3104            #[test]
3105            fn [<test_column_mut_ $suffix>]() {
3106                $crate::matrix::tests::test_column_mut::<$M>();
3107            }
3108            #[test]
3109            fn [<test_add_assign_ $suffix>]() {
3110                $crate::matrix::tests::test_add_assign::<$M>();
3111            }
3112            #[test]
3113            fn [<test_sub_assign_ $suffix>]() {
3114                $crate::matrix::tests::test_sub_assign::<$M>();
3115            }
3116            #[test]
3117            fn [<test_gather_ $suffix>]() {
3118                $crate::matrix::tests::test_gather::<$M>();
3119            }
3120            #[test]
3121            fn [<test_set_data_with_indices_ $suffix>]() {
3122                $crate::matrix::tests::test_set_data_with_indices::<$M>();
3123            }
3124        }
3125    };
3126}
3127
3128#[cfg(test)]
3129#[cfg_attr(not(feature = "cuda"), allow(unused_macros))]
3130macro_rules! generate_dense_matrix_tests_batched {
3131    ($suffix:ident, $M:ty, $ctx1:expr, $ctx2:expr) => {
3132        paste::paste! {
3133            #[test]
3134            fn [<test_batched_update_backward_diff_ $suffix>]() {
3135                $crate::matrix::tests::test_batched_update_backward_diff::<$M>($ctx2);
3136            }
3137            #[test]
3138            fn [<test_batched_gemv_cols_ $suffix>]() {
3139                $crate::matrix::tests::test_batched_gemv_cols_m::<$M>($ctx2);
3140            }
3141            #[test]
3142            fn [<test_batched_ger_cols_ $suffix>]() {
3143                $crate::matrix::tests::test_batched_ger_cols::<$M>($ctx2);
3144            }
3145
3146            #[test]
3147            fn [<test_batched_mul_cols_by_ $suffix>]() {
3148                $crate::matrix::tests::test_batched_mul_cols_by::<$M>($ctx2);
3149            }
3150            #[test]
3151            fn [<test_batched_fill_ $suffix>]() {
3152                $crate::matrix::tests::test_fill::<$M>($ctx2);
3153            }
3154            #[test]
3155            fn [<test_batched_from_diagonal_dense_ $suffix>]() {
3156                $crate::matrix::tests::test_batched_from_diagonal_dense::<$M>($ctx2);
3157            }
3158            #[test]
3159            fn [<test_batched_from_vec_ $suffix>]() {
3160                $crate::matrix::tests::test_batched_from_vec::<$M>($ctx2);
3161            }
3162            #[test]
3163            fn [<test_batched_set_index_batch_ $suffix>]() {
3164                $crate::matrix::tests::test_batched_set_index_batch::<$M>($ctx2);
3165            }
3166            #[test]
3167            fn [<test_batched_gemv_o_broadcast_x_ $suffix>]() {
3168                $crate::matrix::tests::test_batched_gemv_o_broadcast_x::<$M>($ctx2);
3169            }
3170            #[test]
3171            fn [<test_batched_resize_cols_ $suffix>]() {
3172                $crate::matrix::tests::test_batched_resize_cols::<$M>($ctx2);
3173            }
3174            #[test]
3175            fn [<test_batched_combine_ $suffix>]() {
3176                $crate::matrix::tests::test_batched_combine::<$M>($ctx2);
3177            }
3178            #[test]
3179            fn [<test_batched_add_assign_broadcast_ $suffix>]() {
3180                $crate::matrix::tests::test_batched_add_assign_broadcast_m::<$M>($ctx2);
3181            }
3182            #[test]
3183            fn [<test_grouped_add_assign_ $suffix>]() {
3184                $crate::matrix::tests::test_grouped_add_assign_m::<$M>($ctx2);
3185            }
3186            #[test]
3187            #[should_panic(expected = "incompatible nbatch")]
3188            fn [<test_narrow_dest_gemv_cols_ $suffix>]() {
3189                $crate::matrix::tests::test_narrow_dest_gemv_cols_m::<$M>($ctx2);
3190            }
3191            #[test]
3192            #[should_panic(expected = "incompatible nbatch")]
3193            fn [<test_narrow_dest_add_assign_ $suffix>]() {
3194                $crate::matrix::tests::test_narrow_dest_add_assign_m::<$M>($ctx2);
3195            }
3196            #[test]
3197            fn [<test_grouped_gemv_cols_ $suffix>]() {
3198                $crate::matrix::tests::test_grouped_gemv_cols_m::<$M>($ctx2);
3199            }
3200            #[test]
3201            #[should_panic(expected = "incompatible nbatch")]
3202            fn [<test_batched_gemv_incompatible_ $suffix>]() {
3203                $crate::matrix::tests::test_batched_gemv_incompatible::<$M>($ctx2, $ctx1.clone_with_nbatch(3).unwrap());
3204            }
3205        }
3206    };
3207}
3208
3209#[cfg(test)]
3210#[cfg_attr(not(feature = "cuda"), allow(unused_imports))]
3211pub(crate) use generate_dense_matrix_tests_batched;
3212#[cfg(test)]
3213pub(crate) use generate_dense_matrix_tests_nonbatched;
3214#[cfg(test)]
3215#[cfg_attr(not(feature = "cuda"), allow(unused_imports))]
3216pub(crate) use generate_matrix_tests_batched;
3217#[cfg(test)]
3218pub(crate) use generate_matrix_tests_nonbatched;