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