1use std::iter::Sum;
39use std::ops::AddAssign;
40
41use mdarray::{Array, Dim, DynRank, Layout, Shape, Slice, View};
42use num_complex::ComplexFloat;
43use num_traits::{MulAdd, One, Zero};
44
45pub trait Contract<T> {
47 fn matmul<'a, D0, D1, D2, La, Lb>(
58 &self,
59 a: &'a Slice<T, (D0, D1), La>,
60 b: &'a Slice<T, (D1, D2), Lb>,
61 ) -> impl MatmulBuilder<'a, T, D0, D1, D2, La, Lb>
62 where
63 D0: Dim,
64 D1: Dim,
65 D2: Dim,
66 La: Layout,
67 Lb: Layout;
68
69 fn contract_all<'a, Sa, Sb, La, Lb>(
73 &self,
74 a: &'a Slice<T, Sa, La>,
75 b: &'a Slice<T, Sb, Lb>,
76 ) -> T
77 where
78 T: 'a,
79 Sa: Shape,
80 Sb: Shape,
81 La: Layout,
82 Lb: Layout;
83
84 fn contract_n<'a, Sa, Sb, La, Lb>(
88 &self,
89 a: &'a Slice<T, Sa, La>,
90 b: &'a Slice<T, Sb, Lb>,
91 n: usize,
92 ) -> impl ContractBuilder<'a, T, Sa, Sb, La, Lb>
93 where
94 T: 'a,
95 Sa: Shape,
96 Sb: Shape,
97 La: Layout,
98 Lb: Layout;
99
100 fn contract_pairs<'a, Sa, Sb, La, Lb>(
105 &self,
106 a: &'a Slice<T, Sa, La>,
107 b: &'a Slice<T, Sb, Lb>,
108 axes_a: &'a [usize],
109 axes_b: &'a [usize],
110 ) -> impl ContractBuilder<'a, T, Sa, Sb, La, Lb>
111 where
112 T: 'a,
113 Sa: Shape,
114 Sb: Shape,
115 La: Layout,
116 Lb: Layout;
117
118 fn contract<'a, Sa, Sb, La, Lb>(
137 &self,
138 a: &'a Slice<T, Sa, La>,
139 b: &'a Slice<T, Sb, Lb>,
140 indices_a: &'a [u8],
141 indices_b: &'a [u8],
142 indices_c: &'a [u8],
143 ) -> impl ContractBuilder<'a, T, Sa, Sb, La, Lb>
144 where
145 T: 'a,
146 Sa: Shape,
147 Sb: Shape,
148 La: Layout,
149 Lb: Layout;
150}
151
152pub trait MatmulBuilder<'a, T, D0, D1, D2, La, Lb>
154where
155 T: 'a,
156 D0: Dim,
157 D1: Dim,
158 D2: Dim,
159 La: 'a + Layout,
160 Lb: 'a + Layout,
161{
162 fn scale(self, factor: T) -> Self;
164
165 fn eval(self) -> Array<T, (D0, D2)>;
167
168 fn write<Lc: Layout>(self, c: &mut Slice<T, (D0, D2), Lc>);
170
171 fn add_to<Lc: Layout>(self, c: &mut Slice<T, (D0, D2), Lc>);
173
174 fn add_to_scaled<Lc: Layout>(self, c: &mut Slice<T, (D0, D2), Lc>, beta: T);
177}
178
179pub trait ContractBuilder<'a, T, Sa, Sb, La, Lb>
181where
182 T: 'a,
183 La: Layout,
184 Lb: Layout,
185{
186 fn scale(self, factor: T) -> Self;
188
189 fn eval(self) -> Array<T, DynRank>;
191
192 fn write<Sc: Shape, Lc: Layout>(self, c: &mut Slice<T, Sc, Lc>);
194
195 fn add_to<Sc: Shape, Lc: Layout>(self, c: &mut Slice<T, Sc, Lc>);
197
198 fn add_to_scaled<Sc: Shape, Lc: Layout>(self, c: &mut Slice<T, Sc, Lc>, beta: T);
201}
202
203#[doc(hidden)]
207pub enum Axes<'a> {
208 All,
209 LastFirst { k: usize },
210 Specific(&'a [usize], &'a [usize]),
211 SpecificOwned(Vec<usize>, Vec<usize>),
212}
213
214#[doc(hidden)]
215pub struct ContractAxes {
216 pub keep_size_a: usize,
217 pub keep_size_b: usize,
218 pub contract_size: usize,
219 pub keep_shape_a: Vec<usize>,
220 pub keep_shape_b: Vec<usize>,
221 pub order_a: Vec<usize>,
222 pub order_b: Vec<usize>,
223}
224
225#[doc(hidden)]
229pub fn extract_axes<T, Sa, Sb, La, Lb>(
230 axes: Axes,
231 a: &Slice<T, Sa, La>,
232 b: &Slice<T, Sb, Lb>,
233) -> ContractAxes
234where
235 T: Zero + ComplexFloat + MulAdd<Output = T>,
236 La: Layout,
237 Lb: Layout,
238 Sa: Shape,
239 Sb: Shape,
240{
241 let rank_a = a.rank();
242 let rank_b = b.rank();
243
244 let axes_a_storage: Option<Vec<usize>>;
245 let axes_b_storage: Option<Vec<usize>>;
246
247 let (axes_a, axes_b): (&[usize], &[usize]) = match axes {
248 Axes::All => {
249 axes_a_storage = Some((0..rank_a).collect());
250 axes_b_storage = Some((0..rank_b).collect());
251 (
252 axes_a_storage.as_deref().unwrap(),
253 axes_b_storage.as_deref().unwrap(),
254 )
255 }
256 Axes::LastFirst { k } => {
257 axes_a_storage = Some(((rank_a - k)..rank_a).collect());
258 axes_b_storage = Some((0..k).collect());
259 (
260 axes_a_storage.as_deref().unwrap(),
261 axes_b_storage.as_deref().unwrap(),
262 )
263 }
264 Axes::Specific(ax_a, ax_b) => (ax_a, ax_b),
265 Axes::SpecificOwned(ax_a, ax_b) => {
266 axes_a_storage = Some(ax_a);
267 axes_b_storage = Some(ax_b);
268 (
269 axes_a_storage.as_deref().unwrap(),
270 axes_b_storage.as_deref().unwrap(),
271 )
272 }
273 };
274
275 assert_eq!(
276 axes_a.len(),
277 axes_b.len(),
278 "Axis count mismatch: {} (tensor A) vs {} (tensor B)",
279 axes_a.len(),
280 axes_b.len()
281 );
282
283 let mut contract_size = 1;
284 for (&a_ax, &b_ax) in axes_a.iter().zip(axes_b) {
285 assert_eq!(
286 a.dim(a_ax),
287 b.dim(b_ax),
288 "Dimension mismatch at contraction: A[axis {}] = {} ≠ B[axis {}] = {}",
289 a_ax,
290 a.dim(a_ax),
291 b_ax,
292 b.dim(b_ax)
293 );
294 contract_size *= a.dim(a_ax);
295 }
296
297 let mut keep_shape_a = Vec::new();
298 let mut keep_size_a = 1;
299 let mut order_a = Vec::with_capacity(rank_a);
300
301 for i in 0..rank_a {
302 if !axes_a.contains(&i) {
303 keep_shape_a.push(a.dim(i));
304 keep_size_a *= a.dim(i);
305 order_a.push(i);
306 }
307 }
308 order_a.extend_from_slice(axes_a);
309
310 let mut keep_shape_b = Vec::new();
311 let mut keep_size_b = 1;
312 let mut order_b = Vec::with_capacity(rank_b);
313 order_b.extend_from_slice(axes_b);
314
315 for i in 0..rank_b {
316 if !axes_b.contains(&i) {
317 keep_shape_b.push(b.dim(i));
318 keep_size_b *= b.dim(i);
319 order_b.push(i);
320 }
321 }
322
323 ContractAxes {
324 keep_size_a,
325 keep_size_b,
326 contract_size,
327 keep_shape_a,
328 keep_shape_b,
329 order_a,
330 order_b,
331 }
332}
333
334#[doc(hidden)]
335#[macro_export]
336macro_rules! prepare_contraction {
337 ($axes:expr, $a:expr, $b:expr) => {{
338 let ContractAxes {
339 keep_size_a,
340 keep_size_b,
341 contract_size,
342 keep_shape_a,
343 keep_shape_b,
344 order_a,
345 order_b,
346 ..
347 } = extract_axes($axes, $a, $b);
348
349 let trans_a = $a.permute(order_a).to_tensor();
350 let a_2d = trans_a.reshape([keep_size_a, contract_size]).to_tensor();
351
352 let trans_b = $b.permute(order_b).to_tensor();
353 let b_2d = trans_b.reshape([contract_size, keep_size_b]).to_tensor(); (a_2d, b_2d, keep_shape_a, keep_shape_b)
356 }};
357}
358
359#[doc(hidden)]
360#[macro_export]
361macro_rules! finish_contraction {
362 ($ab:expr, $keep_shape_a:expr, $keep_shape_b:expr) => {{
363 let mut keep_shape_a = $keep_shape_a;
364 let keep_shape_b = $keep_shape_b;
365
366 if keep_shape_a.is_empty() && keep_shape_b.is_empty() {
367 mdarray::Array::from_elem((), $ab.into_scalar()).into_dyn()
368 } else if keep_shape_a.is_empty() {
369 $ab.view(0, ..)
370 .reshape(keep_shape_b)
371 .to_owned()
372 .into_dyn()
373 .into()
374 } else if keep_shape_b.is_empty() {
375 $ab.view(.., 0)
376 .reshape(keep_shape_a)
377 .to_owned()
378 .into_dyn()
379 .into()
380 } else {
381 keep_shape_a.extend(keep_shape_b);
382 $ab.reshape(keep_shape_a).to_owned().into_dyn().into()
383 }
384 }};
385}
386
387#[doc(hidden)]
391pub fn _contract<T, La, Lb, Sa, Sb>(
392 bd: impl Contract<T>,
393 a: &Slice<T, Sa, La>,
394 b: &Slice<T, Sb, Lb>,
395 axes: Axes,
396 alpha: T,
397) -> Array<T, DynRank>
398where
399 T: Zero + ComplexFloat + MulAdd<Output = T>,
400 La: Layout,
401 Lb: Layout,
402 Sa: Shape,
403 Sb: Shape,
404{
405 let (a_2d, b_2d, keep_shape_a, keep_shape_b) = prepare_contraction!(axes, a, b);
406
407 let ab = bd.matmul(&a_2d, &b_2d).scale(alpha).eval();
408
409 finish_contraction!(ab, keep_shape_a, keep_shape_b)
410}
411
412#[doc(hidden)]
413pub const FREE_AXIS: usize = usize::MAX;
414
415#[doc(hidden)]
421pub fn _hypercontract<T>(
422 bd: impl Contract<T>,
423 a: View<'_, T, DynRank>,
424 b: View<'_, T, DynRank>,
425 axes_a: &[usize],
426 axes_b: &[usize],
427) -> Array<T, DynRank>
428where
429 T: Copy + Zero + One + Sum + AddAssign + MulAdd<Output = T> + ComplexFloat,
430{
431 assert_eq!(
432 axes_a.len(),
433 a.rank(),
434 "hypercontract axes_a length ({}) must match A rank ({})",
435 axes_a.len(),
436 a.rank()
437 );
438 assert_eq!(
439 axes_b.len(),
440 b.rank(),
441 "hypercontract axes_b length ({}) must match B rank ({})",
442 axes_b.len(),
443 b.rank()
444 );
445
446 let edges = axes_to_hyperedges(axes_a, axes_b);
447
448 let mut a_owned: Option<Array<T, DynRank>> = None;
451 let mut b_owned: Option<Array<T, DynRank>> = None;
452
453 let mut map_a: Vec<usize> = (0..a.shape().dims().len()).collect();
456 let mut map_b: Vec<usize> = (0..b.shape().dims().len()).collect();
457
458 let mut axes_a: Vec<usize> = Vec::new();
461 let mut axes_b: Vec<usize> = Vec::new();
462
463 for edge in &edges {
464 let (idx_a, idx_b) = edge;
465
466 match (idx_a, idx_b) {
467 (Some(axes), None) => {
469 let view = a_owned
470 .as_ref()
471 .map(|o| o.expr())
472 .unwrap_or_else(|| a.clone());
473 a_owned = Some(apply_hypersum(&view, axes, &mut map_a));
474 }
475 (None, Some(axes)) => {
477 let view = b_owned
478 .as_ref()
479 .map(|o| o.expr())
480 .unwrap_or_else(|| b.clone());
481 b_owned = Some(apply_hypersum(&view, axes, &mut map_b));
482 }
483 (Some(axes_a_idx), Some(axes_b_idx)) => {
486 let ax_a = {
487 let view = a_owned
488 .as_ref()
489 .map(|o| o.expr())
490 .unwrap_or_else(|| a.clone());
491 let (arr, ax) = extract_hyperdiag(view, axes_a_idx, &mut map_a);
492 a_owned = Some(arr);
493 ax
494 };
495 let ax_b = {
496 let view = b_owned
497 .as_ref()
498 .map(|o| o.expr())
499 .unwrap_or_else(|| b.clone());
500 let (arr, ax) = extract_hyperdiag(view, axes_b_idx, &mut map_b);
501 b_owned = Some(arr);
502 ax
503 };
504 axes_a.push(ax_a);
505 axes_b.push(ax_b);
506 }
507
508 (None, None) => {}
509 }
510 }
511
512 let final_a = a_owned
514 .as_ref()
515 .map(|o| o.expr())
516 .unwrap_or_else(|| a.clone());
517 let final_b = b_owned
518 .as_ref()
519 .map(|o| o.expr())
520 .unwrap_or_else(|| b.clone());
521
522 _contract(
523 bd,
524 &final_a,
525 &final_b,
526 Axes::SpecificOwned(axes_a, axes_b),
527 T::one(),
528 )
529}
530
531#[doc(hidden)]
533pub fn hyperdiagonal<'a, T, L: Layout>(
534 a: View<'a, T, DynRank, L>,
535 axes: &[usize],
536) -> View<'a, T, DynRank, mdarray::Strided> {
537 let mut axes_sorted = axes.to_vec();
538 axes_sorted.sort_unstable();
539 axes_sorted.dedup();
540
541 let dims = a.shape().dims();
542 let rank = dims.len();
543
544 for &ax in &axes_sorted {
545 assert!(ax < rank, "axis ({ax}) out of bounds for rank {rank}");
546 }
547
548 let m = dims[*axes_sorted.first().unwrap()];
550 for &ax in &axes_sorted {
551 let n = dims[ax];
552 assert!(
553 m == n,
554 "all diagonal axes must have equal size, got {m} and {n}"
555 );
556 }
557
558 let mut out_dims: Vec<usize> = Vec::with_capacity(rank - axes_sorted.len() + 1);
560 let mut out_strides: Vec<isize> = Vec::with_capacity(rank - axes_sorted.len() + 1);
561
562 for (i, item) in dims.iter().enumerate() {
563 if axes_sorted.binary_search(&i).is_ok() {
564 continue;
565 }
566 out_dims.push(*item);
567 out_strides.push(a.stride(i));
568 }
569
570 out_dims.push(m);
572 out_strides.push(axes_sorted.iter().map(|&ax| a.stride(ax)).sum());
573
574 let mapping = mdarray::StridedMapping::new(Shape::from_dims(&out_dims), &out_strides);
575
576 unsafe { View::new_unchecked(a.as_ptr(), mapping) }
585}
586
587#[doc(hidden)]
591pub fn hypersum<T, L: Layout>(a: &View<'_, T, DynRank, L>, axes: &[usize]) -> Array<T, DynRank>
592where
593 T: std::iter::Sum + Copy + Zero + std::ops::AddAssign,
594{
595 let mut axes_sorted = axes.to_vec();
596 axes_sorted.sort_unstable();
597 axes_sorted.dedup();
598
599 let dims = a.shape().dims();
600 let rank = dims.len();
601
602 for &ax in &axes_sorted {
603 assert!(ax < rank, "axis ({ax}) out of bounds for rank {rank}");
604 }
605
606 let out_dims: Vec<usize> = (0..rank)
607 .filter(|i| axes_sorted.binary_search(i).is_err())
608 .map(|i| dims[i])
609 .collect();
610
611 let mut out = Array::from_elem(out_dims, T::zero());
612
613 for idx in odometer(dims) {
614 let out_idx: Vec<usize> = (0..rank)
615 .filter(|i| axes_sorted.binary_search(i).is_err())
616 .map(|i| idx[i])
617 .collect();
618 out[out_idx.as_slice()] += a[idx.as_slice()];
619 }
620
621 out
622}
623
624fn odometer(dims: &[usize]) -> impl Iterator<Item = Vec<usize>> + '_ {
629 let total: usize = dims.iter().product();
630 let mut idx = vec![0usize; dims.len()];
631 let mut first = true;
632
633 (0..total).map(move |_| {
634 if first {
635 first = false;
636 } else {
637 for i in (0..dims.len()).rev() {
638 idx[i] += 1;
639 if idx[i] < dims[i] {
640 break;
641 }
642 idx[i] = 0;
643 }
644 }
645 idx.clone()
646 })
647}
648
649fn update_axis_map(axis_map: &[usize], diag_axes: &[usize], ndim_after: usize) -> Vec<usize> {
651 let removed: Vec<usize> = {
652 let mut v = diag_axes.to_vec();
653 v.sort_unstable();
654 v
655 };
656 let new_diag_pos = ndim_after - 1;
657
658 axis_map
659 .iter()
660 .map(|&cur| {
661 if diag_axes.contains(&cur) {
662 new_diag_pos
663 } else {
664 let shift = removed.iter().filter(|&&r| r < cur).count();
665 cur - shift
666 }
667 })
668 .collect()
669}
670
671fn apply_hypersum<T, L: Layout>(
676 view: &View<'_, T, DynRank, L>,
677 idx: &[usize],
678 axis_map: &mut Vec<usize>,
679) -> Array<T, DynRank>
680where
681 T: Copy + Zero + std::iter::Sum + std::ops::AddAssign,
682{
683 let cur_axes: Vec<usize> = idx.iter().map(|&a| axis_map[a]).collect();
685
686 if cur_axes.len() == 1 {
687 let ax = cur_axes[0];
688 let result = hypersum(view, &[ax]);
689 for cur in axis_map.iter_mut() {
690 if *cur > ax {
691 *cur -= 1;
692 }
693 }
694 result
695 } else {
696 let diag = hyperdiagonal(view.clone(), &cur_axes);
697
698 let diag_ax = diag.shape().dims().len() - 1;
699 *axis_map = update_axis_map(axis_map, &cur_axes, diag.shape().dims().len());
700
701 let result = hypersum(&diag.into_dyn(), &[diag_ax]);
702 for cur in axis_map.iter_mut() {
703 if *cur > diag_ax {
704 *cur -= 1;
705 }
706 }
707 result
708 }
709}
710
711fn extract_hyperdiag<T, L: Layout>(
718 view: View<'_, T, DynRank, L>,
719 idx: &[usize],
720 axis_map: &mut Vec<usize>,
721) -> (Array<T, DynRank>, usize)
722where
723 T: Copy,
724{
725 let cur_axes: Vec<usize> = idx.iter().map(|&a| axis_map[a]).collect();
727
728 if cur_axes.len() == 1 {
729 let ax = cur_axes[0];
731 (view.to_owned().into(), ax)
732 } else {
733 let diag = hyperdiagonal(view, &cur_axes);
734 let diag_ax = diag.shape().dims().len() - 1;
735 *axis_map = update_axis_map(axis_map, &cur_axes, diag.shape().dims().len());
736 (diag.to_owned().into(), diag_ax)
737 }
738}
739
740fn axes_to_hyperedges(
741 axes_a: &[usize],
742 axes_b: &[usize],
743) -> Vec<(Option<Vec<usize>>, Option<Vec<usize>>)> {
744 let mut remap: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
745 let mut edges: Vec<(Option<Vec<usize>>, Option<Vec<usize>>)> = Vec::new();
746
747 for (axis, &label) in axes_a.iter().enumerate() {
748 if label == FREE_AXIS {
749 continue;
750 }
751
752 let edge = *remap.entry(label).or_insert_with(|| {
753 edges.push((None, None));
754 edges.len() - 1
755 });
756 edges[edge].0.get_or_insert_with(Vec::new).push(axis);
757 }
758
759 for (axis, &label) in axes_b.iter().enumerate() {
760 if label == FREE_AXIS {
761 continue;
762 }
763
764 let edge = *remap.entry(label).or_insert_with(|| {
765 edges.push((None, None));
766 edges.len() - 1
767 });
768 edges[edge].1.get_or_insert_with(Vec::new).push(axis);
769 }
770
771 edges
772}
773
774#[doc(hidden)]
775pub fn einsum_to_contract_axes(
776 indices_a: &[u8],
777 indices_b: &[u8],
778 indices_c: &[u8],
779) -> (Vec<usize>, Vec<usize>) {
780 let free: std::collections::HashSet<u8> = indices_c.iter().copied().collect();
781
782 let axes_a = indices_a
783 .iter()
784 .map(|&label| {
785 if free.contains(&label) {
786 FREE_AXIS
787 } else {
788 label as usize
789 }
790 })
791 .collect();
792
793 let axes_b = indices_b
794 .iter()
795 .map(|&label| {
796 if free.contains(&label) {
797 FREE_AXIS
798 } else {
799 label as usize
800 }
801 })
802 .collect();
803
804 (axes_a, axes_b)
805}