Skip to main content

p3_matrix/
lib.rs

1#![doc = include_str!("../README.md")]
2#![no_std]
3
4extern crate alloc;
5
6use alloc::vec::Vec;
7use core::fmt::{Debug, Display, Formatter};
8use core::ops::Deref;
9
10use itertools::Itertools;
11use p3_field::{
12    BasedVectorSpace, ExtensionField, Field, FieldArray, PackedField, PackedFieldExtension,
13    PackedValue, PrimeCharacteristicRing,
14};
15use p3_maybe_rayon::prelude::*;
16use strided::{VerticallyStridedMatrixView, VerticallyStridedRowIndexMap};
17use tracing::instrument;
18
19use crate::dense::RowMajorMatrix;
20
21pub mod bitrev;
22pub mod dense;
23pub mod extension;
24pub mod horizontally_truncated;
25pub mod interpolation;
26pub mod row_index_mapped;
27pub mod stack;
28pub mod strided;
29pub mod util;
30
31/// A simple struct representing the shape of a matrix.
32///
33/// The `Dimensions` type stores the number of columns (`width`) and rows (`height`)
34/// of a matrix. It is commonly used for querying and displaying matrix shapes.
35#[derive(Copy, Clone, PartialEq, Eq)]
36pub struct Dimensions {
37    /// Number of columns in the matrix.
38    pub width: usize,
39    /// Number of rows in the matrix.
40    pub height: usize,
41}
42
43impl Debug for Dimensions {
44    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
45        write!(f, "{}x{}", self.width, self.height)
46    }
47}
48
49impl Display for Dimensions {
50    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
51        write!(f, "{}x{}", self.width, self.height)
52    }
53}
54
55/// A generic trait for two-dimensional matrix-like data structures.
56///
57/// The `Matrix` trait provides a uniform interface for accessing rows, elements,
58/// and computing with matrices in both sequential and parallel contexts. It supports
59/// packing strategies for SIMD optimizations and interaction with extension fields.
60pub trait Matrix<T: Send + Sync + Clone>: Send + Sync {
61    /// Returns the number of columns in the matrix.
62    fn width(&self) -> usize;
63
64    /// Returns the number of rows in the matrix.
65    fn height(&self) -> usize;
66
67    /// Returns the dimensions (width, height) of the matrix.
68    fn dimensions(&self) -> Dimensions {
69        Dimensions {
70            width: self.width(),
71            height: self.height(),
72        }
73    }
74
75    // The methods:
76    // get, get_unchecked, row, row_unchecked, row_subseq_unchecked, row_slice, row_slice_unchecked, row_subslice_unchecked
77    // are all defined in a circular manner so you only need to implement a subset of them.
78    // In particular is is enough to implement just one of: row_unchecked, row_subseq_unchecked
79    //
80    // That being said, most implementations will want to implement several methods for performance reasons.
81
82    /// Returns the element at the given row and column.
83    ///
84    /// Returns `None` if either `r >= height()` or `c >= width()`.
85    #[inline]
86    fn get(&self, r: usize, c: usize) -> Option<T> {
87        (r < self.height() && c < self.width()).then(|| unsafe {
88            // Safety: Clearly `r < self.height()` and `c < self.width()`.
89            self.get_unchecked(r, c)
90        })
91    }
92
93    /// Returns the element at the given row and column.
94    ///
95    /// For a safe alternative, see [`Self::get`].
96    ///
97    /// # Safety
98    /// The caller must ensure that `r < self.height()` and `c < self.width()`.
99    /// Breaking any of these assumptions is considered undefined behaviour.
100    #[inline]
101    unsafe fn get_unchecked(&self, r: usize, c: usize) -> T {
102        unsafe { self.row_slice_unchecked(r)[c].clone() }
103    }
104
105    /// Returns an iterator over the elements of the `r`-th row.
106    ///
107    /// The iterator will have `self.width()` elements.
108    ///
109    /// Returns `None` if `r >= height()`.
110    #[inline]
111    fn row(
112        &self,
113        r: usize,
114    ) -> Option<impl IntoIterator<Item = T, IntoIter = impl Iterator<Item = T> + Send + Sync>> {
115        (r < self.height()).then(|| unsafe {
116            // Safety: Clearly `r < self.height()`.
117            self.row_unchecked(r)
118        })
119    }
120
121    /// Returns an iterator over the elements of the `r`-th row.
122    ///
123    /// The iterator will have `self.width()` elements.
124    ///
125    /// For a safe alternative, see [`Self::row`].
126    ///
127    /// # Safety
128    /// The caller must ensure that `r < self.height()`.
129    /// Breaking this assumption is considered undefined behaviour.
130    #[inline]
131    unsafe fn row_unchecked(
132        &self,
133        r: usize,
134    ) -> impl IntoIterator<Item = T, IntoIter = impl Iterator<Item = T> + Send + Sync> {
135        unsafe { self.row_subseq_unchecked(r, 0, self.width()) }
136    }
137
138    /// Returns an iterator over the elements of the `r`-th row from position `start` to `end`.
139    ///
140    /// When `start = 0` and `end = width()`, this is equivalent to [`Self::row_unchecked`].
141    ///
142    /// For a safe alternative, use [`Self::row`], along with the `skip` and `take` iterator methods.
143    ///
144    /// # Safety
145    /// The caller must ensure that `r < self.height()` and `start <= end <= self.width()`.
146    /// Breaking any of these assumptions is considered undefined behaviour.
147    #[inline]
148    unsafe fn row_subseq_unchecked(
149        &self,
150        r: usize,
151        start: usize,
152        end: usize,
153    ) -> impl IntoIterator<Item = T, IntoIter = impl Iterator<Item = T> + Send + Sync> {
154        unsafe {
155            self.row_unchecked(r)
156                .into_iter()
157                .skip(start)
158                .take(end - start)
159        }
160    }
161
162    /// Returns the elements of the `r`-th row as something which can be coerced to a slice.
163    ///
164    /// Returns `None` if `r >= height()`.
165    #[inline]
166    fn row_slice(&self, r: usize) -> Option<impl Deref<Target = [T]>> {
167        (r < self.height()).then(|| unsafe {
168            // Safety: Clearly `r < self.height()`.
169            self.row_slice_unchecked(r)
170        })
171    }
172
173    /// Returns the elements of the `r`-th row as something which can be coerced to a slice.
174    ///
175    /// For a safe alternative, see [`Self::row_slice`].
176    ///
177    /// # Safety
178    /// The caller must ensure that `r < self.height()`.
179    /// Breaking this assumption is considered undefined behaviour.
180    #[inline]
181    unsafe fn row_slice_unchecked(&self, r: usize) -> impl Deref<Target = [T]> {
182        unsafe { self.row_subslice_unchecked(r, 0, self.width()) }
183    }
184
185    /// Returns a subset of elements of the `r`-th row as something which can be coerced to a slice.
186    ///
187    /// When `start = 0` and `end = width()`, this is equivalent to [`Self::row_slice_unchecked`].
188    ///
189    /// For a safe alternative, see [`Self::row_slice`].
190    ///
191    /// # Safety
192    /// The caller must ensure that `r < self.height()` and `start <= end <= self.width()`.
193    /// Breaking any of these assumptions is considered undefined behaviour.
194    #[inline]
195    unsafe fn row_subslice_unchecked(
196        &self,
197        r: usize,
198        start: usize,
199        end: usize,
200    ) -> impl Deref<Target = [T]> {
201        unsafe {
202            self.row_subseq_unchecked(r, start, end)
203                .into_iter()
204                .collect_vec()
205        }
206    }
207
208    /// Returns an iterator over all rows in the matrix.
209    #[inline]
210    fn rows(&self) -> impl Iterator<Item = impl Iterator<Item = T>> + Send + Sync {
211        unsafe {
212            // Safety: `r` always satisfies `r < self.height()`.
213            (0..self.height()).map(move |r| self.row_unchecked(r).into_iter())
214        }
215    }
216
217    /// Returns a parallel iterator over all rows in the matrix.
218    #[inline]
219    fn par_rows(
220        &self,
221    ) -> impl IndexedParallelIterator<Item = impl Iterator<Item = T>> + Send + Sync {
222        unsafe {
223            // Safety: `r` always satisfies `r < self.height()`.
224            (0..self.height())
225                .into_par_iter()
226                .map(move |r| self.row_unchecked(r).into_iter())
227        }
228    }
229
230    /// Collect the elements of the rows `r` through `r + c`. If anything is larger than `self.height()`
231    /// simply wrap around to the beginning of the matrix.
232    fn wrapping_row_slices(&self, r: usize, c: usize) -> Vec<impl Deref<Target = [T]>> {
233        unsafe {
234            // Safety: Thank to the `%`, the rows index is always less than `self.height()`.
235            (0..c)
236                .map(|i| self.row_slice_unchecked((r + i) % self.height()))
237                .collect_vec()
238        }
239    }
240
241    /// Returns an iterator over the first row of the matrix.
242    ///
243    /// Returns None if `height() == 0`.
244    #[inline]
245    fn first_row(
246        &self,
247    ) -> Option<impl IntoIterator<Item = T, IntoIter = impl Iterator<Item = T> + Send + Sync>> {
248        self.row(0)
249    }
250
251    /// Returns an iterator over the last row of the matrix.
252    ///
253    /// Returns None if `height() == 0`.
254    #[inline]
255    fn last_row(
256        &self,
257    ) -> Option<impl IntoIterator<Item = T, IntoIter = impl Iterator<Item = T> + Send + Sync>> {
258        if self.height() == 0 {
259            None
260        } else {
261            // Safety: Clearly `self.height() - 1 < self.height()`.
262            unsafe { Some(self.row_unchecked(self.height() - 1)) }
263        }
264    }
265
266    /// Converts the matrix into a `RowMajorMatrix` by collecting all rows into a single vector.
267    fn to_row_major_matrix(self) -> RowMajorMatrix<T>
268    where
269        Self: Sized,
270        T: Clone,
271    {
272        RowMajorMatrix::new(self.rows().flatten().collect(), self.width())
273    }
274
275    /// Get a packed iterator over the `r`-th row.
276    ///
277    /// If the row length is not divisible by the packing width, the final elements
278    /// are returned as a base iterator with length `<= P::WIDTH - 1`.
279    ///
280    /// # Panics
281    /// Panics if `r >= height()`.
282    fn horizontally_packed_row<'a, P>(
283        &'a self,
284        r: usize,
285    ) -> (
286        impl Iterator<Item = P> + Send + Sync,
287        impl Iterator<Item = T> + Send + Sync,
288    )
289    where
290        P: PackedValue<Value = T>,
291        T: Clone + 'a,
292    {
293        assert!(r < self.height(), "Row index out of bounds.");
294        let num_packed = self.width() / P::WIDTH;
295        unsafe {
296            // Safety: We have already checked that `r < height()`.
297            let mut iter = self
298                .row_subseq_unchecked(r, 0, num_packed * P::WIDTH)
299                .into_iter();
300
301            // array::from_fn is guaranteed to always call in order.
302            let packed =
303                (0..num_packed).map(move |_| P::from_fn(|_| iter.next().unwrap_unchecked()));
304
305            let sfx = self
306                .row_subseq_unchecked(r, num_packed * P::WIDTH, self.width())
307                .into_iter();
308            (packed, sfx)
309        }
310    }
311
312    /// Get a packed iterator over the `r`-th row.
313    ///
314    /// If the row length is not divisible by the packing width, the final entry will be zero-padded.
315    ///
316    /// # Panics
317    /// Panics if `r >= height()`.
318    fn padded_horizontally_packed_row<'a, P>(
319        &'a self,
320        r: usize,
321    ) -> impl Iterator<Item = P> + Send + Sync
322    where
323        P: PackedValue<Value = T>,
324        T: Clone + Default + 'a,
325    {
326        let mut row_iter = self.row(r).expect("Row index out of bounds.").into_iter();
327        let num_elems = self.width().div_ceil(P::WIDTH);
328        // array::from_fn is guaranteed to always call in order.
329        (0..num_elems).map(move |_| P::from_fn(|_| row_iter.next().unwrap_or_default()))
330    }
331
332    /// Get a parallel iterator over all packed rows of the matrix.
333    ///
334    /// If the matrix width is not divisible by the packing width, the final elements
335    /// of each row are returned as a base iterator with length `<= P::WIDTH - 1`.
336    fn par_horizontally_packed_rows<'a, P>(
337        &'a self,
338    ) -> impl IndexedParallelIterator<
339        Item = (
340            impl Iterator<Item = P> + Send + Sync,
341            impl Iterator<Item = T> + Send + Sync,
342        ),
343    >
344    where
345        P: PackedValue<Value = T>,
346        T: Clone + 'a,
347    {
348        (0..self.height())
349            .into_par_iter()
350            .map(|r| self.horizontally_packed_row(r))
351    }
352
353    /// Get a parallel iterator over all packed rows of the matrix.
354    ///
355    /// If the matrix width is not divisible by the packing width, the final entry of each row will be zero-padded.
356    fn par_padded_horizontally_packed_rows<'a, P>(
357        &'a self,
358    ) -> impl IndexedParallelIterator<Item = impl Iterator<Item = P> + Send + Sync>
359    where
360        P: PackedValue<Value = T>,
361        T: Clone + Default + 'a,
362    {
363        (0..self.height())
364            .into_par_iter()
365            .map(|r| self.padded_horizontally_packed_row(r))
366    }
367
368    /// Pack together a collection of adjacent rows from the matrix.
369    ///
370    /// Returns an iterator whose i'th element is packing of the i'th element of the
371    /// rows r through r + P::WIDTH - 1. If we exceed the height of the matrix,
372    /// wrap around and include initial rows.
373    #[inline]
374    fn vertically_packed_row<P>(&self, r: usize) -> impl Iterator<Item = P>
375    where
376        T: Copy,
377        P: PackedValue<Value = T>,
378    {
379        // Precompute row slices once to minimize redundant calls and improve performance.
380        let rows = self.wrapping_row_slices(r, P::WIDTH);
381
382        // Using precomputed rows avoids repeatedly calling `row_slice`, which is costly.
383        (0..self.width()).map(move |c| P::from_fn(|i| rows[i][c]))
384    }
385
386    /// Pack together a collection of rows and "next" rows from the matrix.
387    ///
388    /// Returns a vector corresponding to 2 packed rows. The i'th element of the first
389    /// row contains the packing of the i'th element of the rows r through r + P::WIDTH - 1.
390    /// The i'th element of the second row contains the packing of the i'th element of the
391    /// rows r + step through r + step + P::WIDTH - 1. If at some point we exceed the
392    /// height of the matrix, wrap around and include initial rows.
393    #[inline]
394    fn vertically_packed_row_pair<P>(&self, r: usize, step: usize) -> Vec<P>
395    where
396        T: Copy,
397        P: PackedValue<Value = T>,
398    {
399        // Whilst it would appear that this can be replaced by two calls to vertically_packed_row
400        // tests seem to indicate that combining them in the same function is slightly faster.
401        // It's probably allowing the compiler to make some optimizations on the fly.
402
403        let rows = self.wrapping_row_slices(r, P::WIDTH);
404        let next_rows = self.wrapping_row_slices(r + step, P::WIDTH);
405
406        (0..self.width())
407            .map(|c| P::from_fn(|i| rows[i][c]))
408            .chain((0..self.width()).map(|c| P::from_fn(|i| next_rows[i][c])))
409            .collect_vec()
410    }
411
412    /// Returns a view over a vertically strided submatrix.
413    ///
414    /// The view selects rows using `r = offset + i * stride` for each `i`.
415    fn vertically_strided(self, stride: usize, offset: usize) -> VerticallyStridedMatrixView<Self>
416    where
417        Self: Sized,
418    {
419        VerticallyStridedRowIndexMap::new_view(self, stride, offset)
420    }
421
422    /// Compute Mᵀv, aka premultiply this matrix by the given vector,
423    /// aka scale each row by the corresponding entry in `v` and take the sum across rows.
424    /// `v` can be a vector of extension elements.
425    #[instrument(level = "debug", skip_all, fields(dims = %self.dimensions()))]
426    fn columnwise_dot_product<EF>(&self, v: &[EF]) -> Vec<EF>
427    where
428        T: Field,
429        EF: ExtensionField<T>,
430    {
431        assert_eq!(v.len(), self.height());
432
433        // Below this many total elements, the rayon fork-join and SIMD-packing machinery
434        // costs more than the dot product itself; fall back to a plain scalar accumulation.
435        // Gating on total elements (rather than height alone) also covers wide-but-short
436        // matrices, where a per-row cost proportional to width still adds up.
437        const SMALL_ELEMS: usize = 256;
438        if self.height().saturating_mul(self.width()) <= SMALL_ELEMS {
439            let mut acc = EF::zero_vec(self.width());
440            for (row, &scale) in self.rows().zip(v) {
441                for (l, r) in acc.iter_mut().zip(row) {
442                    *l += scale * r;
443                }
444            }
445            return acc;
446        }
447
448        let packed_width = self.width().div_ceil(T::Packing::WIDTH);
449
450        let packed_result = self
451            .par_padded_horizontally_packed_rows::<T::Packing>()
452            .zip(v)
453            .par_fold_reduce(
454                || EF::ExtensionPacking::zero_vec(packed_width),
455                |mut acc, (row, &scale)| {
456                    let scale: EF::ExtensionPacking = scale.into();
457                    acc.iter_mut().zip(row).for_each(|(l, r)| *l += scale * r);
458                    acc
459                },
460                |mut acc_l, acc_r| {
461                    acc_l.iter_mut().zip(&acc_r).for_each(|(l, r)| *l += *r);
462                    acc_l
463                },
464            );
465
466        EF::ExtensionPacking::to_ext_iter(packed_result)
467            .take(self.width())
468            .collect()
469    }
470
471    /// Compute Mᵀ · [v₀, v₁, ..., vₙ₋₁] for N weight vectors simultaneously.
472    ///
473    /// Computes `result[col][j] = Σᵣ M[r, col] · vⱼ[r]` for all columns and all j ∈ [0, N).
474    ///
475    /// Batching N weight vectors reduces memory bandwidth: each matrix row is loaded once
476    /// instead of N times. Uses SIMD packing (width W) to process W columns in parallel.
477    #[instrument(level = "debug", skip_all, fields(dims = %self.dimensions()))]
478    fn columnwise_dot_product_batched<EF, const N: usize>(
479        &self,
480        vs: &[FieldArray<EF, N>],
481    ) -> Vec<FieldArray<EF, N>>
482    where
483        T: Field,
484        EF: ExtensionField<T>,
485    {
486        assert_eq!(vs.len(), self.height());
487
488        let packed_width = self.width().div_ceil(T::Packing::WIDTH);
489        let height = self.height();
490
491        // Split the rows into a bounded number of contiguous chunks; each task runs the
492        // field's columnwise kernel serially over its chunk (letting it defer modular
493        // reductions across rows) and the per-task accumulators are summed at the end.
494        let num_chunks = (4 * current_num_threads()).clamp(1, height.max(1));
495        let chunk_rows = height.div_ceil(num_chunks);
496
497        let packed_results: Vec<EF::ExtensionPacking> =
498            (0..num_chunks).into_par_iter().par_fold_reduce(
499                || EF::ExtensionPacking::zero_vec(packed_width * N),
500                |mut acc, chunk| {
501                    let rows = chunk * chunk_rows..((chunk + 1) * chunk_rows).min(height);
502                    T::batched_columnwise_dot_product::<EF, _, _, N>(
503                        &mut acc,
504                        rows.map(|r| {
505                            (
506                                self.padded_horizontally_packed_row::<T::Packing>(r),
507                                vs[r].0,
508                            )
509                        }),
510                    );
511                    acc
512                },
513                |mut acc_l, acc_r| {
514                    acc_l.iter_mut().zip(&acc_r).for_each(|(lj, rj)| *lj += *rj);
515                    acc_l
516                },
517            );
518
519        // Unpack: chunk[j].lane(i) → result[c·W + i][j] for column batch c
520        packed_results
521            .chunks(N)
522            .flat_map(|chunk| {
523                (0..T::Packing::WIDTH)
524                    .map(move |lane| FieldArray::from_fn(|j| chunk[j].extract(lane)))
525            })
526            .take(self.width())
527            .collect()
528    }
529
530    /// Compute the matrix vector product `M . vec`, aka take the dot product of each
531    /// row of `M` by `vec`. If the length of `vec` is longer than the width of `M`,
532    /// `vec` is truncated to the first `width()` elements.
533    ///
534    /// We make use of `PackedFieldExtension` to speed up computations. Thus `vec` is passed in as
535    /// a slice of `PackedFieldExtension` elements.
536    ///
537    /// # Panics
538    /// This function panics if the length of `vec` is less than `self.width().div_ceil(T::Packing::WIDTH)`.
539    fn rowwise_packed_dot_product<EF>(
540        &self,
541        vec: &[EF::ExtensionPacking],
542    ) -> impl IndexedParallelIterator<Item = EF>
543    where
544        T: Field,
545        EF: ExtensionField<T>,
546    {
547        // The length of a `padded_horizontally_packed_row` is `self.width().div_ceil(T::Packing::WIDTH)`.
548        assert!(vec.len() >= self.width().div_ceil(T::Packing::WIDTH));
549
550        // Instead of creating N intermediate ExtPacking products and summing them,
551        // we track D separate BasePacking accumulators (one per extension coefficient).
552        self.par_padded_horizontally_packed_rows::<T::Packing>()
553            .map(move |row_packed| {
554                // Get the extension dimension from the first vec element's coefficients
555                let d = <EF::ExtensionPacking as BasedVectorSpace<T::Packing>>::DIMENSION;
556
557                // Accumulate coefficient-wise: for each (v, r) pair, acc[i] += v.coefficient(i) * r
558                let coeff_accs = T::Packing::coeffwise_dot_product(
559                    d,
560                    vec.iter()
561                        .zip(row_packed)
562                        .map(|(v, r)| (v.as_basis_coefficients_slice(), r)),
563                );
564
565                // Construct the result ExtPacking from the accumulators and sum the coefficients.
566                let packed_result =
567                    EF::ExtensionPacking::from_basis_coefficients_fn(|i| coeff_accs[i]);
568                EF::ExtensionPacking::to_ext_iter([packed_result]).sum()
569            })
570    }
571}
572
573#[cfg(test)]
574mod tests {
575    use alloc::vec::Vec;
576    use alloc::{format, vec};
577
578    use itertools::izip;
579    use p3_baby_bear::BabyBear;
580    use p3_field::PrimeCharacteristicRing;
581    use p3_field::extension::BinomialExtensionField;
582    use rand::SeedableRng;
583    use rand::rngs::SmallRng;
584
585    use super::*;
586
587    #[test]
588    fn test_columnwise_dot_product() {
589        type F = BabyBear;
590        type EF = BinomialExtensionField<BabyBear, 4>;
591
592        let mut rng = SmallRng::seed_from_u64(1);
593        let m = RowMajorMatrix::<F>::rand(&mut rng, 1 << 8, 1 << 4);
594        let v = RowMajorMatrix::<EF>::rand(&mut rng, 1 << 8, 1).values;
595
596        let mut expected = EF::zero_vec(m.width());
597        for (row, &scale) in izip!(m.rows(), &v) {
598            for (l, r) in izip!(&mut expected, row) {
599                *l += scale * r;
600            }
601        }
602
603        assert_eq!(m.columnwise_dot_product(&v), expected);
604    }
605
606    #[test]
607    fn test_columnwise_dot_product_small_height() {
608        type F = BabyBear;
609        type EF = BinomialExtensionField<BabyBear, 4>;
610
611        let mut rng = SmallRng::seed_from_u64(2);
612
613        // Cover heights below, at, and just above the small-height serial threshold.
614        for height in [0, 1, 3, 16, 17] {
615            let m = RowMajorMatrix::<F>::rand(&mut rng, height, 1 << 4);
616            let v = RowMajorMatrix::<EF>::rand(&mut rng, height, 1).values;
617
618            let mut expected = EF::zero_vec(m.width());
619            for (row, &scale) in izip!(m.rows(), &v) {
620                for (l, r) in izip!(&mut expected, row) {
621                    *l += scale * r;
622                }
623            }
624
625            assert_eq!(m.columnwise_dot_product(&v), expected, "height = {height}");
626        }
627    }
628
629    #[test]
630    fn test_columnwise_dot_product_batched() {
631        type F = BabyBear;
632        type EF = BinomialExtensionField<BabyBear, 4>;
633
634        let mut rng = SmallRng::seed_from_u64(1);
635        let m = RowMajorMatrix::<F>::rand(&mut rng, 1 << 8, 1 << 4);
636        let v1 = RowMajorMatrix::<EF>::rand(&mut rng, 1 << 8, 1).values;
637        let v2 = RowMajorMatrix::<EF>::rand(&mut rng, 1 << 8, 1).values;
638
639        // Compute expected via two separate calls
640        let expected1 = m.columnwise_dot_product(&v1);
641        let expected2 = m.columnwise_dot_product(&v2);
642
643        // Compute via batched call - returns Vec<[EF; 2]> where result[col] = [dot1, dot2]
644        let vs: Vec<FieldArray<EF, 2>> = v1
645            .into_iter()
646            .zip(v2)
647            .map(|(a, b)| FieldArray([a, b]))
648            .collect();
649        let results = m.columnwise_dot_product_batched::<EF, 2>(&vs);
650
651        // Extract each point's results
652        let result1: Vec<EF> = results.iter().map(|r| r[0]).collect();
653        let result2: Vec<EF> = results.iter().map(|r| r[1]).collect();
654
655        assert_eq!(result1, expected1);
656        assert_eq!(result2, expected2);
657    }
658
659    // Mock implementation for testing purposes
660    struct MockMatrix {
661        data: Vec<Vec<u32>>,
662        width: usize,
663        height: usize,
664    }
665
666    impl Matrix<u32> for MockMatrix {
667        fn width(&self) -> usize {
668            self.width
669        }
670
671        fn height(&self) -> usize {
672            self.height
673        }
674
675        unsafe fn row_unchecked(
676            &self,
677            r: usize,
678        ) -> impl IntoIterator<Item = u32, IntoIter = impl Iterator<Item = u32> + Send + Sync>
679        {
680            // Just a mock implementation so we just do the easy safe thing.
681            self.data[r].clone()
682        }
683    }
684
685    #[test]
686    fn test_dimensions() {
687        let dims = Dimensions {
688            width: 3,
689            height: 5,
690        };
691        assert_eq!(dims.width, 3);
692        assert_eq!(dims.height, 5);
693        assert_eq!(format!("{dims:?}"), "3x5");
694        assert_eq!(format!("{dims}"), "3x5");
695    }
696
697    #[test]
698    fn test_mock_matrix_dimensions() {
699        let matrix = MockMatrix {
700            data: vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]],
701            width: 3,
702            height: 3,
703        };
704        assert_eq!(matrix.width(), 3);
705        assert_eq!(matrix.height(), 3);
706        assert_eq!(
707            matrix.dimensions(),
708            Dimensions {
709                width: 3,
710                height: 3
711            }
712        );
713    }
714
715    #[test]
716    fn test_first_row() {
717        let matrix = MockMatrix {
718            data: vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]],
719            width: 3,
720            height: 3,
721        };
722        let mut first_row = matrix.first_row().unwrap().into_iter();
723        assert_eq!(first_row.next(), Some(1));
724        assert_eq!(first_row.next(), Some(2));
725        assert_eq!(first_row.next(), Some(3));
726    }
727
728    #[test]
729    fn test_last_row() {
730        let matrix = MockMatrix {
731            data: vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]],
732            width: 3,
733            height: 3,
734        };
735        let mut last_row = matrix.last_row().unwrap().into_iter();
736        assert_eq!(last_row.next(), Some(7));
737        assert_eq!(last_row.next(), Some(8));
738        assert_eq!(last_row.next(), Some(9));
739    }
740
741    #[test]
742    fn test_first_last_row_empty_matrix() {
743        let matrix = MockMatrix {
744            data: vec![],
745            width: 3,
746            height: 0,
747        };
748        let first_row = matrix.first_row();
749        let last_row = matrix.last_row();
750        assert!(first_row.is_none());
751        assert!(last_row.is_none());
752    }
753
754    #[test]
755    fn test_to_row_major_matrix() {
756        let matrix = MockMatrix {
757            data: vec![vec![1, 2], vec![3, 4]],
758            width: 2,
759            height: 2,
760        };
761        let row_major = matrix.to_row_major_matrix();
762        assert_eq!(row_major.values, vec![1, 2, 3, 4]);
763        assert_eq!(row_major.width, 2);
764    }
765
766    #[test]
767    fn test_matrix_get_methods() {
768        let matrix = MockMatrix {
769            data: vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]],
770            width: 3,
771            height: 3,
772        };
773        assert_eq!(matrix.get(0, 0), Some(1));
774        assert_eq!(matrix.get(1, 2), Some(6));
775        assert_eq!(matrix.get(2, 1), Some(8));
776
777        unsafe {
778            assert_eq!(matrix.get_unchecked(0, 1), 2);
779            assert_eq!(matrix.get_unchecked(1, 0), 4);
780            assert_eq!(matrix.get_unchecked(2, 2), 9);
781        }
782
783        assert_eq!(matrix.get(3, 0), None); // Height out of bounds
784        assert_eq!(matrix.get(0, 3), None); // Width out of bounds
785    }
786
787    #[test]
788    fn test_matrix_row_methods_iteration() {
789        let matrix = MockMatrix {
790            data: vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]],
791            width: 3,
792            height: 3,
793        };
794
795        let mut row_iter = matrix.row(1).unwrap().into_iter();
796        assert_eq!(row_iter.next(), Some(4));
797        assert_eq!(row_iter.next(), Some(5));
798        assert_eq!(row_iter.next(), Some(6));
799        assert_eq!(row_iter.next(), None);
800
801        unsafe {
802            let mut row_iter_unchecked = matrix.row_unchecked(2).into_iter();
803            assert_eq!(row_iter_unchecked.next(), Some(7));
804            assert_eq!(row_iter_unchecked.next(), Some(8));
805            assert_eq!(row_iter_unchecked.next(), Some(9));
806            assert_eq!(row_iter_unchecked.next(), None);
807
808            let mut row_iter_subset = matrix.row_subseq_unchecked(0, 1, 3).into_iter();
809            assert_eq!(row_iter_subset.next(), Some(2));
810            assert_eq!(row_iter_subset.next(), Some(3));
811            assert_eq!(row_iter_subset.next(), None);
812        }
813
814        assert!(matrix.row(3).is_none()); // Height out of bounds
815    }
816
817    #[test]
818    fn test_row_slice_methods() {
819        let matrix = MockMatrix {
820            data: vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]],
821            width: 3,
822            height: 3,
823        };
824        let row_slice = matrix.row_slice(1).unwrap();
825        assert_eq!(*row_slice, [4, 5, 6]);
826        unsafe {
827            let row_slice_unchecked = matrix.row_slice_unchecked(2);
828            assert_eq!(*row_slice_unchecked, [7, 8, 9]);
829
830            let row_subslice = matrix.row_subslice_unchecked(0, 1, 2);
831            assert_eq!(*row_subslice, [2]);
832        }
833
834        assert!(matrix.row_slice(3).is_none()); // Height out of bounds
835    }
836
837    #[test]
838    fn test_matrix_rows() {
839        let matrix = MockMatrix {
840            data: vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]],
841            width: 3,
842            height: 3,
843        };
844
845        let all_rows: Vec<Vec<u32>> = matrix.rows().map(|row| row.collect()).collect();
846        assert_eq!(all_rows, vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]]);
847    }
848
849    #[test]
850    fn test_rowwise_packed_dot_product() {
851        use p3_field::PackedFieldExtension;
852
853        type F = BabyBear;
854        type EF = BinomialExtensionField<BabyBear, 4>;
855        type PF = <F as p3_field::Field>::Packing;
856        type EFPacked = <EF as p3_field::ExtensionField<F>>::ExtensionPacking;
857
858        let mut rng = SmallRng::seed_from_u64(42);
859
860        // Test with various matrix dimensions to cover edge cases.
861        for (height, width) in [(32, 16), (64, 128), (128, 17), (256, 255)] {
862            let m = RowMajorMatrix::<F>::rand(&mut rng, height, width);
863            let v = RowMajorMatrix::<EF>::rand(&mut rng, width, 1).values;
864
865            // Compute expected result naively: for each row, compute dot product with v.
866            let expected: Vec<EF> = m
867                .rows()
868                .map(|row| {
869                    row.into_iter()
870                        .zip(v.iter())
871                        .map(|(r, &ve)| ve * r)
872                        .sum::<EF>()
873                })
874                .collect();
875
876            // Pack the vector for the optimized function.
877            let packed_v: Vec<EFPacked> = v
878                .chunks(<PF as PackedValue>::WIDTH)
879                .map(|chunk| {
880                    let mut padded = EF::zero_vec(<PF as PackedValue>::WIDTH);
881                    padded[..chunk.len()].copy_from_slice(chunk);
882                    EFPacked::from_ext_slice(&padded)
883                })
884                .collect();
885
886            // Compute using the optimized function.
887            let result: Vec<EF> = m.rowwise_packed_dot_product::<EF>(&packed_v).collect();
888
889            assert_eq!(result, expected, "Mismatch for matrix {}x{}", height, width);
890        }
891    }
892}