Skip to main content

fdars_core/
matrix.rs

1//! Column-major matrix type for functional data analysis.
2//!
3//! [`FdMatrix`] provides safe, dimension-tracked access to the flat column-major
4//! data layout used throughout this crate. It eliminates manual `data[i + j * n]`
5//! index arithmetic and carries dimensions alongside the data.
6
7use nalgebra::DMatrix;
8
9/// Column-major matrix for functional data.
10///
11/// Stores data in a flat `Vec<f64>` with column-major (Fortran) layout:
12/// element `(row, col)` is at index `row + col * nrows`.
13///
14/// # Conventions
15///
16/// For functional data, rows typically represent observations and columns
17/// represent evaluation points. For 2D surfaces with `m1 x m2` grids,
18/// the surface is flattened into `m1 * m2` columns.
19///
20/// # Examples
21///
22/// ```
23/// use fdars_core::matrix::FdMatrix;
24///
25/// // 3 observations, 4 evaluation points
26/// let data = vec![
27///     1.0, 2.0, 3.0,  // column 0 (all obs at point 0)
28///     4.0, 5.0, 6.0,  // column 1
29///     7.0, 8.0, 9.0,  // column 2
30///     10.0, 11.0, 12.0, // column 3
31/// ];
32/// let mat = FdMatrix::from_column_major(data, 3, 4).unwrap();
33///
34/// assert_eq!(mat[(0, 0)], 1.0);  // obs 0 at point 0
35/// assert_eq!(mat[(1, 2)], 8.0);  // obs 1 at point 2
36/// assert_eq!(mat.column(0), &[1.0, 2.0, 3.0]);
37/// ```
38#[derive(Debug, Clone, PartialEq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub struct FdMatrix {
41    data: Vec<f64>,
42    nrows: usize,
43    ncols: usize,
44}
45
46impl FdMatrix {
47    /// Create from flat column-major data with dimension validation.
48    ///
49    /// Returns `Err` if `data.len() != nrows * ncols`.
50    pub fn from_column_major(
51        data: Vec<f64>,
52        nrows: usize,
53        ncols: usize,
54    ) -> Result<Self, crate::FdarError> {
55        if data.len() != nrows * ncols {
56            return Err(crate::FdarError::InvalidDimension {
57                parameter: "data",
58                expected: format!("{}", nrows * ncols),
59                actual: format!("{}", data.len()),
60            });
61        }
62        Ok(Self { data, nrows, ncols })
63    }
64
65    /// Create from a borrowed slice (copies the data).
66    ///
67    /// Returns `Err` if `data.len() != nrows * ncols`.
68    pub fn from_slice(data: &[f64], nrows: usize, ncols: usize) -> Result<Self, crate::FdarError> {
69        if data.len() != nrows * ncols {
70            return Err(crate::FdarError::InvalidDimension {
71                parameter: "data",
72                expected: format!("{}", nrows * ncols),
73                actual: format!("{}", data.len()),
74            });
75        }
76        Ok(Self {
77            data: data.to_vec(),
78            nrows,
79            ncols,
80        })
81    }
82
83    /// Create a zero-filled matrix.
84    pub fn zeros(nrows: usize, ncols: usize) -> Self {
85        Self {
86            data: vec![0.0; nrows * ncols],
87            nrows,
88            ncols,
89        }
90    }
91
92    /// Number of rows.
93    #[inline]
94    pub fn nrows(&self) -> usize {
95        self.nrows
96    }
97
98    /// Number of columns.
99    #[inline]
100    pub fn ncols(&self) -> usize {
101        self.ncols
102    }
103
104    /// Dimensions as `(nrows, ncols)`.
105    #[inline]
106    pub fn shape(&self) -> (usize, usize) {
107        (self.nrows, self.ncols)
108    }
109
110    /// Total number of elements.
111    #[inline]
112    pub fn len(&self) -> usize {
113        self.data.len()
114    }
115
116    /// Whether the matrix is empty.
117    #[inline]
118    pub fn is_empty(&self) -> bool {
119        self.data.is_empty()
120    }
121
122    /// Get a contiguous column slice (zero-copy).
123    ///
124    /// # Panics
125    /// Panics if `col >= ncols`.
126    #[inline]
127    pub fn column(&self, col: usize) -> &[f64] {
128        let start = col * self.nrows;
129        &self.data[start..start + self.nrows]
130    }
131
132    /// Get a mutable contiguous column slice (zero-copy).
133    ///
134    /// # Panics
135    /// Panics if `col >= ncols`.
136    #[inline]
137    pub fn column_mut(&mut self, col: usize) -> &mut [f64] {
138        let start = col * self.nrows;
139        &mut self.data[start..start + self.nrows]
140    }
141
142    /// Extract a single row as a new `Vec<f64>`.
143    ///
144    /// This is an O(ncols) operation because rows are not contiguous
145    /// in column-major layout.
146    pub fn row(&self, row: usize) -> Vec<f64> {
147        (0..self.ncols)
148            .map(|j| self.data[row + j * self.nrows])
149            .collect()
150    }
151
152    /// Copy a single row into a pre-allocated buffer (zero allocation).
153    ///
154    /// # Panics
155    /// Panics if `buf.len() < ncols` or `row >= nrows`.
156    #[inline]
157    pub fn row_to_buf(&self, row: usize, buf: &mut [f64]) {
158        debug_assert!(
159            row < self.nrows,
160            "row {row} out of bounds for {} rows",
161            self.nrows
162        );
163        debug_assert!(
164            buf.len() >= self.ncols,
165            "buffer len {} < ncols {}",
166            buf.len(),
167            self.ncols
168        );
169        let n = self.nrows;
170        for j in 0..self.ncols {
171            buf[j] = self.data[row + j * n];
172        }
173    }
174
175    /// Compute the dot product of two rows without materializing either one.
176    ///
177    /// The rows may come from different matrices (which must have the same `ncols`).
178    ///
179    /// # Panics
180    /// Panics (in debug) if `row_a >= self.nrows`, `row_b >= other.nrows`,
181    /// or `self.ncols != other.ncols`.
182    #[inline]
183    pub fn row_dot(&self, row_a: usize, other: &FdMatrix, row_b: usize) -> f64 {
184        debug_assert_eq!(self.ncols, other.ncols, "ncols mismatch in row_dot");
185        let na = self.nrows;
186        let nb = other.nrows;
187        let mut sum = 0.0;
188        for j in 0..self.ncols {
189            sum += self.data[row_a + j * na] * other.data[row_b + j * nb];
190        }
191        sum
192    }
193
194    /// Compute the squared L2 distance between two rows without allocation.
195    ///
196    /// Equivalent to `||self.row(row_a) - other.row(row_b)||^2` but without
197    /// materializing either row vector.
198    ///
199    /// # Panics
200    /// Panics (in debug) if `row_a >= self.nrows`, `row_b >= other.nrows`,
201    /// or `self.ncols != other.ncols`.
202    #[inline]
203    pub fn row_l2_sq(&self, row_a: usize, other: &FdMatrix, row_b: usize) -> f64 {
204        debug_assert_eq!(self.ncols, other.ncols, "ncols mismatch in row_l2_sq");
205        let na = self.nrows;
206        let nb = other.nrows;
207        let mut sum = 0.0;
208        for j in 0..self.ncols {
209            let d = self.data[row_a + j * na] - other.data[row_b + j * nb];
210            sum += d * d;
211        }
212        sum
213    }
214
215    /// Iterate over rows, yielding each row as a `Vec<f64>`.
216    ///
217    /// More efficient than [`to_row_major()`](Self::to_row_major) when only a
218    /// subset of rows are needed or when processing rows one at a time, because
219    /// it materializes only one row at a time instead of allocating the entire
220    /// transposed matrix up front.
221    ///
222    /// Because `FdMatrix` uses column-major storage, row elements are not
223    /// contiguous and a zero-copy row slice is not possible. Each yielded
224    /// `Vec<f64>` is an O(ncols) allocation.
225    ///
226    /// # Examples
227    ///
228    /// ```
229    /// use fdars_core::matrix::FdMatrix;
230    ///
231    /// let mat = FdMatrix::from_column_major(vec![
232    ///     1.0, 2.0,   // col 0
233    ///     3.0, 4.0,   // col 1
234    ///     5.0, 6.0,   // col 2
235    /// ], 2, 3).unwrap();
236    ///
237    /// let rows: Vec<Vec<f64>> = mat.iter_rows().collect();
238    /// assert_eq!(rows, vec![vec![1.0, 3.0, 5.0], vec![2.0, 4.0, 6.0]]);
239    /// ```
240    pub fn iter_rows(&self) -> impl Iterator<Item = Vec<f64>> + '_ {
241        (0..self.nrows).map(move |i| self.row(i))
242    }
243
244    /// Iterate over columns, yielding each column as a slice `&[f64]`.
245    ///
246    /// Zero-copy because `FdMatrix` uses column-major storage and each column
247    /// is a contiguous block in memory.
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// use fdars_core::matrix::FdMatrix;
253    ///
254    /// let mat = FdMatrix::from_column_major(vec![
255    ///     1.0, 2.0,   // col 0
256    ///     3.0, 4.0,   // col 1
257    ///     5.0, 6.0,   // col 2
258    /// ], 2, 3).unwrap();
259    ///
260    /// let cols: Vec<&[f64]> = mat.iter_columns().collect();
261    /// assert_eq!(cols, vec![&[1.0, 2.0], &[3.0, 4.0], &[5.0, 6.0]]);
262    /// ```
263    pub fn iter_columns(&self) -> impl Iterator<Item = &[f64]> {
264        (0..self.ncols).map(move |j| self.column(j))
265    }
266
267    /// Extract all rows as `Vec<Vec<f64>>`.
268    ///
269    /// Equivalent to the former `extract_curves` function.
270    pub fn rows(&self) -> Vec<Vec<f64>> {
271        (0..self.nrows).map(|i| self.row(i)).collect()
272    }
273
274    /// Produce a single contiguous flat buffer in row-major order.
275    ///
276    /// Row `i` occupies `buf[i * ncols..(i + 1) * ncols]`. This is a single
277    /// allocation versus `nrows` allocations from `rows()`, and gives better
278    /// cache locality for row-oriented iteration.
279    pub fn to_row_major(&self) -> Vec<f64> {
280        let mut buf = vec![0.0; self.nrows * self.ncols];
281        for i in 0..self.nrows {
282            for j in 0..self.ncols {
283                buf[i * self.ncols + j] = self.data[i + j * self.nrows];
284            }
285        }
286        buf
287    }
288
289    /// Flat slice of the underlying column-major data (zero-copy).
290    #[inline]
291    pub fn as_slice(&self) -> &[f64] {
292        &self.data
293    }
294
295    /// Mutable flat slice of the underlying column-major data.
296    #[inline]
297    pub fn as_mut_slice(&mut self) -> &mut [f64] {
298        &mut self.data
299    }
300
301    /// Consume and return the underlying `Vec<f64>`.
302    pub fn into_vec(self) -> Vec<f64> {
303        self.data
304    }
305
306    /// Convert to a nalgebra `DMatrix<f64>`.
307    ///
308    /// This copies the data into nalgebra's storage. Both use column-major
309    /// layout, so the copy is a simple memcpy.
310    pub fn to_dmatrix(&self) -> DMatrix<f64> {
311        DMatrix::from_column_slice(self.nrows, self.ncols, &self.data)
312    }
313
314    /// Create from a nalgebra `DMatrix<f64>`.
315    ///
316    /// Both use column-major layout so this is a direct copy.
317    pub fn from_dmatrix(mat: &DMatrix<f64>) -> Self {
318        let (nrows, ncols) = mat.shape();
319        Self {
320            data: mat.as_slice().to_vec(),
321            nrows,
322            ncols,
323        }
324    }
325
326    /// Get element at (row, col) with bounds checking.
327    #[inline]
328    pub fn get(&self, row: usize, col: usize) -> Option<f64> {
329        if row < self.nrows && col < self.ncols {
330            Some(self.data[row + col * self.nrows])
331        } else {
332            None
333        }
334    }
335
336    /// Set element at (row, col) with bounds checking.
337    #[inline]
338    pub fn set(&mut self, row: usize, col: usize, value: f64) -> bool {
339        if row < self.nrows && col < self.ncols {
340            self.data[row + col * self.nrows] = value;
341            true
342        } else {
343            false
344        }
345    }
346}
347
348impl std::ops::Index<(usize, usize)> for FdMatrix {
349    type Output = f64;
350
351    #[inline]
352    fn index(&self, (row, col): (usize, usize)) -> &f64 {
353        debug_assert!(
354            row < self.nrows && col < self.ncols,
355            "FdMatrix index ({}, {}) out of bounds for {}x{} matrix",
356            row,
357            col,
358            self.nrows,
359            self.ncols
360        );
361        &self.data[row + col * self.nrows]
362    }
363}
364
365impl std::ops::IndexMut<(usize, usize)> for FdMatrix {
366    #[inline]
367    fn index_mut(&mut self, (row, col): (usize, usize)) -> &mut f64 {
368        debug_assert!(
369            row < self.nrows && col < self.ncols,
370            "FdMatrix index ({}, {}) out of bounds for {}x{} matrix",
371            row,
372            col,
373            self.nrows,
374            self.ncols
375        );
376        &mut self.data[row + col * self.nrows]
377    }
378}
379
380impl std::fmt::Display for FdMatrix {
381    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
382        write!(f, "FdMatrix({}x{})", self.nrows, self.ncols)
383    }
384}
385
386/// A set of multidimensional functional curves in R^d.
387///
388/// Each dimension is stored as a separate [`FdMatrix`] (n curves × m points).
389/// For d=1 this is equivalent to a single `FdMatrix`.
390#[derive(Debug, Clone, PartialEq)]
391pub struct FdCurveSet {
392    /// One matrix per coordinate dimension, each n × m.
393    pub dims: Vec<FdMatrix>,
394}
395
396impl FdCurveSet {
397    /// Number of coordinate dimensions (d).
398    pub fn ndim(&self) -> usize {
399        self.dims.len()
400    }
401
402    /// Number of curves (n).
403    pub fn ncurves(&self) -> usize {
404        if self.dims.is_empty() {
405            0
406        } else {
407            self.dims[0].nrows()
408        }
409    }
410
411    /// Number of evaluation points (m).
412    pub fn npoints(&self) -> usize {
413        if self.dims.is_empty() {
414            0
415        } else {
416            self.dims[0].ncols()
417        }
418    }
419
420    /// Wrap a single 1D `FdMatrix` as a `FdCurveSet`.
421    pub fn from_1d(data: FdMatrix) -> Self {
422        Self { dims: vec![data] }
423    }
424
425    /// Build from multiple dimension matrices.
426    ///
427    /// Returns `Err` if `dims` is empty or if dimensions are inconsistent.
428    pub fn from_dims(dims: Vec<FdMatrix>) -> Result<Self, crate::FdarError> {
429        if dims.is_empty() {
430            return Err(crate::FdarError::InvalidDimension {
431                parameter: "dims",
432                expected: "non-empty".to_string(),
433                actual: "empty".to_string(),
434            });
435        }
436        let (n, m) = dims[0].shape();
437        if dims.iter().any(|d| d.shape() != (n, m)) {
438            return Err(crate::FdarError::InvalidDimension {
439                parameter: "dims",
440                expected: format!("all ({n}, {m})"),
441                actual: "inconsistent shapes".to_string(),
442            });
443        }
444        Ok(Self { dims })
445    }
446
447    /// Extract the R^d point for a given curve and time index.
448    pub fn point(&self, curve: usize, time_idx: usize) -> Vec<f64> {
449        self.dims.iter().map(|d| d[(curve, time_idx)]).collect()
450    }
451}
452
453impl std::fmt::Display for FdCurveSet {
454    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455        write!(
456            f,
457            "FdCurveSet(d={}, n={}, m={})",
458            self.ndim(),
459            self.ncurves(),
460            self.npoints()
461        )
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468
469    fn sample_3x4() -> FdMatrix {
470        // 3 rows, 4 columns, column-major
471        let data = vec![
472            1.0, 2.0, 3.0, // col 0
473            4.0, 5.0, 6.0, // col 1
474            7.0, 8.0, 9.0, // col 2
475            10.0, 11.0, 12.0, // col 3
476        ];
477        FdMatrix::from_column_major(data, 3, 4).unwrap()
478    }
479
480    #[test]
481    fn test_from_column_major_valid() {
482        let mat = sample_3x4();
483        assert_eq!(mat.nrows(), 3);
484        assert_eq!(mat.ncols(), 4);
485        assert_eq!(mat.shape(), (3, 4));
486        assert_eq!(mat.len(), 12);
487        assert!(!mat.is_empty());
488    }
489
490    #[test]
491    fn test_from_column_major_invalid() {
492        assert!(FdMatrix::from_column_major(vec![1.0, 2.0], 3, 4).is_err());
493    }
494
495    #[test]
496    fn test_from_slice() {
497        let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
498        let mat = FdMatrix::from_slice(&data, 2, 3).unwrap();
499        assert_eq!(mat[(0, 0)], 1.0);
500        assert_eq!(mat[(1, 0)], 2.0);
501        assert_eq!(mat[(0, 1)], 3.0);
502    }
503
504    #[test]
505    fn test_from_slice_invalid() {
506        assert!(FdMatrix::from_slice(&[1.0, 2.0], 3, 3).is_err());
507    }
508
509    #[test]
510    fn test_zeros() {
511        let mat = FdMatrix::zeros(2, 3);
512        assert_eq!(mat.nrows(), 2);
513        assert_eq!(mat.ncols(), 3);
514        for j in 0..3 {
515            for i in 0..2 {
516                assert_eq!(mat[(i, j)], 0.0);
517            }
518        }
519    }
520
521    #[test]
522    fn test_index() {
523        let mat = sample_3x4();
524        assert_eq!(mat[(0, 0)], 1.0);
525        assert_eq!(mat[(1, 0)], 2.0);
526        assert_eq!(mat[(2, 0)], 3.0);
527        assert_eq!(mat[(0, 1)], 4.0);
528        assert_eq!(mat[(1, 1)], 5.0);
529        assert_eq!(mat[(2, 3)], 12.0);
530    }
531
532    #[test]
533    fn test_index_mut() {
534        let mut mat = sample_3x4();
535        mat[(1, 2)] = 99.0;
536        assert_eq!(mat[(1, 2)], 99.0);
537    }
538
539    #[test]
540    fn test_column() {
541        let mat = sample_3x4();
542        assert_eq!(mat.column(0), &[1.0, 2.0, 3.0]);
543        assert_eq!(mat.column(1), &[4.0, 5.0, 6.0]);
544        assert_eq!(mat.column(3), &[10.0, 11.0, 12.0]);
545    }
546
547    #[test]
548    fn test_column_mut() {
549        let mut mat = sample_3x4();
550        mat.column_mut(1)[0] = 99.0;
551        assert_eq!(mat[(0, 1)], 99.0);
552    }
553
554    #[test]
555    fn test_row() {
556        let mat = sample_3x4();
557        assert_eq!(mat.row(0), vec![1.0, 4.0, 7.0, 10.0]);
558        assert_eq!(mat.row(1), vec![2.0, 5.0, 8.0, 11.0]);
559        assert_eq!(mat.row(2), vec![3.0, 6.0, 9.0, 12.0]);
560    }
561
562    #[test]
563    fn test_rows() {
564        let mat = sample_3x4();
565        let rows = mat.rows();
566        assert_eq!(rows.len(), 3);
567        assert_eq!(rows[0], vec![1.0, 4.0, 7.0, 10.0]);
568        assert_eq!(rows[2], vec![3.0, 6.0, 9.0, 12.0]);
569    }
570
571    #[test]
572    fn test_as_slice() {
573        let mat = sample_3x4();
574        let expected = vec![
575            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
576        ];
577        assert_eq!(mat.as_slice(), expected.as_slice());
578    }
579
580    #[test]
581    fn test_into_vec() {
582        let mat = sample_3x4();
583        let v = mat.into_vec();
584        assert_eq!(v.len(), 12);
585        assert_eq!(v[0], 1.0);
586    }
587
588    #[test]
589    fn test_get_bounds_check() {
590        let mat = sample_3x4();
591        assert_eq!(mat.get(0, 0), Some(1.0));
592        assert_eq!(mat.get(2, 3), Some(12.0));
593        assert_eq!(mat.get(3, 0), None); // row out of bounds
594        assert_eq!(mat.get(0, 4), None); // col out of bounds
595    }
596
597    #[test]
598    fn test_set_bounds_check() {
599        let mut mat = sample_3x4();
600        assert!(mat.set(1, 1, 99.0));
601        assert_eq!(mat[(1, 1)], 99.0);
602        assert!(!mat.set(5, 0, 99.0)); // out of bounds
603    }
604
605    #[test]
606    fn test_nalgebra_roundtrip() {
607        let mat = sample_3x4();
608        let dmat = mat.to_dmatrix();
609        assert_eq!(dmat.nrows(), 3);
610        assert_eq!(dmat.ncols(), 4);
611        assert_eq!(dmat[(0, 0)], 1.0);
612        assert_eq!(dmat[(1, 2)], 8.0);
613
614        let back = FdMatrix::from_dmatrix(&dmat);
615        assert_eq!(mat, back);
616    }
617
618    #[test]
619    fn test_empty() {
620        let mat = FdMatrix::zeros(0, 0);
621        assert!(mat.is_empty());
622        assert_eq!(mat.len(), 0);
623    }
624
625    #[test]
626    fn test_single_element() {
627        let mat = FdMatrix::from_column_major(vec![42.0], 1, 1).unwrap();
628        assert_eq!(mat[(0, 0)], 42.0);
629        assert_eq!(mat.column(0), &[42.0]);
630        assert_eq!(mat.row(0), vec![42.0]);
631    }
632
633    #[test]
634    fn test_display() {
635        let mat = sample_3x4();
636        assert_eq!(format!("{}", mat), "FdMatrix(3x4)");
637    }
638
639    #[test]
640    fn test_clone() {
641        let mat = sample_3x4();
642        let cloned = mat.clone();
643        assert_eq!(mat, cloned);
644    }
645
646    #[test]
647    fn test_as_mut_slice() {
648        let mut mat = FdMatrix::zeros(2, 2);
649        let s = mat.as_mut_slice();
650        s[0] = 1.0;
651        s[1] = 2.0;
652        s[2] = 3.0;
653        s[3] = 4.0;
654        assert_eq!(mat[(0, 0)], 1.0);
655        assert_eq!(mat[(1, 0)], 2.0);
656        assert_eq!(mat[(0, 1)], 3.0);
657        assert_eq!(mat[(1, 1)], 4.0);
658    }
659
660    #[test]
661    fn test_fd_curve_set_empty() {
662        assert!(FdCurveSet::from_dims(vec![]).is_err());
663        let cs = FdCurveSet::from_dims(vec![]).unwrap_or(FdCurveSet { dims: vec![] });
664        assert_eq!(cs.ndim(), 0);
665        assert_eq!(cs.ncurves(), 0);
666        assert_eq!(cs.npoints(), 0);
667        assert_eq!(format!("{}", cs), "FdCurveSet(d=0, n=0, m=0)");
668    }
669
670    #[test]
671    fn test_fd_curve_set_from_1d() {
672        let mat = sample_3x4();
673        let cs = FdCurveSet::from_1d(mat.clone());
674        assert_eq!(cs.ndim(), 1);
675        assert_eq!(cs.ncurves(), 3);
676        assert_eq!(cs.npoints(), 4);
677        assert_eq!(cs.point(0, 0), vec![1.0]);
678        assert_eq!(cs.point(1, 2), vec![8.0]);
679    }
680
681    #[test]
682    fn test_fd_curve_set_from_dims_consistent() {
683        let m1 = FdMatrix::from_column_major(vec![1.0, 2.0, 3.0, 4.0], 2, 2).unwrap();
684        let m2 = FdMatrix::from_column_major(vec![5.0, 6.0, 7.0, 8.0], 2, 2).unwrap();
685        let cs = FdCurveSet::from_dims(vec![m1, m2]).unwrap();
686        assert_eq!(cs.ndim(), 2);
687        assert_eq!(cs.point(0, 0), vec![1.0, 5.0]);
688        assert_eq!(cs.point(1, 1), vec![4.0, 8.0]);
689        assert_eq!(format!("{}", cs), "FdCurveSet(d=2, n=2, m=2)");
690    }
691
692    #[test]
693    fn test_fd_curve_set_from_dims_inconsistent() {
694        let m1 = FdMatrix::from_column_major(vec![1.0, 2.0], 2, 1).unwrap();
695        let m2 = FdMatrix::from_column_major(vec![1.0, 2.0, 3.0], 3, 1).unwrap();
696        assert!(FdCurveSet::from_dims(vec![m1, m2]).is_err());
697    }
698
699    #[test]
700    fn test_to_row_major() {
701        let mat = sample_3x4();
702        let rm = mat.to_row_major();
703        // Row 0: [1,4,7,10], Row 1: [2,5,8,11], Row 2: [3,6,9,12]
704        assert_eq!(
705            rm,
706            vec![1.0, 4.0, 7.0, 10.0, 2.0, 5.0, 8.0, 11.0, 3.0, 6.0, 9.0, 12.0]
707        );
708    }
709
710    #[test]
711    fn test_row_to_buf() {
712        let mat = sample_3x4();
713        let mut buf = vec![0.0; 4];
714        mat.row_to_buf(0, &mut buf);
715        assert_eq!(buf, vec![1.0, 4.0, 7.0, 10.0]);
716        mat.row_to_buf(1, &mut buf);
717        assert_eq!(buf, vec![2.0, 5.0, 8.0, 11.0]);
718        mat.row_to_buf(2, &mut buf);
719        assert_eq!(buf, vec![3.0, 6.0, 9.0, 12.0]);
720    }
721
722    #[test]
723    fn test_row_to_buf_larger_buffer() {
724        let mat = sample_3x4();
725        let mut buf = vec![99.0; 6]; // bigger than ncols
726        mat.row_to_buf(0, &mut buf);
727        assert_eq!(&buf[..4], &[1.0, 4.0, 7.0, 10.0]);
728        // Remaining elements unchanged
729        assert_eq!(buf[4], 99.0);
730    }
731
732    #[test]
733    fn test_row_dot_same_matrix() {
734        let mat = sample_3x4();
735        // row0 = [1, 4, 7, 10], row1 = [2, 5, 8, 11]
736        // dot = 1*2 + 4*5 + 7*8 + 10*11 = 2 + 20 + 56 + 110 = 188
737        assert_eq!(mat.row_dot(0, &mat, 1), 188.0);
738        // self dot: row0 . row0 = 1+16+49+100 = 166
739        assert_eq!(mat.row_dot(0, &mat, 0), 166.0);
740    }
741
742    #[test]
743    fn test_row_dot_different_matrices() {
744        let mat1 = sample_3x4();
745        let data2 = vec![
746            10.0, 20.0, 30.0, // col 0
747            40.0, 50.0, 60.0, // col 1
748            70.0, 80.0, 90.0, // col 2
749            100.0, 110.0, 120.0, // col 3
750        ];
751        let mat2 = FdMatrix::from_column_major(data2, 3, 4).unwrap();
752        // mat1 row0 = [1, 4, 7, 10], mat2 row0 = [10, 40, 70, 100]
753        // dot = 10 + 160 + 490 + 1000 = 1660
754        assert_eq!(mat1.row_dot(0, &mat2, 0), 1660.0);
755    }
756
757    #[test]
758    fn test_row_l2_sq_identical() {
759        let mat = sample_3x4();
760        assert_eq!(mat.row_l2_sq(0, &mat, 0), 0.0);
761        assert_eq!(mat.row_l2_sq(1, &mat, 1), 0.0);
762    }
763
764    #[test]
765    fn test_row_l2_sq_different() {
766        let mat = sample_3x4();
767        // row0 = [1,4,7,10], row1 = [2,5,8,11]
768        // diff = [-1,-1,-1,-1], sq sum = 4
769        assert_eq!(mat.row_l2_sq(0, &mat, 1), 4.0);
770    }
771
772    #[test]
773    fn test_row_l2_sq_cross_matrix() {
774        let mat1 = FdMatrix::from_column_major(vec![0.0, 0.0], 1, 2).unwrap();
775        let mat2 = FdMatrix::from_column_major(vec![3.0, 4.0], 1, 2).unwrap();
776        // row0 = [0, 0], row0 = [3, 4], sq dist = 9 + 16 = 25
777        assert_eq!(mat1.row_l2_sq(0, &mat2, 0), 25.0);
778    }
779
780    #[test]
781    fn test_column_major_layout_matches_manual() {
782        // Verify that FdMatrix[(i, j)] == data[i + j * n] for all i, j
783        let n = 5;
784        let m = 7;
785        let data: Vec<f64> = (0..n * m).map(|x| x as f64).collect();
786        let mat = FdMatrix::from_column_major(data.clone(), n, m).unwrap();
787
788        for j in 0..m {
789            for i in 0..n {
790                assert_eq!(mat[(i, j)], data[i + j * n]);
791            }
792        }
793    }
794
795    #[test]
796    fn test_iter_rows() {
797        let mat = sample_3x4();
798        let rows: Vec<Vec<f64>> = mat.iter_rows().collect();
799        assert_eq!(rows.len(), 3);
800        assert_eq!(rows[0], vec![1.0, 4.0, 7.0, 10.0]);
801        assert_eq!(rows[1], vec![2.0, 5.0, 8.0, 11.0]);
802        assert_eq!(rows[2], vec![3.0, 6.0, 9.0, 12.0]);
803    }
804
805    #[test]
806    fn test_iter_rows_matches_rows() {
807        let mat = sample_3x4();
808        let from_iter: Vec<Vec<f64>> = mat.iter_rows().collect();
809        let from_rows = mat.rows();
810        assert_eq!(from_iter, from_rows);
811    }
812
813    #[test]
814    fn test_iter_rows_partial() {
815        // Verify that taking only a subset avoids full materialization
816        let mat = sample_3x4();
817        let first_two: Vec<Vec<f64>> = mat.iter_rows().take(2).collect();
818        assert_eq!(first_two.len(), 2);
819        assert_eq!(first_two[0], vec![1.0, 4.0, 7.0, 10.0]);
820        assert_eq!(first_two[1], vec![2.0, 5.0, 8.0, 11.0]);
821    }
822
823    #[test]
824    fn test_iter_rows_empty() {
825        let mat = FdMatrix::zeros(0, 0);
826        let rows: Vec<Vec<f64>> = mat.iter_rows().collect();
827        assert!(rows.is_empty());
828    }
829
830    #[test]
831    fn test_iter_rows_single_row() {
832        let mat = FdMatrix::from_column_major(vec![1.0, 2.0, 3.0], 1, 3).unwrap();
833        let rows: Vec<Vec<f64>> = mat.iter_rows().collect();
834        assert_eq!(rows, vec![vec![1.0, 2.0, 3.0]]);
835    }
836
837    #[test]
838    fn test_iter_rows_single_column() {
839        let mat = FdMatrix::from_column_major(vec![1.0, 2.0, 3.0], 3, 1).unwrap();
840        let rows: Vec<Vec<f64>> = mat.iter_rows().collect();
841        assert_eq!(rows, vec![vec![1.0], vec![2.0], vec![3.0]]);
842    }
843
844    #[test]
845    fn test_iter_columns() {
846        let mat = sample_3x4();
847        let cols: Vec<&[f64]> = mat.iter_columns().collect();
848        assert_eq!(cols.len(), 4);
849        assert_eq!(cols[0], &[1.0, 2.0, 3.0]);
850        assert_eq!(cols[1], &[4.0, 5.0, 6.0]);
851        assert_eq!(cols[2], &[7.0, 8.0, 9.0]);
852        assert_eq!(cols[3], &[10.0, 11.0, 12.0]);
853    }
854
855    #[test]
856    fn test_iter_columns_partial() {
857        let mat = sample_3x4();
858        let first_two: Vec<&[f64]> = mat.iter_columns().take(2).collect();
859        assert_eq!(first_two.len(), 2);
860        assert_eq!(first_two[0], &[1.0, 2.0, 3.0]);
861        assert_eq!(first_two[1], &[4.0, 5.0, 6.0]);
862    }
863
864    #[test]
865    fn test_iter_columns_empty() {
866        let mat = FdMatrix::zeros(0, 0);
867        let cols: Vec<&[f64]> = mat.iter_columns().collect();
868        assert!(cols.is_empty());
869    }
870
871    #[test]
872    fn test_iter_columns_single_column() {
873        let mat = FdMatrix::from_column_major(vec![1.0, 2.0, 3.0], 3, 1).unwrap();
874        let cols: Vec<&[f64]> = mat.iter_columns().collect();
875        assert_eq!(cols, vec![&[1.0, 2.0, 3.0]]);
876    }
877
878    #[test]
879    fn test_iter_columns_single_row() {
880        let mat = FdMatrix::from_column_major(vec![1.0, 2.0, 3.0], 1, 3).unwrap();
881        let cols: Vec<&[f64]> = mat.iter_columns().collect();
882        assert_eq!(cols, vec![&[1.0_f64] as &[f64], &[2.0], &[3.0]]);
883    }
884
885    #[test]
886    fn test_iter_rows_enumerate() {
887        let mat = sample_3x4();
888        for (i, row) in mat.iter_rows().enumerate() {
889            assert_eq!(row, mat.row(i));
890        }
891    }
892
893    #[test]
894    fn test_iter_columns_enumerate() {
895        let mat = sample_3x4();
896        for (j, col) in mat.iter_columns().enumerate() {
897            assert_eq!(col, mat.column(j));
898        }
899    }
900}