Skip to main content

oxiblas_matrix/
packed.rs

1//! Packed matrix storage for triangular and symmetric matrices.
2//!
3//! Packed storage stores only the upper or lower triangular portion of a matrix
4//! in a one-dimensional array, reducing memory usage by nearly half.
5//!
6//! # Storage Layout
7//!
8//! For an `n × n` matrix, packed storage uses `n*(n+1)/2` elements.
9//!
10//! **Upper triangular (column-major)**: Elements are stored column by column,
11//! starting from the diagonal:
12//! ```text
13//! [a00, a01, a11, a02, a12, a22, a03, a13, a23, a33, ...]
14//! ```
15//!
16//! **Lower triangular (column-major)**: Elements are stored column by column:
17//! ```text
18//! [a00, a10, a20, a30, a11, a21, a31, a22, a32, a33, ...]
19//! ```
20
21#[cfg(not(feature = "std"))]
22use alloc::vec::Vec;
23
24use oxiblas_core::memory::AlignedVec;
25use oxiblas_core::scalar::Scalar;
26
27/// Specifies whether to use upper or lower triangular storage.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum TriangularKind {
30    /// Upper triangular: stores elements where row <= col.
31    Upper,
32    /// Lower triangular: stores elements where row >= col.
33    Lower,
34}
35
36/// Error returned when an index falls outside the triangle stored by a
37/// [`PackedMat`] (or one of the packed view types [`PackedRef`]/[`PackedMut`]).
38///
39/// A packed triangular matrix only stores the upper or lower triangle
40/// (including the diagonal); the complementary triangle is never
41/// materialized. Attempting to write to an index outside the stored
42/// triangle -- or outside the matrix bounds -- returns this error instead
43/// of panicking, so callers can decide how to handle invalid indices.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct OutOfTriangleError {
46    /// The row index that was requested.
47    pub row: usize,
48    /// The column index that was requested.
49    pub col: usize,
50    /// The matrix dimension (`n x n`) at the time of the request.
51    pub dim: usize,
52    /// Which triangle is stored.
53    pub kind: TriangularKind,
54}
55
56impl core::fmt::Display for OutOfTriangleError {
57    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
58        write!(
59            f,
60            "index ({}, {}) is outside the stored {:?} triangle of a {}x{} packed matrix",
61            self.row, self.col, self.kind, self.dim, self.dim
62        )
63    }
64}
65
66#[cfg(feature = "std")]
67impl std::error::Error for OutOfTriangleError {}
68
69/// A packed matrix storing only the triangular portion.
70///
71/// This is useful for symmetric, Hermitian, and triangular matrices
72/// where only half the elements need to be stored.
73///
74/// # Example
75///
76/// ```
77/// use oxiblas_matrix::packed::{PackedMat, TriangularKind};
78///
79/// // Create a 3x3 upper triangular packed matrix
80/// let mut p: PackedMat<f64> = PackedMat::zeros(3, TriangularKind::Upper);
81///
82/// // Set diagonal and upper triangle
83/// p.set(0, 0, 1.0).unwrap();
84/// p.set(0, 1, 2.0).unwrap();
85/// p.set(0, 2, 3.0).unwrap();
86/// p.set(1, 1, 4.0).unwrap();
87/// p.set(1, 2, 5.0).unwrap();
88/// p.set(2, 2, 6.0).unwrap();
89///
90/// // Access elements
91/// assert_eq!(p.get(0, 1), Some(&2.0));
92/// assert_eq!(p.get(1, 0), None); // Below diagonal in upper triangular
93/// ```
94#[derive(Clone)]
95pub struct PackedMat<T: Scalar> {
96    /// Packed data storage.
97    data: AlignedVec<T>,
98    /// Matrix dimension (n × n).
99    n: usize,
100    /// Upper or lower triangular.
101    kind: TriangularKind,
102}
103
104impl<T: Scalar> PackedMat<T> {
105    /// Creates a new packed matrix filled with zeros.
106    pub fn zeros(n: usize, kind: TriangularKind) -> Self
107    where
108        T: bytemuck::Zeroable,
109    {
110        let len = Self::packed_len(n);
111        PackedMat {
112            data: AlignedVec::zeros(len),
113            n,
114            kind,
115        }
116    }
117
118    /// Creates a new packed matrix filled with a specific value.
119    pub fn filled(n: usize, kind: TriangularKind, value: T) -> Self {
120        let len = Self::packed_len(n);
121        PackedMat {
122            data: AlignedVec::filled(len, value),
123            n,
124            kind,
125        }
126    }
127
128    /// Creates a packed matrix from a slice.
129    ///
130    /// # Panics
131    /// Panics if the slice length doesn't match `n*(n+1)/2`.
132    pub fn from_slice(n: usize, kind: TriangularKind, data: &[T]) -> Self {
133        let len = Self::packed_len(n);
134        assert_eq!(
135            data.len(),
136            len,
137            "Slice length must equal n*(n+1)/2 = {}",
138            len
139        );
140
141        PackedMat {
142            data: AlignedVec::from_slice(data),
143            n,
144            kind,
145        }
146    }
147
148    /// Computes the packed storage length for dimension `n`, i.e. `n*(n+1)/2`.
149    ///
150    /// # Panics
151    ///
152    /// Panics if `n*(n+1)` overflows `usize`. The multiplication is *checked*
153    /// rather than wrapping because this value is used both as an allocation
154    /// length ([`PackedMat::zeros`] / [`PackedMat::filled`]) and as the
155    /// length assertion that makes [`PackedRef::from_slice`] /
156    /// [`PackedMut::from_slice`] sound: a silent release-mode wraparound would
157    /// hand out a small buffer for a matrix whose `packed_index` reaches far
158    /// beyond it. A dimension that overflows here could never be allocated
159    /// anyway, so a clear panic is strictly better than a wrong length. This
160    /// mirrors `Mat`'s `checked_dim_mul` helper.
161    #[inline]
162    pub const fn packed_len(n: usize) -> usize {
163        match n.checked_add(1) {
164            Some(np1) => match n.checked_mul(np1) {
165                Some(product) => product / 2,
166                None => panic!(
167                    "PackedMat: packed length overflow (n*(n+1) exceeds usize::MAX); \
168                     requested matrix dimension is too large to allocate"
169                ),
170            },
171            None => panic!(
172                "PackedMat: packed length overflow (n+1 exceeds usize::MAX); \
173                 requested matrix dimension is too large to allocate"
174            ),
175        }
176    }
177
178    /// Returns the matrix dimension.
179    #[inline]
180    pub fn dim(&self) -> usize {
181        self.n
182    }
183
184    /// Returns the storage kind (upper or lower).
185    #[inline]
186    pub fn kind(&self) -> TriangularKind {
187        self.kind
188    }
189
190    /// Returns the packed data length.
191    #[inline]
192    pub fn len(&self) -> usize {
193        self.data.len()
194    }
195
196    /// Returns true if the matrix is empty.
197    #[inline]
198    pub fn is_empty(&self) -> bool {
199        self.n == 0
200    }
201
202    /// Computes the packed index for element (row, col).
203    ///
204    /// Returns `None` if the element is in the non-stored triangle.
205    #[inline]
206    pub fn packed_index(&self, row: usize, col: usize) -> Option<usize> {
207        if row >= self.n || col >= self.n {
208            return None;
209        }
210
211        match self.kind {
212            TriangularKind::Upper => {
213                if row <= col {
214                    // Column-major upper: index = col*(col+1)/2 + row
215                    Some(col * (col + 1) / 2 + row)
216                } else {
217                    None
218                }
219            }
220            TriangularKind::Lower => {
221                if row >= col {
222                    // Column-major lower:
223                    // Column j starts at index: n*j - j*(j-1)/2
224                    // Element (row, col) is at: start + (row - col)
225                    let offset = self.n * col - col * (col.saturating_sub(1)) / 2;
226                    Some(offset + (row - col))
227                } else {
228                    None
229                }
230            }
231        }
232    }
233
234    /// Returns a reference to the element at (row, col).
235    ///
236    /// Returns `None` if the element is outside the stored triangle.
237    #[inline]
238    pub fn get(&self, row: usize, col: usize) -> Option<&T> {
239        self.packed_index(row, col).map(|idx| &self.data[idx])
240    }
241
242    /// Returns a mutable reference to the element at (row, col).
243    ///
244    /// Returns `None` if the element is outside the stored triangle.
245    #[inline]
246    pub fn get_mut(&mut self, row: usize, col: usize) -> Option<&mut T> {
247        self.packed_index(row, col).map(|idx| &mut self.data[idx])
248    }
249
250    /// Sets the element at (row, col).
251    ///
252    /// # Errors
253    /// Returns [`OutOfTriangleError`] if `(row, col)` lies outside the
254    /// stored triangle (this includes indices outside the matrix bounds).
255    #[inline]
256    pub fn set(&mut self, row: usize, col: usize, value: T) -> Result<(), OutOfTriangleError> {
257        let idx = self.packed_index(row, col).ok_or(OutOfTriangleError {
258            row,
259            col,
260            dim: self.n,
261            kind: self.kind,
262        })?;
263        self.data[idx] = value;
264        Ok(())
265    }
266
267    /// Returns a pointer to the packed data.
268    #[inline]
269    pub fn as_ptr(&self) -> *const T {
270        self.data.as_ptr()
271    }
272
273    /// Returns a mutable pointer to the packed data.
274    #[inline]
275    pub fn as_mut_ptr(&mut self) -> *mut T {
276        self.data.as_mut_ptr()
277    }
278
279    /// Returns the packed data as a slice.
280    #[inline]
281    pub fn as_slice(&self) -> &[T] {
282        self.data.as_slice()
283    }
284
285    /// Returns the packed data as a mutable slice.
286    #[inline]
287    pub fn as_slice_mut(&mut self) -> &mut [T] {
288        self.data.as_mut_slice()
289    }
290
291    /// Converts to a full dense matrix.
292    pub fn to_dense(&self) -> crate::Mat<T>
293    where
294        T: bytemuck::Zeroable,
295    {
296        let mut mat = crate::Mat::zeros(self.n, self.n);
297
298        for j in 0..self.n {
299            for i in 0..self.n {
300                if let Some(idx) = self.packed_index(i, j) {
301                    mat[(i, j)] = self.data[idx];
302                }
303            }
304        }
305
306        mat
307    }
308
309    /// Creates a packed matrix from a dense matrix.
310    ///
311    /// Only copies elements from the specified triangle.
312    pub fn from_dense(mat: &crate::MatRef<'_, T>, kind: TriangularKind) -> Self
313    where
314        T: bytemuck::Zeroable,
315    {
316        assert_eq!(mat.nrows(), mat.ncols(), "Matrix must be square");
317        let n = mat.nrows();
318        let mut packed = Self::zeros(n, kind);
319
320        for j in 0..n {
321            for i in 0..n {
322                if let Some(idx) = packed.packed_index(i, j) {
323                    packed.data[idx] = mat[(i, j)];
324                }
325            }
326        }
327
328        packed
329    }
330
331    /// Computes the packed index of the diagonal element `(i, i)`.
332    ///
333    /// Diagonal elements are always part of the stored triangle for both
334    /// [`TriangularKind::Upper`] (`row <= col`) and [`TriangularKind::Lower`]
335    /// (`row >= col`), since `row == col` trivially satisfies both
336    /// conditions. This computes the index directly (bypassing
337    /// [`Self::packed_index`]'s `Option`) so callers never need to handle an
338    /// unreachable "not found" case -- the caller must ensure `i < self.n`.
339    #[inline]
340    fn diagonal_packed_index(&self, i: usize) -> usize {
341        match self.kind {
342            TriangularKind::Upper => i * (i + 1) / 2 + i,
343            TriangularKind::Lower => self.n * i - i * (i.saturating_sub(1)) / 2,
344        }
345    }
346
347    /// Returns the diagonal elements as a vector.
348    pub fn diagonal(&self) -> Vec<T> {
349        (0..self.n)
350            .map(|i| self.data[self.diagonal_packed_index(i)])
351            .collect()
352    }
353
354    /// Sets the diagonal elements from a slice.
355    pub fn set_diagonal(&mut self, diag: &[T]) {
356        assert_eq!(
357            diag.len(),
358            self.n,
359            "Diagonal length must match matrix dimension"
360        );
361        for (i, &val) in diag.iter().enumerate() {
362            let idx = self.diagonal_packed_index(i);
363            self.data[idx] = val;
364        }
365    }
366
367    /// Fills the stored triangle with a value.
368    pub fn fill(&mut self, value: T) {
369        for elem in self.data.as_mut_slice() {
370            *elem = value;
371        }
372    }
373
374    /// Scales all stored elements by a scalar.
375    pub fn scale(&mut self, alpha: T) {
376        for elem in self.data.as_mut_slice() {
377            *elem *= alpha;
378        }
379    }
380
381    /// Converts between upper and lower triangular representation.
382    ///
383    /// For symmetric matrices, this effectively transposes the packed data.
384    pub fn transpose(&self) -> Self
385    where
386        T: bytemuck::Zeroable,
387    {
388        let new_kind = match self.kind {
389            TriangularKind::Upper => TriangularKind::Lower,
390            TriangularKind::Lower => TriangularKind::Upper,
391        };
392
393        let mut result = Self::zeros(self.n, new_kind);
394
395        for j in 0..self.n {
396            for i in 0..self.n {
397                if let Some(src_idx) = self.packed_index(i, j) {
398                    // In transposed storage, (i,j) becomes (j,i)
399                    if let Some(dst_idx) = result.packed_index(j, i) {
400                        result.data[dst_idx] = self.data[src_idx];
401                    }
402                }
403            }
404        }
405
406        result
407    }
408}
409
410impl<T: Scalar + core::fmt::Debug> core::fmt::Debug for PackedMat<T> {
411    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
412        writeln!(f, "PackedMat {}x{} {:?} {{", self.n, self.n, self.kind)?;
413
414        for i in 0..self.n.min(8) {
415            write!(f, "  [")?;
416            for j in 0..self.n.min(8) {
417                if j > 0 {
418                    write!(f, ", ")?;
419                }
420                match self.get(i, j) {
421                    Some(v) => write!(f, "{:8.4?}", v)?,
422                    None => write!(f, "      * ")?,
423                }
424            }
425            if self.n > 8 {
426                write!(f, ", ...")?;
427            }
428            writeln!(f, "]")?;
429        }
430        if self.n > 8 {
431            writeln!(f, "  ...")?;
432        }
433        write!(f, "}}")
434    }
435}
436
437/// A view into packed matrix data.
438#[derive(Clone, Copy)]
439pub struct PackedRef<'a, T: Scalar> {
440    /// Pointer to packed data.
441    ptr: *const T,
442    /// Matrix dimension.
443    n: usize,
444    /// Storage kind.
445    kind: TriangularKind,
446    /// Lifetime marker.
447    _marker: core::marker::PhantomData<&'a T>,
448}
449
450impl<'a, T: Scalar> PackedRef<'a, T> {
451    /// Creates a new packed reference from raw components.
452    ///
453    /// # Safety
454    ///
455    /// The caller must ensure that:
456    /// - `ptr` is non-null, well-aligned, and points to valid, initialized data
457    /// - The data remains valid and immutable for the lifetime `'a`
458    /// - The allocation behind `ptr` holds at least
459    ///   `PackedMat::<T>::packed_len(n)` elements of `T`
460    ///
461    /// Every accessor ([`get`](Self::get)) dereferences `ptr` at the offset
462    /// returned by [`packed_index`](Self::packed_index), which ranges over
463    /// `0..packed_len(n)`. A shorter allocation therefore yields out-of-bounds
464    /// reads. Prefer the validated [`PackedRef::from_slice`] constructor
465    /// whenever a backing slice is available.
466    #[inline]
467    pub unsafe fn new(ptr: *const T, n: usize, kind: TriangularKind) -> Self {
468        PackedRef {
469            ptr,
470            n,
471            kind,
472            _marker: core::marker::PhantomData,
473        }
474    }
475
476    /// Creates a packed reference from a slice.
477    ///
478    /// # Panics
479    ///
480    /// Panics if `data.len() != n*(n+1)/2`. This check is what makes the
481    /// resulting view sound, so it is never elided.
482    #[inline]
483    pub fn from_slice(data: &'a [T], n: usize, kind: TriangularKind) -> Self {
484        let expected_len = PackedMat::<T>::packed_len(n);
485        assert_eq!(
486            data.len(),
487            expected_len,
488            "Slice length must equal n*(n+1)/2"
489        );
490        // SAFETY: `data` is a live shared slice for `'a`, so its pointer is
491        // non-null, aligned and initialized; the assertion above proves it
492        // holds exactly `packed_len(n)` elements, which is the full range
493        // `packed_index` can produce.
494        unsafe { PackedRef::new(data.as_ptr(), n, kind) }
495    }
496
497    /// Returns the matrix dimension.
498    #[inline]
499    pub fn dim(&self) -> usize {
500        self.n
501    }
502
503    /// Returns the storage kind.
504    #[inline]
505    pub fn kind(&self) -> TriangularKind {
506        self.kind
507    }
508
509    /// Computes the packed index for element (row, col).
510    #[inline]
511    pub fn packed_index(&self, row: usize, col: usize) -> Option<usize> {
512        if row >= self.n || col >= self.n {
513            return None;
514        }
515
516        match self.kind {
517            TriangularKind::Upper => {
518                if row <= col {
519                    Some(col * (col + 1) / 2 + row)
520                } else {
521                    None
522                }
523            }
524            TriangularKind::Lower => {
525                if row >= col {
526                    // Column-major lower:
527                    // Column j starts at index: n*j - j*(j-1)/2
528                    // Element (row, col) is at: start + (row - col)
529                    let offset = self.n * col - col * (col.saturating_sub(1)) / 2;
530                    Some(offset + (row - col))
531                } else {
532                    None
533                }
534            }
535        }
536    }
537
538    /// Returns a reference to the element at (row, col).
539    #[inline]
540    pub fn get(&self, row: usize, col: usize) -> Option<&T> {
541        self.packed_index(row, col)
542            .map(|idx| unsafe { &*self.ptr.add(idx) })
543    }
544
545    /// Returns a pointer to the packed data.
546    #[inline]
547    pub fn as_ptr(&self) -> *const T {
548        self.ptr
549    }
550}
551
552unsafe impl<'a, T: Scalar + Send> Send for PackedRef<'a, T> {}
553unsafe impl<'a, T: Scalar + Sync> Sync for PackedRef<'a, T> {}
554
555/// A mutable view into packed matrix data.
556pub struct PackedMut<'a, T: Scalar> {
557    /// Pointer to packed data.
558    ptr: *mut T,
559    /// Matrix dimension.
560    n: usize,
561    /// Storage kind.
562    kind: TriangularKind,
563    /// Lifetime marker.
564    _marker: core::marker::PhantomData<&'a mut T>,
565}
566
567impl<'a, T: Scalar> PackedMut<'a, T> {
568    /// Creates a new mutable packed reference from raw components.
569    ///
570    /// # Safety
571    ///
572    /// The caller must ensure that:
573    /// - `ptr` is non-null, well-aligned, and points to valid, initialized data
574    /// - The data remains valid and *exclusively* borrowed for the lifetime `'a`
575    /// - The allocation behind `ptr` holds at least
576    ///   `PackedMat::<T>::packed_len(n)` elements of `T`
577    ///
578    /// The mutating accessors ([`get_mut`](Self::get_mut), [`set`](Self::set))
579    /// dereference `ptr` at the offset returned by
580    /// [`packed_index`](Self::packed_index), which ranges over
581    /// `0..packed_len(n)`. A shorter allocation therefore yields out-of-bounds
582    /// **writes**. Prefer the validated [`PackedMut::from_slice`] constructor
583    /// whenever a backing slice is available.
584    #[inline]
585    pub unsafe fn new(ptr: *mut T, n: usize, kind: TriangularKind) -> Self {
586        PackedMut {
587            ptr,
588            n,
589            kind,
590            _marker: core::marker::PhantomData,
591        }
592    }
593
594    /// Creates a mutable packed reference from a mutable slice.
595    ///
596    /// # Panics
597    ///
598    /// Panics if `data.len() != n*(n+1)/2`. This check is what makes the
599    /// resulting view sound, so it is never elided.
600    #[inline]
601    pub fn from_slice(data: &'a mut [T], n: usize, kind: TriangularKind) -> Self {
602        let expected_len = PackedMat::<T>::packed_len(n);
603        assert_eq!(
604            data.len(),
605            expected_len,
606            "Slice length must equal n*(n+1)/2"
607        );
608        // SAFETY: `data` is a live exclusive slice for `'a`, so its pointer is
609        // non-null, aligned and initialized; the assertion above proves it
610        // holds exactly `packed_len(n)` elements, which is the full range
611        // `packed_index` can produce.
612        unsafe { PackedMut::new(data.as_mut_ptr(), n, kind) }
613    }
614
615    /// Returns the matrix dimension.
616    #[inline]
617    pub fn dim(&self) -> usize {
618        self.n
619    }
620
621    /// Returns the storage kind.
622    #[inline]
623    pub fn kind(&self) -> TriangularKind {
624        self.kind
625    }
626
627    /// Computes the packed index for element (row, col).
628    #[inline]
629    pub fn packed_index(&self, row: usize, col: usize) -> Option<usize> {
630        if row >= self.n || col >= self.n {
631            return None;
632        }
633
634        match self.kind {
635            TriangularKind::Upper => {
636                if row <= col {
637                    Some(col * (col + 1) / 2 + row)
638                } else {
639                    None
640                }
641            }
642            TriangularKind::Lower => {
643                if row >= col {
644                    // Column-major lower:
645                    // Column j starts at index: n*j - j*(j-1)/2
646                    // Element (row, col) is at: start + (row - col)
647                    let offset = self.n * col - col * (col.saturating_sub(1)) / 2;
648                    Some(offset + (row - col))
649                } else {
650                    None
651                }
652            }
653        }
654    }
655
656    /// Returns a reference to the element at (row, col).
657    #[inline]
658    pub fn get(&self, row: usize, col: usize) -> Option<&T> {
659        self.packed_index(row, col)
660            .map(|idx| unsafe { &*self.ptr.add(idx) })
661    }
662
663    /// Returns a mutable reference to the element at (row, col).
664    #[inline]
665    pub fn get_mut(&mut self, row: usize, col: usize) -> Option<&mut T> {
666        self.packed_index(row, col)
667            .map(|idx| unsafe { &mut *self.ptr.add(idx) })
668    }
669
670    /// Sets the element at (row, col).
671    ///
672    /// # Errors
673    /// Returns [`OutOfTriangleError`] if `(row, col)` lies outside the
674    /// stored triangle (this includes indices outside the matrix bounds).
675    #[inline]
676    pub fn set(&mut self, row: usize, col: usize, value: T) -> Result<(), OutOfTriangleError> {
677        let idx = self.packed_index(row, col).ok_or(OutOfTriangleError {
678            row,
679            col,
680            dim: self.n,
681            kind: self.kind,
682        })?;
683        unsafe {
684            *self.ptr.add(idx) = value;
685        }
686        Ok(())
687    }
688
689    /// Returns a pointer to the packed data.
690    #[inline]
691    pub fn as_ptr(&self) -> *const T {
692        self.ptr
693    }
694
695    /// Returns a mutable pointer to the packed data.
696    #[inline]
697    pub fn as_mut_ptr(&mut self) -> *mut T {
698        self.ptr
699    }
700
701    /// Creates an immutable reborrow.
702    #[inline]
703    pub fn rb(&self) -> PackedRef<'_, T> {
704        // SAFETY: `self` upholds the `PackedMut::new` contract (its own
705        // constructor required it), and the reborrow narrows the lifetime and
706        // weakens the access, keeping every invariant.
707        unsafe { PackedRef::new(self.ptr, self.n, self.kind) }
708    }
709
710    /// Creates a mutable reborrow.
711    #[inline]
712    pub fn rb_mut(&mut self) -> PackedMut<'_, T> {
713        // SAFETY: as `rb`, and `&mut self` guarantees the reborrow is the only
714        // live handle for the shortened lifetime.
715        unsafe { PackedMut::new(self.ptr, self.n, self.kind) }
716    }
717}
718
719unsafe impl<'a, T: Scalar + Send> Send for PackedMut<'a, T> {}
720unsafe impl<'a, T: Scalar + Sync> Sync for PackedMut<'a, T> {}
721
722#[cfg(test)]
723mod tests {
724    use super::*;
725
726    #[test]
727    fn test_packed_upper_indexing() {
728        // For a 3x3 upper triangular matrix:
729        // [0  1  3]
730        // [*  2  4]
731        // [*  *  5]
732        // Packed: [a00, a01, a11, a02, a12, a22]
733        let mut p: PackedMat<f64> = PackedMat::zeros(3, TriangularKind::Upper);
734
735        // Check indices
736        assert_eq!(p.packed_index(0, 0), Some(0));
737        assert_eq!(p.packed_index(0, 1), Some(1));
738        assert_eq!(p.packed_index(1, 1), Some(2));
739        assert_eq!(p.packed_index(0, 2), Some(3));
740        assert_eq!(p.packed_index(1, 2), Some(4));
741        assert_eq!(p.packed_index(2, 2), Some(5));
742
743        // Below diagonal should return None
744        assert_eq!(p.packed_index(1, 0), None);
745        assert_eq!(p.packed_index(2, 0), None);
746        assert_eq!(p.packed_index(2, 1), None);
747
748        // Set and get values
749        p.set(0, 0, 1.0).unwrap();
750        p.set(0, 1, 2.0).unwrap();
751        p.set(1, 1, 3.0).unwrap();
752        p.set(0, 2, 4.0).unwrap();
753        p.set(1, 2, 5.0).unwrap();
754        p.set(2, 2, 6.0).unwrap();
755
756        assert_eq!(p.get(0, 0), Some(&1.0));
757        assert_eq!(p.get(0, 1), Some(&2.0));
758        assert_eq!(p.get(1, 1), Some(&3.0));
759        assert_eq!(p.get(0, 2), Some(&4.0));
760        assert_eq!(p.get(1, 2), Some(&5.0));
761        assert_eq!(p.get(2, 2), Some(&6.0));
762    }
763
764    #[test]
765    fn test_packed_lower_indexing() {
766        // For a 3x3 lower triangular matrix:
767        // [0  *  *]
768        // [1  3  *]
769        // [2  4  5]
770        // Packed: [a00, a10, a20, a11, a21, a22]
771        let mut p: PackedMat<f64> = PackedMat::zeros(3, TriangularKind::Lower);
772
773        // Check indices
774        assert_eq!(p.packed_index(0, 0), Some(0));
775        assert_eq!(p.packed_index(1, 0), Some(1));
776        assert_eq!(p.packed_index(2, 0), Some(2));
777        assert_eq!(p.packed_index(1, 1), Some(3));
778        assert_eq!(p.packed_index(2, 1), Some(4));
779        assert_eq!(p.packed_index(2, 2), Some(5));
780
781        // Above diagonal should return None
782        assert_eq!(p.packed_index(0, 1), None);
783        assert_eq!(p.packed_index(0, 2), None);
784        assert_eq!(p.packed_index(1, 2), None);
785
786        // Set and get values
787        p.set(0, 0, 1.0).unwrap();
788        p.set(1, 0, 2.0).unwrap();
789        p.set(2, 0, 3.0).unwrap();
790        p.set(1, 1, 4.0).unwrap();
791        p.set(2, 1, 5.0).unwrap();
792        p.set(2, 2, 6.0).unwrap();
793
794        assert_eq!(p.get(0, 0), Some(&1.0));
795        assert_eq!(p.get(1, 0), Some(&2.0));
796        assert_eq!(p.get(2, 0), Some(&3.0));
797        assert_eq!(p.get(1, 1), Some(&4.0));
798        assert_eq!(p.get(2, 1), Some(&5.0));
799        assert_eq!(p.get(2, 2), Some(&6.0));
800    }
801
802    #[test]
803    fn test_packed_len() {
804        assert_eq!(PackedMat::<f64>::packed_len(0), 0);
805        assert_eq!(PackedMat::<f64>::packed_len(1), 1);
806        assert_eq!(PackedMat::<f64>::packed_len(2), 3);
807        assert_eq!(PackedMat::<f64>::packed_len(3), 6);
808        assert_eq!(PackedMat::<f64>::packed_len(4), 10);
809        assert_eq!(PackedMat::<f64>::packed_len(10), 55);
810    }
811
812    #[test]
813    fn test_packed_to_dense() {
814        let mut p: PackedMat<f64> = PackedMat::zeros(3, TriangularKind::Upper);
815        p.set(0, 0, 1.0).unwrap();
816        p.set(0, 1, 2.0).unwrap();
817        p.set(1, 1, 3.0).unwrap();
818        p.set(0, 2, 4.0).unwrap();
819        p.set(1, 2, 5.0).unwrap();
820        p.set(2, 2, 6.0).unwrap();
821
822        let dense = p.to_dense();
823        assert_eq!(dense[(0, 0)], 1.0);
824        assert_eq!(dense[(0, 1)], 2.0);
825        assert_eq!(dense[(1, 1)], 3.0);
826        assert_eq!(dense[(0, 2)], 4.0);
827        assert_eq!(dense[(1, 2)], 5.0);
828        assert_eq!(dense[(2, 2)], 6.0);
829
830        // Below diagonal should be zero
831        assert_eq!(dense[(1, 0)], 0.0);
832        assert_eq!(dense[(2, 0)], 0.0);
833        assert_eq!(dense[(2, 1)], 0.0);
834    }
835
836    #[test]
837    fn test_packed_from_dense() {
838        use crate::Mat;
839
840        let dense = Mat::from_rows(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0], &[7.0, 8.0, 9.0]]);
841
842        let upper = PackedMat::from_dense(&dense.as_ref(), TriangularKind::Upper);
843        assert_eq!(upper.get(0, 0), Some(&1.0));
844        assert_eq!(upper.get(0, 1), Some(&2.0));
845        assert_eq!(upper.get(0, 2), Some(&3.0));
846        assert_eq!(upper.get(1, 1), Some(&5.0));
847        assert_eq!(upper.get(1, 2), Some(&6.0));
848        assert_eq!(upper.get(2, 2), Some(&9.0));
849
850        let lower = PackedMat::from_dense(&dense.as_ref(), TriangularKind::Lower);
851        assert_eq!(lower.get(0, 0), Some(&1.0));
852        assert_eq!(lower.get(1, 0), Some(&4.0));
853        assert_eq!(lower.get(2, 0), Some(&7.0));
854        assert_eq!(lower.get(1, 1), Some(&5.0));
855        assert_eq!(lower.get(2, 1), Some(&8.0));
856        assert_eq!(lower.get(2, 2), Some(&9.0));
857    }
858
859    #[test]
860    fn test_packed_diagonal() {
861        let mut p: PackedMat<f64> = PackedMat::zeros(3, TriangularKind::Upper);
862        p.set(0, 0, 1.0).unwrap();
863        p.set(0, 1, 10.0).unwrap();
864        p.set(1, 1, 2.0).unwrap();
865        p.set(0, 2, 20.0).unwrap();
866        p.set(1, 2, 30.0).unwrap();
867        p.set(2, 2, 3.0).unwrap();
868
869        let diag = p.diagonal();
870        assert_eq!(diag, vec![1.0, 2.0, 3.0]);
871
872        // Set diagonal
873        p.set_diagonal(&[10.0, 20.0, 30.0]);
874        let diag2 = p.diagonal();
875        assert_eq!(diag2, vec![10.0, 20.0, 30.0]);
876    }
877
878    #[test]
879    fn test_packed_transpose() {
880        let mut upper: PackedMat<f64> = PackedMat::zeros(3, TriangularKind::Upper);
881        upper.set(0, 0, 1.0).unwrap();
882        upper.set(0, 1, 2.0).unwrap();
883        upper.set(1, 1, 3.0).unwrap();
884        upper.set(0, 2, 4.0).unwrap();
885        upper.set(1, 2, 5.0).unwrap();
886        upper.set(2, 2, 6.0).unwrap();
887
888        let lower = upper.transpose();
889        assert_eq!(lower.kind(), TriangularKind::Lower);
890
891        // Transposed elements should be swapped
892        assert_eq!(lower.get(0, 0), Some(&1.0));
893        assert_eq!(lower.get(1, 0), Some(&2.0)); // Was (0, 1)
894        assert_eq!(lower.get(1, 1), Some(&3.0));
895        assert_eq!(lower.get(2, 0), Some(&4.0)); // Was (0, 2)
896        assert_eq!(lower.get(2, 1), Some(&5.0)); // Was (1, 2)
897        assert_eq!(lower.get(2, 2), Some(&6.0));
898    }
899
900    #[test]
901    fn test_packed_ref() {
902        let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
903        let pref = PackedRef::from_slice(&data, 3, TriangularKind::Upper);
904
905        assert_eq!(pref.dim(), 3);
906        assert_eq!(pref.get(0, 0), Some(&1.0));
907        assert_eq!(pref.get(0, 1), Some(&2.0));
908        assert_eq!(pref.get(1, 1), Some(&3.0));
909        assert_eq!(pref.get(0, 2), Some(&4.0));
910        assert_eq!(pref.get(1, 2), Some(&5.0));
911        assert_eq!(pref.get(2, 2), Some(&6.0));
912    }
913
914    #[test]
915    fn test_packed_mut() {
916        let mut data = [0.0f64; 6];
917        let mut pmut = PackedMut::from_slice(&mut data, 3, TriangularKind::Lower);
918
919        pmut.set(0, 0, 1.0).unwrap();
920        pmut.set(1, 0, 2.0).unwrap();
921        pmut.set(2, 0, 3.0).unwrap();
922        pmut.set(1, 1, 4.0).unwrap();
923        pmut.set(2, 1, 5.0).unwrap();
924        pmut.set(2, 2, 6.0).unwrap();
925
926        assert_eq!(data, [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
927    }
928
929    #[test]
930    fn test_packed_scale() {
931        let mut p: PackedMat<f64> = PackedMat::zeros(2, TriangularKind::Upper);
932        p.set(0, 0, 1.0).unwrap();
933        p.set(0, 1, 2.0).unwrap();
934        p.set(1, 1, 3.0).unwrap();
935
936        p.scale(2.0);
937
938        assert_eq!(p.get(0, 0), Some(&2.0));
939        assert_eq!(p.get(0, 1), Some(&4.0));
940        assert_eq!(p.get(1, 1), Some(&6.0));
941    }
942
943    // Regression test for finding #1: `PackedMat::set` used to `.expect()`
944    // (panic) on an out-of-triangle index instead of returning a typed
945    // error. A caller passing a below-diagonal index into upper-triangular
946    // storage (or vice versa) must get `Err(OutOfTriangleError)` back, not
947    // a panic.
948    #[test]
949    fn test_packed_mat_set_out_of_triangle_returns_error_not_panic() {
950        let mut upper: PackedMat<f64> = PackedMat::zeros(3, TriangularKind::Upper);
951        let err = upper
952            .set(1, 0, 99.0)
953            .expect_err("(1, 0) is below the diagonal of upper-triangular storage");
954        assert_eq!(
955            err,
956            OutOfTriangleError {
957                row: 1,
958                col: 0,
959                dim: 3,
960                kind: TriangularKind::Upper,
961            }
962        );
963        // Display must not panic and should surface the offending index.
964        let message = err.to_string();
965        assert!(message.contains("(1, 0)"), "message was: {message}");
966
967        let mut lower: PackedMat<f64> = PackedMat::zeros(3, TriangularKind::Lower);
968        assert!(lower.set(0, 2, 1.0).is_err());
969
970        // Out-of-bounds indices (>= n) must also be a typed error, not a panic.
971        assert!(upper.set(5, 5, 1.0).is_err());
972
973        // The matrix must be left untouched by a failed `set`.
974        assert_eq!(upper.get(0, 0), Some(&0.0));
975    }
976
977    // Same regression, but for the raw-pointer `PackedMut` view, which has
978    // its own independent `packed_index().expect(...)` call site.
979    #[test]
980    fn test_packed_mut_set_out_of_triangle_returns_error_not_panic() {
981        let mut data = [0.0f64; 6];
982        let mut pmut = PackedMut::from_slice(&mut data, 3, TriangularKind::Lower);
983
984        let err = pmut
985            .set(0, 1, 42.0)
986            .expect_err("(0, 1) is above the diagonal of lower-triangular storage");
987        assert_eq!(err.row, 0);
988        assert_eq!(err.col, 1);
989        assert_eq!(err.dim, 3);
990        assert_eq!(err.kind, TriangularKind::Lower);
991
992        // A valid index still succeeds and writes through the pointer.
993        pmut.set(0, 0, 7.0).unwrap();
994        assert_eq!(data[0], 7.0);
995    }
996
997    // --- Regression: `packed_len` must not wrap ------------------------------
998    //
999    // `n * (n + 1) / 2` with plain arithmetic wraps in release builds, so
1000    // `PackedMat::<f64>::zeros(1 << 32)` allocated a ~2^31-element buffer for a
1001    // matrix whose `packed_index` reaches ~2^63, and the same wrapped value was
1002    // used as `from_slice`'s soundness assertion. It must be a loud, defined
1003    // panic on every profile instead.
1004
1005    #[test]
1006    #[should_panic(expected = "packed length overflow")]
1007    fn test_packed_len_overflow_panics_not_wraps() {
1008        // 2^32 * (2^32 + 1) = 2^64 + 2^32, i.e. just past usize::MAX on 64-bit.
1009        let _ = PackedMat::<f64>::packed_len(1usize << 32);
1010    }
1011
1012    #[test]
1013    #[should_panic(expected = "packed length overflow")]
1014    fn test_packed_zeros_overflow_panics_not_wraps() {
1015        let _: PackedMat<f64> = PackedMat::zeros(1usize << 32, TriangularKind::Upper);
1016    }
1017
1018    #[test]
1019    fn test_packed_len_is_still_exact_for_sane_dims() {
1020        assert_eq!(PackedMat::<f64>::packed_len(0), 0);
1021        assert_eq!(PackedMat::<f64>::packed_len(1), 1);
1022        assert_eq!(PackedMat::<f64>::packed_len(3), 6);
1023        assert_eq!(PackedMat::<f64>::packed_len(100), 5050);
1024    }
1025
1026    // --- Regression: the safe view constructors validate their slice ---------
1027
1028    #[test]
1029    #[should_panic(expected = "Slice length must equal")]
1030    fn test_packed_ref_from_slice_rejects_short_slice() {
1031        // The unsound path was `PackedRef::new(v.as_ptr(), 1000, ..)` on a
1032        // 10-element buffer from 100% safe code; `new` is now `unsafe`, and the
1033        // safe `from_slice` alternative rejects the mismatch outright.
1034        let data = [0.0f64; 10];
1035        let _ = PackedRef::from_slice(&data, 1000, TriangularKind::Upper);
1036    }
1037
1038    #[test]
1039    #[should_panic(expected = "Slice length must equal")]
1040    fn test_packed_mut_from_slice_rejects_short_slice() {
1041        let mut data = [0.0f64; 10];
1042        let _ = PackedMut::from_slice(&mut data, 1000, TriangularKind::Upper);
1043    }
1044}