Skip to main content

diffsol_la/matrix/
dense_nalgebra_serial.rs

1use std::ops::{Add, AddAssign, Index, IndexMut, Mul, Sub, SubAssign};
2
3use nalgebra::{
4    Const, DMatrix, DVectorView, MatrixView as NaMatrixView, MatrixViewMut as NaMatrixViewMut,
5};
6
7use super::default_solver::DefaultSolver;
8use super::sparsity::{Dense, DenseRef};
9use std::mem::MaybeUninit;
10
11use crate::context::broadcast_batch;
12use crate::error::LaError;
13use crate::{scalar::Scale, Context, NalgebraScalar, Vector};
14use crate::{
15    DenseMatrix, Matrix, MatrixCommon, NalgebraContext, NalgebraLU, NalgebraVec, NalgebraVecMut,
16    NalgebraVecRef, VectorIndex,
17};
18
19/// A batched matrix, stored as `nbatch` side-by-side blocks of `logical_ncols` columns.
20///
21/// Invariant: `data.ncols() == logical_ncols() * context.nbatch()`, so batch `b` column `j`
22/// lives at physical column `col(b, j)`.
23#[derive(Clone, Debug, PartialEq)]
24pub struct NalgebraMat<T: NalgebraScalar> {
25    pub(crate) data: DMatrix<T>,
26    pub(crate) context: NalgebraContext,
27}
28impl<T: NalgebraScalar> NalgebraMat<T> {
29    fn logical_ncols(&self) -> usize {
30        self.data.ncols() / self.context.nbatch()
31    }
32    /// Physical column of logical column `j` in this matrix's own batch `b`.
33    fn col(&self, b: usize, j: usize) -> usize {
34        debug_assert!(b < self.context.nbatch(), "batch out of bounds");
35        b * self.logical_ncols() + j
36    }
37    /// Physical column feeding batch `b` of a `nbatch`-batch destination, broadcasting this
38    /// matrix over contiguous groups of destination batches (see [`broadcast_batch`]).
39    fn col_bcast(&self, b: usize, j: usize, nbatch: usize) -> usize {
40        self.col(broadcast_batch(b, self.context.nbatch(), nbatch), j)
41    }
42}
43/// Scalars of staging buffer `mul_cols_by` uses per row tile. The tile is
44/// `MUL_COLS_TILE / ncols` rows, so a narrow range gets a tall tile and a wide one a short
45/// tile, and either way the staged tile and the destination it writes back stay in L1.
46///
47/// Swept over 64..=2048 against `lin_alg_ops mul_cols_by` (nstates 2..=10000, `ncols` 2 and
48/// 6): 1024 is the best or tied-best at every tall shape and 8% ahead of 512 on
49/// `mul_cols_by/faer/k6/10000`, while 2048 falls off again.  The buffer is uninitialised, so
50/// short vectors no longer pay for the part of it they do not use and the choice is decided
51/// purely by the tall shapes.
52const MUL_COLS_TILE: usize = 1024;
53
54impl<T: NalgebraScalar> DefaultSolver for NalgebraMat<T> {
55    type LS = NalgebraLU<T>;
56}
57
58impl<T: NalgebraScalar> MatrixCommon for NalgebraMat<T> {
59    type T = T;
60    type V = NalgebraVec<T>;
61    type C = NalgebraContext;
62    type Inner = DMatrix<T>;
63    fn nrows(&self) -> usize {
64        self.data.nrows()
65    }
66    fn ncols(&self) -> usize {
67        self.logical_ncols()
68    }
69    fn inner(&self) -> &Self::Inner {
70        &self.data
71    }
72}
73/// `self` is the owned operand, so it is the destination -- which makes the in-place op the
74/// whole implementation, broadcast check and whole-matrix fast path included.
75macro_rules! binary_owned_lhs {
76    ($tr:ident, $fn:ident, $assign_tr:ident, $assign:ident, $rhs:ty) => {
77        impl<T: NalgebraScalar> $tr<$rhs> for NalgebraMat<T> {
78            type Output = NalgebraMat<T>;
79
80            fn $fn(mut self, rhs: $rhs) -> Self::Output {
81                $assign_tr::$assign(&mut self, rhs);
82                self
83            }
84        }
85    };
86}
87binary_owned_lhs!(Add, add, AddAssign, add_assign, &NalgebraMat<T>);
88binary_owned_lhs!(Sub, sub, SubAssign, sub_assign, &NalgebraMat<T>);
89
90/// `rhs` is the owned operand, so it is the destination.
91///
92/// A commutative op is just the in-place op with the operands swapped.  A non-commutative one
93/// cannot be: `rhs -= lhs` computes `rhs - lhs`, so it writes `combine(&mut rhs_ij, lhs_ij)`
94/// instead, which gets `lhs - rhs` in the same single pass.
95macro_rules! binary_owned_rhs {
96    (commutes, $tr:ident, $fn:ident, $assign_tr:ident, $assign:ident, $lhs:ty, $op:tt,
97     $combine:expr) => {
98        impl<T: NalgebraScalar> $tr<NalgebraMat<T>> for $lhs {
99            type Output = NalgebraMat<T>;
100
101            fn $fn(self, mut rhs: NalgebraMat<T>) -> Self::Output {
102                $assign_tr::$assign(&mut rhs, self);
103                rhs
104            }
105        }
106    };
107    (noncommutes, $tr:ident, $fn:ident, $assign_tr:ident, $assign:ident, $lhs:ty, $op:tt, $combine:expr) => {
108        impl<T: NalgebraScalar> $tr<NalgebraMat<T>> for $lhs {
109            type Output = NalgebraMat<T>;
110
111            fn $fn(self, mut rhs: NalgebraMat<T>) -> Self::Output {
112                rhs.context
113                    .assert_broadcastable_into(self.context.nbatch(), stringify!($fn));
114                let nc = self.ncols();
115                let nb = rhs.context.nbatch();
116                for b in 0..nb {
117                    let mut columns = rhs.data.columns_mut(rhs.col(b, 0), nc);
118                    columns.zip_apply(&self.data.columns(self.col_bcast(b, 0, nb), nc), $combine);
119                }
120                rhs
121            }
122        }
123    };
124}
125binary_owned_rhs!(
126    commutes, Add, add, AddAssign, add_assign, &NalgebraMat<T>, +=, |rhs: &mut T, lhs: T| *rhs += lhs
127);
128binary_owned_rhs!(
129    noncommutes, Sub, sub, SubAssign, sub_assign, &NalgebraMat<T>, -=,
130    |rhs: &mut T, lhs: T| *rhs = lhs - *rhs
131);
132macro_rules! assign {
133    ($tr:ident, $fn:ident, $l:ty, $r:ty, $op:tt) => {
134        impl<T: NalgebraScalar> $tr<$r> for $l {
135            fn $fn(&mut self, rhs: $r) {
136                self.context
137                    .assert_broadcastable_into(rhs.context.nbatch(), stringify!($fn));
138                let nb = self.context.nbatch();
139                let nc = self.ncols();
140                if nb == rhs.context.nbatch()
141                    && self.data.ncols() == nc * nb
142                    && rhs.data.ncols() == nc * nb
143                {
144                    self.data $op &rhs.data;
145                    return;
146                }
147                for b in 0..nb {
148                    let mut columns = self.data.columns_mut(self.col(b, 0), nc);
149                    columns $op &rhs.data.columns(rhs.col_bcast(b, 0, nb), nc);
150                }
151            }
152        }
153    };
154}
155assign!(AddAssign, add_assign, NalgebraMat<T>, &NalgebraMat<T>, +=);
156assign!(SubAssign, sub_assign, NalgebraMat<T>, &NalgebraMat<T>, -=);
157impl<T: NalgebraScalar> Mul<Scale<T>> for NalgebraMat<T> {
158    type Output = Self;
159    fn mul(mut self, r: Scale<T>) -> Self {
160        self.data *= r.value();
161        self
162    }
163}
164macro_rules! scale_ref {
165    ($t:ty) => {
166        impl<T: NalgebraScalar> Mul<Scale<T>> for $t {
167            type Output = NalgebraMat<T>;
168            fn mul(self, r: Scale<T>) -> Self::Output {
169                // every batch is scaled by the same factor, and the batches tile the columns,
170                // so this is one whole-matrix multiply whatever the batch count
171                NalgebraMat {
172                    data: &self.data * r.value(),
173                    context: self.context,
174                }
175            }
176        }
177    };
178}
179scale_ref!(&NalgebraMat<T>);
180macro_rules! ind {
181    ($t:ty) => {
182        impl<T: NalgebraScalar> Index<(usize, usize)> for $t {
183            type Output = T;
184            fn index(&self, x: (usize, usize)) -> &T {
185                assert_eq!(
186                    self.context.nbatch(),
187                    1,
188                    "indexing not supported for batched matrices"
189                );
190                &self.data[(x.0, self.col(0, x.1))]
191            }
192        }
193    };
194}
195ind!(NalgebraMat<T>);
196impl<T: NalgebraScalar> IndexMut<(usize, usize)> for NalgebraMat<T> {
197    fn index_mut(&mut self, x: (usize, usize)) -> &mut T {
198        assert_eq!(
199            self.context.nbatch(),
200            1,
201            "indexing not supported for batched matrices"
202        );
203        let c = self.col(0, x.1);
204        &mut self.data[(x.0, c)]
205    }
206}
207
208/// `y_b = a * self_b * x_b + beta * y_b` for every batch of `y`, broadcasting single-batch
209/// operands. `x` is a full vector operand of length `ncols`.
210///
211/// nalgebra's own `gemv` is a per-column `axcpy` loop, so it traverses `y` once per column.
212/// A single flat pass accumulating every column into `y` measures ~5-8% faster once `nrows`
213/// reaches a few hundred, but slower below that (the `stage_accumulate` benchmark: 19.4ns
214/// against 16.5ns at 2 rows), and the solvers' hot matrices are small — so this stays on
215/// nalgebra's path throughout.
216macro_rules! gemv_data {
217    ($self:ident, $a:ident, $x:ident, $beta:ident, $y:ident, $op:literal) => {{
218        $y.context
219            .assert_broadcastable_into($self.context.nbatch(), $op);
220        $y.context
221            .assert_broadcastable_into($x.context.nbatch(), $op);
222        let nc = $self.ncols();
223        // the unbatched case keeps constant column indices, which codegens better than the
224        // loop variable below
225        if $y.data.ncols() == 1 {
226            $y.data.column_mut(0).gemv(
227                $a,
228                &$self.data.columns($self.col(0, 0), nc),
229                &$x.data.column(0),
230                $beta,
231            );
232            return;
233        }
234        let nb = $y.data.ncols();
235        for b in 0..nb {
236            $y.data.column_mut(b).gemv(
237                $a,
238                &$self.data.columns($self.col_bcast(b, 0, nb), nc),
239                &$x.data.column($x.batch(b, nb)),
240                $beta,
241            );
242        }
243    }};
244}
245
246/// `y_b = a * self_b[:, start..start + nc] * x + beta * y_b` for every batch of `y`, with `x` a
247/// host coefficient slice shared by all batches. `DVectorView::from_slice` wraps it without
248/// copying, so nalgebra's own `gemv` still does the work.
249macro_rules! gemv_cols_data {
250    ($self:ident, $start:expr, $nc:expr, $a:ident, $x:ident, $beta:ident, $y:ident, $op:literal) => {{
251        $y.context
252            .assert_broadcastable_into($self.context.nbatch(), $op);
253        let (start, nc) = ($start, $nc);
254        // only a debug assert: `ncols()` on the owned matrix is a division, and this runs once
255        // per Runge-Kutta stage.  An out-of-range range still panics in release, inside
256        // nalgebra's `columns`.
257        debug_assert!(
258            start + nc <= $self.ncols(),
259            concat!($op, ": column range out of bounds")
260        );
261        let x = DVectorView::from_slice(&$x[..nc], nc);
262        if $y.data.ncols() == 1 {
263            $y.data
264                .column_mut(0)
265                .gemv($a, &$self.data.columns($self.col(0, start), nc), &x, $beta);
266            return;
267        }
268        let nb = $y.data.ncols();
269        for b in 0..nb {
270            $y.data.column_mut(b).gemv(
271                $a,
272                &$self.data.columns($self.col_bcast(b, start, nb), nc),
273                &x,
274                $beta,
275            );
276        }
277    }};
278}
279
280impl<T: NalgebraScalar> Matrix for NalgebraMat<T> {
281    type Sparsity = Dense<Self>;
282    type SparsityRef<'a> = DenseRef<'a, Self>;
283    fn sparsity(&self) -> Option<Self::SparsityRef<'_>> {
284        None
285    }
286    fn context(&self) -> &Self::C {
287        &self.context
288    }
289    fn inner_mut(&mut self) -> &mut Self::Inner {
290        &mut self.data
291    }
292    fn set_data_with_indices(
293        &mut self,
294        dst: &crate::vector::nalgebra_serial::NalgebraIndex,
295        src: &crate::vector::nalgebra_serial::NalgebraIndex,
296        data: &NalgebraVec<T>,
297    ) {
298        self.context
299            .assert_broadcastable_into(data.context.nbatch(), "set_data_with_indices");
300        let nb = self.context.nbatch();
301        for (d, s) in dst.data.iter().zip(src.data.iter()) {
302            let i = d % self.nrows();
303            let j = d / self.nrows();
304            for b in 0..nb {
305                let c = self.col(b, j);
306                self.data[(i, c)] = data.data[(*s, data.batch(b, nb))]
307            }
308        }
309    }
310    fn gather(&mut self, o: &Self, idx: &crate::vector::nalgebra_serial::NalgebraIndex) {
311        self.context
312            .assert_broadcastable_into(o.context.nbatch(), "gather");
313        let nb = self.context.nbatch();
314        for b in 0..nb {
315            for (d, s) in idx.data.iter().enumerate() {
316                let i = d % self.nrows();
317                let j = d / self.nrows();
318                let oi = s % o.nrows();
319                let oj = s / o.nrows();
320                let c = self.col(b, j);
321                self.data[(i, c)] = o.data[(oi, o.col_bcast(b, oj, nb))]
322            }
323        }
324    }
325    fn partition_indices_by_zero_diagonal(
326        &self,
327    ) -> (
328        crate::vector::nalgebra_serial::NalgebraIndex,
329        crate::vector::nalgebra_serial::NalgebraIndex,
330    ) {
331        let (mut z, mut nz) = (Vec::new(), Vec::new());
332        for i in 0..self.nrows() {
333            if self.data[(i, self.col(0, i))].is_zero() {
334                z.push(i)
335            } else {
336                nz.push(i)
337            }
338        }
339        (
340            crate::vector::nalgebra_serial::NalgebraIndex::from_vec(z, self.context),
341            crate::vector::nalgebra_serial::NalgebraIndex::from_vec(nz, self.context),
342        )
343    }
344    fn add_column_to_vector(&self, j: usize, v: &mut NalgebraVec<T>) {
345        v.context
346            .assert_broadcastable_into(self.context.nbatch(), "add_column_to_vector");
347        if self.context.nbatch() == 1 && v.context.nbatch() == 1 {
348            v.data
349                .column_mut(0)
350                .axpy(T::one(), &self.data.column(j), T::one());
351            return;
352        }
353        let nb = v.context.nbatch();
354        for b in 0..nb {
355            v.data.column_mut(b).axpy(
356                T::one(),
357                &self.data.column(self.col_bcast(b, j, nb)),
358                T::one(),
359            );
360        }
361    }
362    fn add_columns_to_batched_vector(&self, v: &mut NalgebraVec<T>) {
363        assert_eq!(v.len(), self.nrows(), "row count mismatch");
364        assert_eq!(
365            v.context.nbatch(),
366            self.context.nbatch() * self.ncols(),
367            "batch count mismatch: the destination holds one lane per (batch, column)"
368        );
369        // batch `b` column `j` lives at physical column `b * ncols + j`, which is exactly the
370        // destination lane, so the two buffers are congruent
371        v.data += &self.data;
372    }
373    fn triplet_iter(
374        &self,
375    ) -> (
376        impl Iterator<Item = (usize, usize)> + '_,
377        impl Iterator<Item = T> + '_,
378    ) {
379        let pos: Vec<_> = (0..self.ncols())
380            .flat_map(|j| (0..self.nrows()).map(move |i| (i, j)))
381            .collect();
382        let val: Vec<_> = (0..self.context.nbatch())
383            .flat_map(|b| {
384                pos.iter()
385                    .map(move |&(i, j)| self.data[(i, self.col(b, j))])
386            })
387            .collect();
388        (pos.into_iter(), val.into_iter())
389    }
390    fn try_from_triplets(
391        nr: usize,
392        nc: usize,
393        idx: Vec<(usize, usize)>,
394        v: Vec<T>,
395        ctx: Self::C,
396    ) -> Result<Self, LaError> {
397        assert_eq!(v.len(), idx.len() * ctx.nbatch());
398        let mut out = Self::zeros(nr, nc, ctx);
399        for b in 0..ctx.nbatch() {
400            for (k, &(i, j)) in idx.iter().enumerate() {
401                let c = out.col(b, j);
402                out.data[(i, c)] = v[b * idx.len() + k]
403            }
404        }
405        Ok(out)
406    }
407    fn zeros(nr: usize, nc: usize, ctx: Self::C) -> Self {
408        Self {
409            data: DMatrix::zeros(nr, nc * ctx.nbatch()),
410            context: ctx,
411        }
412    }
413    fn from_diagonal(v: &NalgebraVec<T>) -> Self {
414        if v.context.nbatch() == 1 {
415            return Self {
416                data: DMatrix::from_diagonal(&v.data.column(0)),
417                context: v.context,
418            };
419        }
420        let mut out = Self::zeros(v.len(), v.len(), v.context);
421        for b in 0..v.context.nbatch() {
422            out.data
423                .columns_mut(out.col(b, 0), out.ncols())
424                .set_diagonal(&v.data.column(b));
425        }
426        out
427    }
428    fn gemv(&self, a: T, x: &NalgebraVec<T>, beta: T, y: &mut NalgebraVec<T>) {
429        gemv_data!(self, a, x, beta, y, "gemv")
430    }
431    fn copy_from(&mut self, o: &Self) {
432        self.context
433            .assert_broadcastable_into(o.context.nbatch(), "copy_from");
434        if self.context.nbatch() == o.context.nbatch() {
435            self.data.copy_from(&o.data);
436            return;
437        }
438        let nb = self.context.nbatch();
439        for b in 0..nb {
440            let nc = self.ncols();
441            self.data
442                .columns_mut(self.col(b, 0), nc)
443                .copy_from(&o.data.columns(o.col_bcast(b, 0, nb), nc));
444        }
445    }
446    fn set_column(&mut self, j: usize, v: &NalgebraVec<T>) {
447        self.context
448            .assert_broadcastable_into(v.context.nbatch(), "set_column");
449        // unbatched fast path: worth ~4x on lin_alg_ops set_column/nalgebra/10
450        if self.context.nbatch() == 1 && v.context.nbatch() == 1 {
451            self.data.column_mut(j).copy_from(&v.data.column(0));
452            return;
453        }
454        let nb = self.context.nbatch();
455        for b in 0..nb {
456            self.data
457                .column_mut(self.col(b, j))
458                .copy_from(&v.data.column(v.batch(b, nb)));
459        }
460    }
461    fn scale_add_and_assign(&mut self, x: &Self, b: T, y: &Self) {
462        self.context
463            .assert_broadcastable_into(x.context.nbatch(), "scale_add_and_assign");
464        self.context
465            .assert_broadcastable_into(y.context.nbatch(), "scale_add_and_assign");
466        let nc = self.ncols();
467        let nb = self.context.nbatch();
468        // the unbatched path keeps constant column indices, which is worth ~6% here (see
469        // lin_alg_ops scale_add_and_assign/nalgebra)
470        if nb == 1 {
471            let mut columns = self.data.columns_mut(0, nc);
472            columns.copy_from(&x.data.columns(0, nc));
473            columns.zip_apply(&y.data.columns(0, nc), |s, y| {
474                *s += b * y;
475            });
476            return;
477        }
478        for batch in 0..nb {
479            let mut columns = self.data.columns_mut(self.col(batch, 0), nc);
480            columns.copy_from(&x.data.columns(x.col_bcast(batch, 0, nb), nc));
481            columns.zip_apply(&y.data.columns(y.col_bcast(batch, 0, nb), nc), |s, y| {
482                *s += b * y;
483            });
484        }
485    }
486    fn new_from_sparsity(nr: usize, nc: usize, _: Option<Self::Sparsity>, ctx: Self::C) -> Self {
487        Self::zeros(nr, nc, ctx)
488    }
489}
490impl<T: NalgebraScalar> DenseMatrix for NalgebraMat<T> {
491    fn fill(&mut self, value: T) {
492        self.data.fill(value);
493    }
494    fn resize_cols(&mut self, nc: usize) {
495        let old = self.ncols();
496        if old == nc {
497            return;
498        }
499        let mut d = DMatrix::zeros(self.nrows(), nc * self.context.nbatch());
500        for b in 0..self.context.nbatch() {
501            d.columns_mut(b * nc, old.min(nc))
502                .copy_from(&self.data.columns(self.col(b, 0), old.min(nc)));
503        }
504        self.data = d
505    }
506    fn get_index(&self, i: usize, j: usize) -> T {
507        assert_eq!(
508            self.context.nbatch(),
509            1,
510            "get_index not supported for batched matrices"
511        );
512        self.data[(i, self.col(0, j))]
513    }
514    fn gemv_cols(
515        &self,
516        start: usize,
517        end: usize,
518        alpha: T,
519        x: &[T],
520        beta: T,
521        y: &mut NalgebraVec<T>,
522    ) {
523        assert!(start <= end, "gemv_cols: column range start > end");
524        assert!(
525            end - start <= crate::matrix::MAX_SMALL_COLS,
526            "gemv_cols: column range exceeds MAX_SMALL_COLS"
527        );
528        assert!(
529            x.len() >= end - start,
530            "gemv_cols: x must hold at least end - start values"
531        );
532        gemv_cols_data!(self, start, end - start, alpha, x, beta, y, "gemv_cols")
533    }
534    fn from_vec(nr: usize, nc: usize, d: Vec<T>, ctx: Self::C) -> Self {
535        assert_eq!(d.len(), nr * nc * ctx.nbatch());
536        Self {
537            data: DMatrix::from_vec(nr, nc * ctx.nbatch(), d),
538            context: ctx,
539        }
540    }
541    fn column_mut(&mut self, i: usize) -> NalgebraVecMut<'_, T> {
542        if self.context.nbatch() == 1 {
543            return NalgebraVecMut {
544                data: self.data.columns_mut(i, 1),
545                context: self.context,
546            };
547        }
548        let nr = self.nrows();
549        let nc = self.ncols();
550        let nb = self.context.nbatch();
551        assert!(i < nc, "column index out of bounds");
552        // column i of every batch: batch b holds it at offset (b * nc + i) * nr
553        let data = NaMatrixViewMut::from_slice_with_strides_generic(
554            &mut self.data.as_mut_slice()[i * nr..],
555            nalgebra::Dyn(nr),
556            nalgebra::Dyn(nb),
557            Const,
558            nalgebra::Dyn(nr * nc),
559        );
560        NalgebraVecMut {
561            data,
562            context: self.context,
563        }
564    }
565    fn set_index(&mut self, i: usize, j: usize, v: T) {
566        assert_eq!(
567            self.context.nbatch(),
568            1,
569            "set_index not supported for batched matrices"
570        );
571        let c = self.col(0, j);
572        self.data[(i, c)] = v;
573    }
574    fn set_index_batch(&mut self, batch: usize, i: usize, j: usize, value: T) {
575        let c = self.col(batch, j);
576        self.data[(i, c)] = value;
577    }
578    fn column(&self, i: usize) -> NalgebraVecRef<'_, T> {
579        if self.context.nbatch() == 1 {
580            return NalgebraVecRef {
581                data: self.data.columns(i, 1),
582                context: self.context,
583            };
584        }
585        let nr = self.nrows();
586        let nc = self.ncols();
587        let nb = self.context.nbatch();
588        assert!(i < nc, "column index out of bounds");
589        // column i of every batch: batch b holds it at offset (b * nc + i) * nr
590        let data = NaMatrixView::from_slice_with_strides_generic(
591            &self.data.as_slice()[i * nr..],
592            nalgebra::Dyn(nr),
593            nalgebra::Dyn(nb),
594            Const,
595            nalgebra::Dyn(nr * nc),
596        );
597        NalgebraVecRef {
598            data,
599            context: self.context,
600        }
601    }
602    fn mul_cols_by(&mut self, ncols: usize, rhs: &[T]) {
603        assert!(
604            ncols <= self.logical_ncols(),
605            "mul_cols_by: column range out of bounds"
606        );
607        assert_eq!(
608            rhs.len(),
609            ncols * ncols,
610            "mul_cols_by: rhs must hold ncols * ncols values"
611        );
612        if ncols == 0 {
613            return;
614        }
615        assert!(
616            ncols <= crate::matrix::MAX_SMALL_COLS,
617            "mul_cols_by: ncols exceeds MAX_SMALL_COLS"
618        );
619        let nrows = self.nrows();
620        let nbatch = self.context.nbatch();
621        let logical_ncols = self.logical_ncols();
622        // A column is contiguous, so a tile of consecutive rows is a contiguous run of each
623        // column: stage the tile's old values (the multiply cannot alias its input and
624        // output), then overwrite in place.
625        let tile = (MUL_COLS_TILE / ncols).max(1);
626        // uninitialised on purpose: the staging loop below writes every element that the
627        // accumulate loop reads, and zeroing the whole buffer costs more than the entire
628        // operation for the two- and three-state systems the ODE solvers spend their time in
629        let mut buf: [MaybeUninit<T>; MUL_COLS_TILE] =
630            [const { MaybeUninit::uninit() }; MUL_COLS_TILE];
631        let data = self.data.as_mut_slice();
632        for b in 0..nbatch {
633            let base = b * logical_ncols * nrows;
634            let mut row = 0;
635            while row < nrows {
636                let rows = tile.min(nrows - row);
637                for j in 0..ncols {
638                    let src = &data[base + j * nrows + row..][..rows];
639                    // SAFETY: `MaybeUninit<T>` is layout-compatible with `T`, and
640                    // `j * rows + rows <= rows * ncols <= MUL_COLS_TILE` (see below), so the
641                    // destination run is in bounds; the two buffers cannot overlap.
642                    unsafe {
643                        std::ptr::copy_nonoverlapping(
644                            src.as_ptr(),
645                            buf.as_mut_ptr().add(j * rows) as *mut T,
646                            rows,
647                        );
648                    }
649                }
650                // SAFETY: the loop above initialised `buf[..rows * ncols]`, and
651                // `rows * ncols <= tile * ncols <= MUL_COLS_TILE` -- the `.max(1)` branch
652                // cannot be taken because `ncols <= MAX_SMALL_COLS`, asserted above.
653                let buf =
654                    unsafe { &*(&buf[..rows * ncols] as *const [MaybeUninit<T>] as *const [T]) };
655                // One dot product per output element, rather than accumulating a column at a
656                // time. Two alternatives were (a) an explicit per-column axpy pass, and (b) handing the staged
657                // tile to nalgebra's `gemm`. Both performed worse for the very small rhs matrix
658                for j in 0..ncols {
659                    let w = &rhs[j * ncols..][..ncols];
660                    let dst = base + j * nrows + row;
661                    for i in 0..rows {
662                        let mut acc = T::zero();
663                        for l in 0..ncols {
664                            acc += buf[l * rows + i] * w[l];
665                        }
666                        data[dst + i] = acc;
667                    }
668                }
669                row += rows;
670            }
671        }
672    }
673
674    fn update_backward_diff(&mut self, order: usize, d: &NalgebraVec<T>) {
675        assert!(order + 2 < self.logical_ncols(), "order out of bounds");
676        self.context
677            .assert_broadcastable_into(d.context.nbatch(), "update_backward_diff");
678        let nb = self.context.nbatch();
679        for b in 0..nb {
680            let base = self.col(b, 0);
681            let d_col = d.batch(b, nb);
682            // diff[:, order+2] = d - diff[:, order+1]
683            {
684                let (src, mut dst) = self.data.columns_range_pair_mut(
685                    base + order + 1..base + order + 2,
686                    base + order + 2..base + order + 3,
687                );
688                dst.column_mut(0).copy_from(&d.data.column(d_col));
689                dst.column_mut(0).axpy(-T::one(), &src.column(0), T::one());
690            }
691            // for i in (0..=order+1).rev(): diff[:, i] += diff[:, i+1]
692            for i in (0..=order + 1).rev() {
693                let (mut dst, src) = self
694                    .data
695                    .columns_range_pair_mut(base + i..base + i + 1, base + i + 1..base + i + 2);
696                dst.column_mut(0).axpy(T::one(), &src.column(0), T::one());
697            }
698        }
699    }
700}
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705
706    #[test]
707    fn test_update_backward_diff() {
708        super::super::tests::test_update_backward_diff::<NalgebraMat<f64>>();
709    }
710
711    #[test]
712    fn test_index_operator() {
713        let mut mat =
714            NalgebraMat::from_vec(2, 2, vec![1.0, 3.0, 2.0, 4.0], NalgebraContext::default());
715        assert_eq!(mat[(1, 0)], 3.0);
716        mat[(1, 0)] = 30.0;
717        assert_eq!(mat.get_index(1, 0), 30.0);
718    }
719
720    #[test]
721    fn test_partition_indices_by_zero_diagonal() {
722        super::super::tests::test_partition_indices_by_zero_diagonal::<NalgebraMat<f64>>();
723    }
724
725    #[test]
726    fn test_resize_cols() {
727        super::super::tests::test_resize_cols::<NalgebraMat<f64>>();
728    }
729
730    #[test]
731    fn test_owned_rhs() {
732        super::super::tests::test_owned_rhs_m::<NalgebraMat<f64>>();
733    }
734
735    #[test]
736    fn test_batched_owned_rhs_broadcast() {
737        super::super::tests::test_batched_owned_rhs_broadcast_m::<NalgebraMat<f64>>(
738            NalgebraContext::with_nbatch(2),
739        );
740    }
741
742    #[test]
743    #[should_panic(expected = "incompatible nbatch")]
744    fn test_batched_owned_rhs_narrow() {
745        super::super::tests::test_batched_owned_rhs_narrow_m::<NalgebraMat<f64>>(
746            NalgebraContext::with_nbatch(2),
747        );
748    }
749
750    super::super::generate_matrix_tests_nonbatched!(nalgebra, NalgebraMat<f64>);
751    super::super::generate_matrix_tests_nonbatched!(nalgebra_f32, NalgebraMat<f32>);
752    super::super::generate_matrix_tests_batched!(
753        nalgebra,
754        NalgebraMat<f64>,
755        NalgebraContext::default(),
756        NalgebraContext::with_nbatch(2)
757    );
758    super::super::generate_matrix_tests_batched!(
759        nalgebra_f32,
760        NalgebraMat<f32>,
761        NalgebraContext::default(),
762        NalgebraContext::with_nbatch(2)
763    );
764    super::super::generate_dense_matrix_tests_nonbatched!(nalgebra, NalgebraMat<f64>);
765    super::super::generate_dense_matrix_tests_nonbatched!(nalgebra_f32, NalgebraMat<f32>);
766    super::super::generate_dense_matrix_tests_batched!(
767        nalgebra,
768        NalgebraMat<f64>,
769        NalgebraContext::default(),
770        NalgebraContext::with_nbatch(2)
771    );
772    super::super::generate_dense_matrix_tests_batched!(
773        nalgebra_f32,
774        NalgebraMat<f32>,
775        NalgebraContext::default(),
776        NalgebraContext::with_nbatch(2)
777    );
778}