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}