Skip to main content

mdarray_linalg/
contract.rs

1//! Tensor contraction and matrix multiplication
2//!
3//!```rust
4//!use mdarray::tensor;
5//!use mdarray_linalg::prelude::*;
6//!use mdarray_linalg::Naive;
7//!
8//!let a = tensor![[1., 2.], [3., 4.]];
9//!let b = tensor![[5., 6.], [7., 8.]];
10//!
11//!// Standard matrix multiplication
12//!let expected_matmul = tensor![[19., 22.], [43., 50.]];
13//!let result = Naive.matmul(&a, &b).eval();
14//!assert_eq!(result, expected_matmul);
15//!
16//!// Matrix multiplication with scalar factor
17//!let result_scaled = Naive.matmul(&a, &b).scale(2.0).eval();
18//!assert_eq!(result_scaled, expected_matmul.map(|x| x * 2.0));
19//!
20//!// Full contraction
21//!let expected_all = 70.0;
22//!let result_all = Naive.contract_all(&a, &b);
23//!assert_eq!(result_all, expected_all);
24//!
25//!// Contract last n axes of a with first n axes of b
26//!let expected_n = tensor![[19., 22.], [43., 50.]].into_dyn();
27//!let result_contract_n = Naive.contract_n(&a, &b, 1).eval();
28//!assert_eq!(result_contract_n, expected_n);
29//!
30//!// Contract specific axes (equivalent to matmul: contract axis 1 of a with axis 0 of b)
31//!let expected_pairs = tensor![[19., 22.], [43., 50.]].into_dyn();
32//!let result_specific = Naive
33//!    .contract_pairs(&a, &b, &[1], &[0])
34//!    .eval();
35//!assert_eq!(result_specific, expected_pairs);
36//!```
37
38use 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
45/// Tensor contraction and related operations
46pub trait Contract<T> {
47    /// Matrix multiplication.
48    ///
49    /// ```rust
50    /// use mdarray::tensor;
51    /// use mdarray_linalg::{Naive, prelude::*};
52    ///
53    /// let a = tensor![[1., 2.], [3., 4.]];
54    /// let b = tensor![[5., 6.], [7., 8.]];
55    /// assert_eq!(Naive.matmul(&a, &b).eval(), tensor![[19., 22.], [43., 50.]]);
56    /// ```
57    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    /// Contracts all axes of `a` with all axes of `b`.
70    ///
71    /// This is the full reduction case, i.e. a scalar result.
72    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    /// Contracts the last `n` axes of `a` with the first `n` axes of `b`.
85    ///
86    /// For matrices, `contract_n(1)` is standard matrix multiplication.
87    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    /// Contracts explicit pairs of axes.
101    ///
102    /// This is the structured contraction API.
103    /// `contract_pairs(&a, &b, &[1], &[0])` is matrix multiplication for 2D inputs.
104    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    /// Fully general contraction of two tensors, à la einsum.
119    ///
120    /// ```rust
121    /// use mdarray::array;
122    /// use mdarray_linalg::{Naive, prelude::*};
123    ///
124    /// let a = array![[1., 2.], [3., 4.]].into_dyn();
125    /// let b = array![[5., 6.], [7., 8.]].into_dyn();
126    /// let c = Naive.contract(&a, &b, &[0, 1], &[1, 2], &[0, 2]).eval();
127    /// assert_eq!(c, array![[19., 22.], [43., 50.]].into_dyn());
128    /// ```
129    ///
130    /// *New* indices in `indices_a` and `indices_b` *must* be subsequent integers starting with 0.
131    /// For example, having `[0, 1, 1, 2]` or `[0, 1, 0, 2]` for `indices_a` is OK, but `[0, 2, 2, 3]` is not.
132    /// Note that this is not limiting in any way.  Any legal einsum can be specified in this way.
133    ///
134    /// Note that this is a low-level operation.  The above restrictions allow to avoid runtime checks.
135    /// We will add a more user-friendly higher-level wrapper.
136    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
152/// Builder interface for configuring matrix-matrix operations
153pub 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    /// Multiplies the result by a scalar factor.
163    fn scale(self, factor: T) -> Self;
164
165    /// Returns a new owned tensor containing the result.
166    fn eval(self) -> Array<T, (D0, D2)>;
167
168    /// Overwrites the provided slice with the result.
169    fn write<Lc: Layout>(self, c: &mut Slice<T, (D0, D2), Lc>);
170
171    /// Adds the result to the provided slice.
172    fn add_to<Lc: Layout>(self, c: &mut Slice<T, (D0, D2), Lc>);
173
174    /// Adds the result to the provided slice after scaling the slice by `beta`
175    /// (i.e. C := beta * C + result).
176    fn add_to_scaled<Lc: Layout>(self, c: &mut Slice<T, (D0, D2), Lc>, beta: T);
177}
178
179/// Builder interface for configuring tensor contraction operations
180pub trait ContractBuilder<'a, T, Sa, Sb, La, Lb>
181where
182    T: 'a,
183    La: Layout,
184    Lb: Layout,
185{
186    /// Multiplies the result by a scalar factor.
187    fn scale(self, factor: T) -> Self;
188
189    /// Returns a new owned tensor containing the result.
190    fn eval(self) -> Array<T, DynRank>;
191
192    /// Overwrites the provided slice with the result.
193    fn write<Sc: Shape, Lc: Layout>(self, c: &mut Slice<T, Sc, Lc>);
194
195    /// Adds the result to the provided slice.
196    fn add_to<Sc: Shape, Lc: Layout>(self, c: &mut Slice<T, Sc, Lc>);
197
198    /// Adds the result to the provided slice after scaling the slice by `beta`
199    /// (i.e. C := beta * C + result).
200    fn add_to_scaled<Sc: Shape, Lc: Layout>(self, c: &mut Slice<T, Sc, Lc>, beta: T);
201}
202
203// The following exported items are unstable backend-implementation helpers.
204// They are hidden from generated documentation and may be redesigned before
205// the public API stabilizes.
206#[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/// Resolves the axis partition for a tensor contraction, avoiding
226/// allocations when axes are already provided as slices
227/// (`Axes::Specific`).
228#[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(); // TODO remove this useless copy
354
355        (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/// Helper for implementing contraction through matrix multiplication.
388/// Backends that implement `Contract` directly should call the macros
389/// `prepare_contraction!` and `finish_contraction!` themselves.
390#[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/// General contraction on labeled axes.
416///
417/// `axes_a` and `axes_b` have one entry per original axis. Equal values belong
418/// to the same contraction/diagonalization hyper-edge. `FREE_AXIS` marks axes
419/// that remain in the output.
420#[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    // Owned buffers for A and B. Allocated only when a transformation
449    // (diagonal extraction or summation) actually modifies the tensor
450    let mut a_owned: Option<Array<T, DynRank>> = None;
451    let mut b_owned: Option<Array<T, DynRank>> = None;
452
453    // map_x[i] = current position of original axis i in the transformed tensor.
454    // Initialised to the identity: no transformation has occurred yet.
455    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    // Contraction axis pairs accumulated from (Some, Some) edges,
459    // consumed in one shot by the final contraction.
460    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            // Delta attached to A only: diagonal-sum on A, no contraction with B.
468            (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            // Delta attached to B only: diagonal-sum on B, no contraction with A.
476            (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            // Delta bridges A and B: prepare one contraction axis on each side.
484            // The actual dot-product is deferred to the final contraction below.
485            (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    // Final contraction: contracts all (Some, Some) edge axes simultaneously.
513    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/// Generalized diagonal extraction along an arbitrary set of axes (zero-copy)
532#[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    // All diagonal axes must have the same size.
549    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    // Build output shape: drop all diagonal axes, ...
559    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    // ... and append the diagonal axis as the last one.
571    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    // SAFETY:
577    // - `a.as_ptr()` is valid and live for `'a` (inherited from input view).
578    // - Let upper = Σ_{i<r} (dims[i] - 1) * stride(i) be the maximum offset
579    //   reachable in `a`'s allocation. Any index over the output view is
580    //   (a_0, ..., a_{r-|axes|-1}, k) with a_j < dims[j] for j ∉ axes_set
581    //   and k < m = dims[ax] for all ax ∈ axes_set. The offset decomposes as:
582    //   Σ_{j ∉ axes_set} a_j * stride(j)  +  k * Σ_{ax ∈ axes_set} stride(ax)
583    //   Each term is bounded by (dims[j]-1)*stride(j), so the total ≤ upper.
584    unsafe { View::new_unchecked(a.as_ptr(), mapping) }
585}
586
587/// Sum a tensor over an arbitrary subset of its axes.
588/// Equivalent to a sequence of `np.sum(a, axis=s)` calls (with appropriate
589/// index renumbering after each removal), but performed in a single pass.
590#[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
624/// Row-major multi-index iterator over a tensor of the given shape.
625///
626/// Yields every index tuple (i₀, i₁, …, i_{r−1}) in lexicographic order
627/// (last axis varies fastest — C-contiguous order).
628fn 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
649/// Recompute the axis-position map after `hyperdiagonal` has been applied.
650fn 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
671/// Process a `(Some, None)` or `(None, Some)` edge: diagonal-sum on one tensor.
672///
673/// This corresponds to a Kronecker delta that is only connected to one side of
674/// the network.
675fn 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    // Translate original axis indices to their current positions.
684    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
711/// Process a `(Some, Some)` edge: prepare the contraction axis.
712///
713/// For a delta that bridges A and B, we do *not* contract immediately. Instead
714/// we reduce the multi-axis delta to a single axis (by extracting the
715/// generalized diagonal when needed) and return its current position so that
716/// `hypercontract` can pass it to the final contraction.
717fn 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    // Translate original axis indices to their current positions.
726    let cur_axes: Vec<usize> = idx.iter().map(|&a| axis_map[a]).collect();
727
728    if cur_axes.len() == 1 {
729        // Single-axis edge: no diagonal to extract, axis_map is unchanged.
730        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}