Skip to main content

tropical_gemm/mat/
owned.rs

1//! Owned matrix type.
2
3use std::ops::{Index, IndexMut};
4
5use crate::core::Transpose;
6use crate::simd::{tropical_gemm_dispatch, KernelDispatch};
7use crate::types::{TropicalSemiring, TropicalWithArgmax};
8
9use super::{MatRef, MatWithArgmax};
10
11/// Owned matrix storing semiring values.
12///
13/// The matrix stores values in column-major order (Fortran/BLAS convention).
14/// Use factory methods to create matrices:
15///
16/// ```
17/// use tropical_gemm::{Mat, MaxPlus, TropicalSemiring};
18///
19/// let zeros = Mat::<MaxPlus<f32>>::zeros(3, 4);
20/// let identity = Mat::<MaxPlus<f32>>::identity(3);
21/// let custom = Mat::<MaxPlus<f32>>::from_fn(2, 2, |i, j| {
22///     MaxPlus::<f32>::from_scalar((i + j) as f32)
23/// });
24/// ```
25#[derive(Debug, Clone)]
26pub struct Mat<S: TropicalSemiring> {
27    pub(crate) data: Vec<S>,
28    pub(crate) scalars: std::sync::OnceLock<Vec<S::Scalar>>,
29    pub(crate) nrows: usize,
30    pub(crate) ncols: usize,
31}
32
33impl<S: TropicalSemiring> Mat<S> {
34    /// Create a matrix filled with tropical zeros.
35    ///
36    /// For MaxPlus, this fills with -∞.
37    /// For MinPlus, this fills with +∞.
38    pub fn zeros(nrows: usize, ncols: usize) -> Self {
39        Self {
40            scalars: Default::default(),
41            data: vec![
42                S::tropical_zero();
43                nrows
44                    .checked_mul(ncols)
45                    .expect("matrix dimensions overflow")
46            ],
47            nrows,
48            ncols,
49        }
50    }
51
52    /// Create a tropical identity matrix.
53    ///
54    /// Diagonal elements are tropical one (0 for MaxPlus/MinPlus).
55    /// Off-diagonal elements are tropical zero (-∞ for MaxPlus, +∞ for MinPlus).
56    pub fn identity(n: usize) -> Self {
57        let mut mat = Self::zeros(n, n);
58        for i in 0..n {
59            // Column-major: diagonal element (i, i) at index i + i * n
60            mat.data[i + i * n] = S::tropical_one();
61        }
62        mat
63    }
64
65    /// Create a matrix from a function.
66    ///
67    /// The function is called with (row, col) indices.
68    /// Data is stored in column-major order internally.
69    pub fn from_fn<F>(nrows: usize, ncols: usize, mut f: F) -> Self
70    where
71        F: FnMut(usize, usize) -> S,
72    {
73        // Column-major: iterate column by column
74        let data = (0..nrows
75            .checked_mul(ncols)
76            .expect("matrix dimensions overflow"))
77            .map(|idx| f(idx % nrows, idx / nrows))
78            .collect();
79        Self {
80            data,
81            nrows,
82            ncols,
83            scalars: Default::default(),
84        }
85    }
86
87    /// Create a matrix from column-major scalar data.
88    ///
89    /// Each scalar is wrapped in the semiring type.
90    /// Data should be in column-major order: first column, then second column, etc.
91    pub fn from_col_major(data: &[S::Scalar], nrows: usize, ncols: usize) -> Self
92    where
93        S::Scalar: Copy,
94    {
95        assert_eq!(
96            data.len(),
97            nrows
98                .checked_mul(ncols)
99                .expect("matrix dimensions overflow"),
100            "data length {} != nrows {} * ncols {}",
101            data.len(),
102            nrows,
103            ncols
104        );
105        let data = data.iter().map(|&s| S::from_scalar(s)).collect();
106        Self {
107            data,
108            nrows,
109            ncols,
110            scalars: Default::default(),
111        }
112    }
113
114    /// Create a matrix from row-major scalar data.
115    ///
116    /// This is a convenience method that converts row-major input to column-major storage.
117    ///
118    /// # Performance Warning
119    ///
120    /// This method performs an O(m×n) transpose operation. For performance-critical code,
121    /// provide data in column-major order and use [`from_col_major`] instead.
122    #[deprecated(
123        note = "use from_col_major instead for direct column-major input; this method has O(m×n) transpose overhead"
124    )]
125    pub fn from_row_major(data: &[S::Scalar], nrows: usize, ncols: usize) -> Self
126    where
127        S::Scalar: Copy,
128    {
129        assert_eq!(
130            data.len(),
131            nrows
132                .checked_mul(ncols)
133                .expect("matrix dimensions overflow"),
134            "data length {} != nrows {} * ncols {}",
135            data.len(),
136            nrows,
137            ncols
138        );
139        // Convert row-major to column-major
140        let col_major: Vec<S> = (0..nrows
141            .checked_mul(ncols)
142            .expect("matrix dimensions overflow"))
143            .map(|idx| {
144                let i = idx % nrows;
145                let j = idx / nrows;
146                S::from_scalar(data[i * ncols + j])
147            })
148            .collect();
149        Self {
150            data: col_major,
151            nrows,
152            ncols,
153            scalars: Default::default(),
154        }
155    }
156
157    /// Create a matrix from a vector of semiring values.
158    pub fn from_vec(data: Vec<S>, nrows: usize, ncols: usize) -> Self {
159        assert_eq!(
160            data.len(),
161            nrows
162                .checked_mul(ncols)
163                .expect("matrix dimensions overflow"),
164            "data length {} != nrows {} * ncols {}",
165            data.len(),
166            nrows,
167            ncols
168        );
169        Self {
170            data,
171            nrows,
172            ncols,
173            scalars: Default::default(),
174        }
175    }
176
177    /// Number of rows.
178    #[inline]
179    pub fn nrows(&self) -> usize {
180        self.nrows
181    }
182
183    /// Number of columns.
184    #[inline]
185    pub fn ncols(&self) -> usize {
186        self.ncols
187    }
188
189    /// Get the underlying data as a slice.
190    #[inline]
191    pub fn as_slice(&self) -> &[S] {
192        &self.data
193    }
194
195    /// Get the underlying data as a mutable slice.
196    #[inline]
197    pub fn as_mut_slice(&mut self) -> &mut [S] {
198        self.scalars.take();
199        &mut self.data
200    }
201
202    /// Get the scalar value at position (i, j).
203    ///
204    /// This is a convenience method that extracts the underlying scalar
205    /// without requiring a trait import.
206    ///
207    /// # Example
208    ///
209    /// ```
210    /// use tropical_gemm::{Mat, MaxPlus};
211    ///
212    /// let m = Mat::<MaxPlus<f64>>::from_col_major(&[1.0, 3.0, 2.0, 4.0], 2, 2);
213    /// assert_eq!(m.get_value(0, 0), 1.0);
214    /// assert_eq!(m.get_value(1, 1), 4.0);
215    /// ```
216    #[inline]
217    pub fn get_value(&self, i: usize, j: usize) -> S::Scalar {
218        self[(i, j)].value()
219    }
220
221    /// Convert to an immutable matrix reference.
222    ///
223    /// The returned reference views the scalar values.
224    pub fn as_ref(&self) -> MatRef<'_, S>
225    where
226        S::Scalar: Copy,
227    {
228        MatRef::from_mat(self)
229    }
230
231    /// Get a mutable pointer to the data.
232    #[inline]
233    pub fn as_mut_ptr(&mut self) -> *mut S {
234        self.scalars.take();
235        self.data.as_mut_ptr()
236    }
237}
238
239impl<S: TropicalSemiring> Index<(usize, usize)> for Mat<S> {
240    type Output = S;
241
242    #[inline]
243    fn index(&self, (i, j): (usize, usize)) -> &S {
244        debug_assert!(
245            i < self.nrows,
246            "row index {} out of bounds {}",
247            i,
248            self.nrows
249        );
250        debug_assert!(
251            j < self.ncols,
252            "col index {} out of bounds {}",
253            j,
254            self.ncols
255        );
256        // Column-major indexing
257        &self.data[j * self.nrows + i]
258    }
259}
260
261impl<S: TropicalSemiring> IndexMut<(usize, usize)> for Mat<S> {
262    #[inline]
263    fn index_mut(&mut self, (i, j): (usize, usize)) -> &mut S {
264        debug_assert!(
265            i < self.nrows,
266            "row index {} out of bounds {}",
267            i,
268            self.nrows
269        );
270        debug_assert!(
271            j < self.ncols,
272            "col index {} out of bounds {}",
273            j,
274            self.ncols
275        );
276        // Column-major indexing
277        self.scalars.take();
278        &mut self.data[j * self.nrows + i]
279    }
280}
281
282// Matrix multiplication methods directly on Mat
283impl<S> Mat<S>
284where
285    S: TropicalSemiring + KernelDispatch,
286    S::Scalar: Copy,
287{
288    /// Perform tropical matrix multiplication: C = A ⊗ B.
289    ///
290    /// Computes C[i,j] = ⊕_k (A[i,k] ⊗ B[k,j])
291    ///
292    /// # Panics
293    ///
294    /// Panics if dimensions don't match (self.ncols != b.nrows).
295    ///
296    /// # Example
297    ///
298    /// ```
299    /// use tropical_gemm::{Mat, MaxPlus, TropicalSemiring};
300    ///
301    /// let a = Mat::<MaxPlus<f64>>::from_col_major(&[1.0, 4.0, 2.0, 5.0, 3.0, 6.0], 2, 3);
302    /// let b = Mat::<MaxPlus<f64>>::from_col_major(&[1.0, 3.0, 5.0, 2.0, 4.0, 6.0], 3, 2);
303    ///
304    /// let c = a.matmul(&b);
305    ///
306    /// // C[0,0] = max(1+1, 2+3, 3+5) = 8
307    /// assert_eq!(c[(0, 0)].value(), 8.0);
308    /// ```
309    pub fn matmul(&self, b: &Mat<S>) -> Mat<S> {
310        assert_eq!(
311            self.ncols, b.nrows,
312            "dimension mismatch: A is {}x{}, B is {}x{}",
313            self.nrows, self.ncols, b.nrows, b.ncols
314        );
315
316        let a_ref = self.as_ref();
317        let b_ref = b.as_ref();
318
319        let m = self.nrows;
320        let n = b.ncols;
321        let k = self.ncols;
322
323        let mut c = Mat::<S>::zeros(m, n);
324
325        // The kernel uses row-major convention. For column-major data,
326        // we use the transpose trick: C = A * B becomes C^T = B^T * A^T.
327        // Column-major A (m×k) viewed as row-major is A^T (k×m) with ld=m.
328        // So we swap A and B, swap m and n, and the result is written
329        // in the correct column-major layout.
330        unsafe {
331            tropical_gemm_dispatch::<S>(
332                n, // rows of C^T = cols of C
333                m, // cols of C^T = rows of C
334                k,
335                b_ref.as_slice().as_ptr(), // B becomes first operand (B^T)
336                k,                         // lda = nrows of B in col-major
337                Transpose::NoTrans,
338                a_ref.as_slice().as_ptr(), // A becomes second operand (A^T)
339                m,                         // ldb = nrows of A in col-major
340                Transpose::NoTrans,
341                c.data.as_mut_ptr(),
342                m, // ldc = nrows of C in col-major
343            );
344        }
345
346        c
347    }
348
349    /// Perform tropical matrix multiplication with a MatRef.
350    ///
351    /// This allows mixing owned and reference matrices.
352    pub fn matmul_ref(&self, b: &MatRef<S>) -> Mat<S> {
353        assert_eq!(
354            self.ncols,
355            b.nrows(),
356            "dimension mismatch: A is {}x{}, B is {}x{}",
357            self.nrows,
358            self.ncols,
359            b.nrows(),
360            b.ncols()
361        );
362
363        let a_ref = self.as_ref();
364
365        let m = self.nrows;
366        let n = b.ncols();
367        let k = self.ncols;
368
369        let mut c = Mat::<S>::zeros(m, n);
370
371        // Transpose trick for column-major: C = A * B becomes C^T = B^T * A^T
372        unsafe {
373            tropical_gemm_dispatch::<S>(
374                n,
375                m,
376                k,
377                b.as_slice().as_ptr(),
378                k,
379                Transpose::NoTrans,
380                a_ref.as_slice().as_ptr(),
381                m,
382                Transpose::NoTrans,
383                c.data.as_mut_ptr(),
384                m,
385            );
386        }
387
388        c
389    }
390}
391
392// Argmax methods on Mat
393impl<S> Mat<S>
394where
395    S: TropicalWithArgmax<Index = u32> + KernelDispatch,
396    S::Scalar: Copy,
397{
398    /// Perform tropical matrix multiplication with argmax tracking.
399    ///
400    /// Returns both the result matrix and the argmax indices indicating
401    /// which k-index produced each optimal value.
402    ///
403    /// # Example
404    ///
405    /// ```
406    /// use tropical_gemm::{Mat, MaxPlus, TropicalSemiring};
407    ///
408    /// let a = Mat::<MaxPlus<f64>>::from_col_major(&[1.0, 4.0, 2.0, 5.0, 3.0, 6.0], 2, 3);
409    /// let b = Mat::<MaxPlus<f64>>::from_col_major(&[1.0, 3.0, 5.0, 2.0, 4.0, 6.0], 3, 2);
410    ///
411    /// let result = a.matmul_argmax(&b);
412    ///
413    /// assert_eq!(result.get(0, 0).value(), 8.0);
414    /// assert_eq!(result.get_argmax(0, 0), 2); // k=2 gave max
415    /// ```
416    pub fn matmul_argmax(&self, b: &Mat<S>) -> MatWithArgmax<S> {
417        assert_eq!(
418            self.ncols, b.nrows,
419            "dimension mismatch: A is {}x{}, B is {}x{}",
420            self.nrows, self.ncols, b.nrows, b.ncols
421        );
422
423        let a_ref = self.as_ref();
424        let b_ref = b.as_ref();
425
426        let m = self.nrows;
427        let n = b.ncols;
428        let k = self.ncols;
429
430        // The kernel outputs row-major. We use the transpose trick:
431        // C = A * B becomes C^T = B^T * A^T.
432        // Create result with swapped dimensions (n×m) which the kernel fills
433        // in row-major, then we interpret as (m×n) column-major.
434        let mut result = crate::core::GemmWithArgmax::<S>::new(n, m);
435
436        unsafe {
437            crate::simd::tropical_gemm_with_argmax_dispatch::<S>(
438                n,
439                m,
440                k,
441                b_ref.as_slice().as_ptr(),
442                k,
443                Transpose::NoTrans,
444                a_ref.as_slice().as_ptr(),
445                m,
446                Transpose::NoTrans,
447                &mut result,
448            );
449        }
450
451        // The result is stored as (n×m) row-major = (m×n) column-major
452        MatWithArgmax {
453            values: Mat {
454                scalars: Default::default(),
455                data: result.values,
456                nrows: m,
457                ncols: n,
458            },
459            argmax: result.argmax,
460        }
461    }
462
463    /// Batched tropical matrix multiplication with argmax tracking.
464    ///
465    /// Computes C[i] = A[i] ⊗ B[i] for each pair of matrices in the batch,
466    /// tracking which k-index produced each optimal value.
467    ///
468    /// All matrices in `a_batch` must have the same dimensions, and all
469    /// matrices in `b_batch` must have the same dimensions.
470    ///
471    /// # Panics
472    ///
473    /// Panics if:
474    /// - `a_batch` and `b_batch` have different lengths
475    /// - Matrices in `a_batch` have different dimensions
476    /// - Matrices in `b_batch` have different dimensions
477    /// - Inner dimensions don't match (A.ncols != B.nrows)
478    ///
479    /// # Example
480    ///
481    /// ```
482    /// use tropical_gemm::{Mat, MaxPlus};
483    ///
484    /// let a1 = Mat::<MaxPlus<f32>>::from_col_major(&[1.0, 3.0, 2.0, 4.0], 2, 2);
485    /// let a2 = Mat::<MaxPlus<f32>>::from_col_major(&[5.0, 7.0, 6.0, 8.0], 2, 2);
486    /// let b1 = Mat::<MaxPlus<f32>>::from_col_major(&[1.0, 0.0, 0.0, 1.0], 2, 2);
487    /// let b2 = Mat::<MaxPlus<f32>>::from_col_major(&[1.0, 3.0, 2.0, 4.0], 2, 2);
488    ///
489    /// let results = Mat::matmul_batched_with_argmax(&[a1, a2], &[b1, b2]);
490    /// assert_eq!(results.len(), 2);
491    /// ```
492    pub fn matmul_batched_with_argmax(
493        a_batch: &[Mat<S>],
494        b_batch: &[Mat<S>],
495    ) -> Vec<MatWithArgmax<S>> {
496        assert_eq!(
497            a_batch.len(),
498            b_batch.len(),
499            "batch sizes must match: {} != {}",
500            a_batch.len(),
501            b_batch.len()
502        );
503
504        if a_batch.is_empty() {
505            return Vec::new();
506        }
507
508        // Validate dimensions
509        let (m, k) = (a_batch[0].nrows, a_batch[0].ncols);
510        let n = b_batch[0].ncols;
511
512        for (i, (a, b)) in a_batch.iter().zip(b_batch.iter()).enumerate() {
513            assert_eq!(
514                (a.nrows, a.ncols),
515                (m, k),
516                "A[{}] has dimensions {}x{}, expected {}x{}",
517                i,
518                a.nrows,
519                a.ncols,
520                m,
521                k
522            );
523            assert_eq!(
524                (b.nrows, b.ncols),
525                (k, n),
526                "B[{}] has dimensions {}x{}, expected {}x{}",
527                i,
528                b.nrows,
529                b.ncols,
530                k,
531                n
532            );
533        }
534
535        a_batch
536            .iter()
537            .zip(b_batch.iter())
538            .map(|(a, b)| a.matmul_argmax(b))
539            .collect()
540    }
541}
542
543// Batched operations on Mat
544impl<S> Mat<S>
545where
546    S: TropicalSemiring + KernelDispatch,
547    S::Scalar: Copy,
548{
549    /// Batched tropical matrix multiplication.
550    ///
551    /// Computes C[i] = A[i] ⊗ B[i] for each pair of matrices in the batch.
552    /// All matrices in `a_batch` must have the same dimensions, and all
553    /// matrices in `b_batch` must have the same dimensions.
554    ///
555    /// # Panics
556    ///
557    /// Panics if:
558    /// - `a_batch` and `b_batch` have different lengths
559    /// - Matrices in `a_batch` have different dimensions
560    /// - Matrices in `b_batch` have different dimensions
561    /// - Inner dimensions don't match (A.ncols != B.nrows)
562    ///
563    /// # Example
564    ///
565    /// ```
566    /// use tropical_gemm::{Mat, MaxPlus};
567    ///
568    /// let a1 = Mat::<MaxPlus<f32>>::from_col_major(&[1.0, 3.0, 2.0, 4.0], 2, 2);
569    /// let a2 = Mat::<MaxPlus<f32>>::from_col_major(&[5.0, 7.0, 6.0, 8.0], 2, 2);
570    /// let b1 = Mat::<MaxPlus<f32>>::from_col_major(&[1.0, 0.0, 0.0, 1.0], 2, 2);
571    /// let b2 = Mat::<MaxPlus<f32>>::from_col_major(&[1.0, 3.0, 2.0, 4.0], 2, 2);
572    ///
573    /// let results = Mat::matmul_batched(&[a1, a2], &[b1, b2]);
574    /// assert_eq!(results.len(), 2);
575    /// ```
576    pub fn matmul_batched(a_batch: &[Mat<S>], b_batch: &[Mat<S>]) -> Vec<Mat<S>> {
577        assert_eq!(
578            a_batch.len(),
579            b_batch.len(),
580            "batch sizes must match: {} != {}",
581            a_batch.len(),
582            b_batch.len()
583        );
584
585        if a_batch.is_empty() {
586            return Vec::new();
587        }
588
589        // Validate dimensions
590        let (m, k) = (a_batch[0].nrows, a_batch[0].ncols);
591        let n = b_batch[0].ncols;
592
593        for (i, (a, b)) in a_batch.iter().zip(b_batch.iter()).enumerate() {
594            assert_eq!(
595                (a.nrows, a.ncols),
596                (m, k),
597                "A[{}] has dimensions {}x{}, expected {}x{}",
598                i,
599                a.nrows,
600                a.ncols,
601                m,
602                k
603            );
604            assert_eq!(
605                (b.nrows, b.ncols),
606                (k, n),
607                "B[{}] has dimensions {}x{}, expected {}x{}",
608                i,
609                b.nrows,
610                b.ncols,
611                k,
612                n
613            );
614        }
615
616        a_batch
617            .iter()
618            .zip(b_batch.iter())
619            .map(|(a, b)| a.matmul(b))
620            .collect()
621    }
622}