Skip to main content

strided_einsum2/
lib.rs

1//! Binary Einstein summation on strided views.
2//!
3//! Provides `einsum2_into` for computing binary tensor contractions with
4//! accumulation semantics: `C = alpha * A * B + beta * C`.
5//!
6//! # Example
7//!
8//! ```
9//! use strided_view::StridedArray;
10//! use strided_einsum2::einsum2_into;
11//!
12//! // Matrix multiply: C_ik = A_ij * B_jk
13//! let a = StridedArray::<f64>::from_fn_row_major(&[2, 3], |idx| (idx[0] * 3 + idx[1] + 1) as f64);
14//! let b = StridedArray::<f64>::from_fn_row_major(&[3, 2], |idx| (idx[0] * 2 + idx[1] + 1) as f64);
15//! let mut c = StridedArray::<f64>::row_major(&[2, 2]);
16//!
17//! einsum2_into(
18//!     c.view_mut(), &a.view(), &b.view(),
19//!     &['i', 'k'], &['i', 'j'], &['j', 'k'],
20//!     1.0, 0.0,
21//! ).unwrap();
22//! ```
23
24#[cfg(all(feature = "blas", feature = "blas-inject"))]
25compile_error!("Features `blas` and `blas-inject` are mutually exclusive.");
26
27#[cfg(any(
28    all(feature = "blas-accelerate", feature = "blas-openblas"),
29    all(feature = "blas-accelerate", feature = "blas-mkl"),
30    all(feature = "blas-openblas", feature = "blas-mkl")
31))]
32compile_error!("Select at most one explicit BLAS provider feature.");
33
34#[cfg(all(feature = "blas-inject", not(feature = "blas")))]
35extern crate cblas_inject as cblas_sys;
36#[cfg(all(feature = "blas", not(feature = "blas-inject")))]
37extern crate cblas_sys;
38
39#[cfg(any(
40    all(feature = "blas", not(feature = "blas-inject")),
41    all(feature = "blas-inject", not(feature = "blas"))
42))]
43pub mod bgemm_blas;
44
45#[cfg(feature = "faer")]
46/// Batched GEMM backend using the [`faer`] library.
47pub mod bgemm_faer;
48/// Batched GEMM fallback using explicit loops.
49pub mod bgemm_naive;
50/// GEMM-ready operand types and preparation functions for contiguous data.
51pub mod contiguous;
52/// Axis-based general dot product API.
53pub mod dot_general;
54/// Contraction planning: axis classification and permutation computation.
55pub mod plan;
56/// Trace-axis reduction (summing axes that appear only in one operand).
57pub mod trace;
58/// Shared helpers (permutation inversion, multi-index iteration, dimension fusion).
59pub mod util;
60
61/// Backend abstraction for batched GEMM dispatch.
62pub mod backend;
63
64use std::any::TypeId;
65use std::fmt::Debug;
66use std::hash::Hash;
67
68use strided_kernel::zip_map2_into;
69#[cfg(any(feature = "faer", feature = "blas", feature = "blas-inject"))]
70use strided_view::StridedArray;
71use strided_view::{Adjoint, Conj, ElementOp, ElementOpApply};
72
73pub use strided_traits::ScalarBase;
74pub use strided_view::{col_major_strides, StridedView, StridedViewMut};
75
76pub use backend::Backend;
77pub use dot_general::{dot_general_into, dot_general_with_backend_into, DotGeneralConfig};
78pub use plan::Einsum2Plan;
79
80/// Trait alias for axis label types.
81pub trait AxisId: Clone + Eq + Hash + Debug {}
82impl<T: Clone + Eq + Hash + Debug> AxisId for T {}
83
84// ScalarBase is re-exported from strided_traits (see above).
85// It no longer requires ElementOpApply, enabling custom scalar types
86// (e.g., tropical semiring) to work with Identity-only views.
87
88/// Trait alias for element types supported by einsum operations.
89///
90/// When BLAS is enabled, `Scalar` follows the active backend and requires
91/// `BlasGemm`, even if faer is also compiled for explicit backend use.
92#[cfg(any(
93    all(feature = "blas", not(feature = "blas-inject")),
94    all(feature = "blas-inject", not(feature = "blas"))
95))]
96pub trait Scalar: ScalarBase + ElementOpApply + bgemm_blas::BlasGemm {}
97
98#[cfg(any(
99    all(feature = "blas", not(feature = "blas-inject")),
100    all(feature = "blas-inject", not(feature = "blas"))
101))]
102impl<T> Scalar for T where T: ScalarBase + ElementOpApply + bgemm_blas::BlasGemm {}
103
104/// Trait alias for element types supported by the faer active backend.
105///
106/// When only faer is enabled, this additionally requires `faer::ComplexField`
107/// so that the faer GEMM backend can be used.
108#[cfg(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject"))))]
109pub trait Scalar: ScalarBase + ElementOpApply + faer_traits::ComplexField {}
110
111#[cfg(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject"))))]
112impl<T> Scalar for T where T: ScalarBase + ElementOpApply + faer_traits::ComplexField {}
113
114/// Trait alias for element types (without `faer` or BLAS features).
115#[cfg(not(any(feature = "faer", feature = "blas", feature = "blas-inject")))]
116pub trait Scalar: ScalarBase + ElementOpApply {}
117
118#[cfg(not(any(feature = "faer", feature = "blas", feature = "blas-inject")))]
119impl<T> Scalar for T where T: ScalarBase + ElementOpApply {}
120
121/// Placeholder trait definition for invalid mutually-exclusive feature combinations.
122///
123/// The crate emits `compile_error!` above for these combinations. This trait only
124/// avoids cascading type-resolution errors so users see the intended diagnostics.
125#[cfg(all(feature = "blas", feature = "blas-inject"))]
126pub trait Scalar: ScalarBase + ElementOpApply {}
127
128#[cfg(all(feature = "blas", feature = "blas-inject"))]
129impl<T> Scalar for T where T: ScalarBase + ElementOpApply {}
130
131/// Errors specific to einsum operations.
132#[derive(Debug, thiserror::Error)]
133pub enum EinsumError {
134    #[error("duplicate axis label: {0}")]
135    DuplicateAxis(String),
136    #[error("output axis {0} not found in any input")]
137    OrphanOutputAxis(String),
138    #[error("dimension mismatch for axis {axis:?}: {dim_a} vs {dim_b}")]
139    DimensionMismatch {
140        axis: String,
141        dim_a: usize,
142        dim_b: usize,
143    },
144    #[error("invalid dot-general config: {0}")]
145    InvalidDotGeneralConfig(String),
146    #[error("output shape mismatch: expected {expected:?}, got {got:?}")]
147    OutputShapeMismatch {
148        expected: Vec<usize>,
149        got: Vec<usize>,
150    },
151    #[error(transparent)]
152    Strided(#[from] strided_view::StridedError),
153}
154
155/// Convenience alias for `Result<T, EinsumError>`.
156pub type Result<T> = std::result::Result<T, EinsumError>;
157
158/// Returns `true` if the given `ElementOp` type represents conjugation.
159///
160/// - `Identity` / `Transpose` → `false` (no per-element conjugation)
161/// - `Conj` / `Adjoint` → `true` (per-element conjugation needed)
162///
163/// For scalar types, `Transpose::apply(x) = x` (identity) and the dimension
164/// swap is already reflected in the view's strides/dims.  Similarly,
165/// `Adjoint::apply(x) = x.conj()` with the dimension swap in the view.
166fn op_is_conj<Op: 'static>() -> bool {
167    TypeId::of::<Op>() == TypeId::of::<Conj>() || TypeId::of::<Op>() == TypeId::of::<Adjoint>()
168}
169
170/// Binary einsum contraction: `C = alpha * contract(A, B) + beta * C`.
171///
172/// `ic`, `ia`, `ib` are axis labels for C, A, B respectively.
173/// Axes are classified as:
174/// - **batch**: in A, B, and C
175/// - **lo** (left-output): in A and C, not B
176/// - **ro** (right-output): in B and C, not A
177/// - **sum** (contraction): in A and B, not C
178///
179/// Trace axes (only in A or only in B) are detected and reduced lazily.
180pub fn einsum2_into<T: Scalar, OpA, OpB, ID: AxisId>(
181    c: StridedViewMut<T>,
182    a: &StridedView<T, OpA>,
183    b: &StridedView<T, OpB>,
184    ic: &[ID],
185    ia: &[ID],
186    ib: &[ID],
187    alpha: T,
188    beta: T,
189) -> Result<()>
190where
191    OpA: ElementOp<T> + 'static,
192    OpB: ElementOp<T> + 'static,
193{
194    // 1. Build plan
195    let plan = Einsum2Plan::new(ia, ib, ic)?;
196
197    // 2. Validate dimension consistency across operands
198    validate_dimensions::<ID>(&plan, a.dims(), b.dims(), c.dims(), ia, ib, ic)?;
199
200    // 3. Reduce trace axes if present; determine conjugation flags.
201    //    When trace reduction occurs, Op is already applied during the reduction,
202    //    so conj flag is false. Otherwise, we strip the Op and pass a conj flag
203    //    to the GEMM kernel (avoiding materialization).
204    let left_trace = trace::find_trace_indices(ia, ib, ic);
205    let (a_buf, conj_a) = if !left_trace.is_empty() {
206        (Some(trace::reduce_trace_axes(a, &left_trace)?), false)
207    } else {
208        (None, op_is_conj::<OpA>())
209    };
210
211    let a_view: StridedView<T> = match a_buf.as_ref() {
212        Some(buf) => buf.view(),
213        None => StridedView::new(a.data(), a.dims(), a.strides(), a.offset())
214            .expect("strip_op_view: metadata already validated"),
215    };
216
217    let right_trace = trace::find_trace_indices(ib, ia, ic);
218    let (b_buf, conj_b) = if !right_trace.is_empty() {
219        (Some(trace::reduce_trace_axes(b, &right_trace)?), false)
220    } else {
221        (None, op_is_conj::<OpB>())
222    };
223
224    let b_view: StridedView<T> = match b_buf.as_ref() {
225        Some(buf) => buf.view(),
226        None => StridedView::new(b.data(), b.dims(), b.strides(), b.offset())
227            .expect("strip_op_view: metadata already validated"),
228    };
229
230    // 4. Dispatch to GEMM
231    #[cfg(any(feature = "faer", feature = "blas", feature = "blas-inject"))]
232    {
233        let conj_fn = make_conj_fn::<T>();
234        einsum2_dispatch::<T, backend::ActiveBackend, _>(
235            c, &a_view, &b_view, &plan, alpha, beta, conj_a, conj_b, conj_fn,
236        )?;
237    }
238
239    #[cfg(not(any(feature = "faer", feature = "blas", feature = "blas-inject")))]
240    {
241        let a_perm = a_view.permute(&plan.left_perm)?;
242        let b_perm = b_view.permute(&plan.right_perm)?;
243        let mut c_perm = c.permute(&plan.c_to_internal_perm)?;
244
245        if plan.sum.is_empty() && plan.lo.is_empty() && plan.ro.is_empty() && beta == T::zero() {
246            let mul_fn = move |a_val: T, b_val: T| -> T {
247                let a_c = if conj_a { Conj::apply(a_val) } else { a_val };
248                let b_c = if conj_b { Conj::apply(b_val) } else { b_val };
249                alpha * a_c * b_c
250            };
251            zip_map2_into(&mut c_perm, &a_perm, &b_perm, mul_fn)?;
252            return Ok(());
253        }
254
255        bgemm_naive::bgemm_strided_into(
256            &mut c_perm,
257            &a_perm,
258            &b_perm,
259            plan.batch.len(),
260            plan.lo.len(),
261            plan.ro.len(),
262            plan.sum.len(),
263            alpha,
264            beta,
265            conj_a,
266            conj_b,
267        )?;
268    }
269
270    Ok(())
271}
272
273/// Binary einsum for custom scalar types using naive GEMM.
274///
275/// Like [`einsum2_into`] but works with any `ScalarBase` type (no `ElementOpApply` required).
276/// Uses closures `map_a` and `map_b` for per-element transformation instead of
277/// conjugation flags. Always dispatches to the naive GEMM kernel.
278///
279/// Views must use `Identity` element operations (the default).
280pub fn einsum2_naive_into<T, ID, MapA, MapB>(
281    c: StridedViewMut<T>,
282    a: &StridedView<T>,
283    b: &StridedView<T>,
284    ic: &[ID],
285    ia: &[ID],
286    ib: &[ID],
287    alpha: T,
288    beta: T,
289    map_a: MapA,
290    map_b: MapB,
291) -> Result<()>
292where
293    T: ScalarBase,
294    ID: AxisId,
295    MapA: Fn(T) -> T + strided_kernel::MaybeSync,
296    MapB: Fn(T) -> T + strided_kernel::MaybeSync,
297{
298    let plan = Einsum2Plan::new(ia, ib, ic)?;
299    validate_dimensions::<ID>(&plan, a.dims(), b.dims(), c.dims(), ia, ib, ic)?;
300
301    // Reduce trace axes if present.
302    // When trace reduction occurs, map is applied via map_into before reduction,
303    // so we use identity map for GEMM. Otherwise, pass through the original map.
304    let left_trace = trace::find_trace_indices(ia, ib, ic);
305    let (a_buf, use_map_a) = if !left_trace.is_empty() {
306        let mut mapped = unsafe { strided_view::StridedArray::<T>::col_major_uninit(a.dims()) };
307        strided_kernel::map_into(&mut mapped.view_mut(), a, &map_a)?;
308        let reduced = trace::reduce_trace_axes(&mapped.view(), &left_trace)?;
309        (Some(reduced), false)
310    } else {
311        (None, true)
312    };
313    let a_view: StridedView<T> = match a_buf.as_ref() {
314        Some(buf) => buf.view(),
315        None => a.clone(),
316    };
317
318    let right_trace = trace::find_trace_indices(ib, ia, ic);
319    let (b_buf, use_map_b) = if !right_trace.is_empty() {
320        let mut mapped = unsafe { strided_view::StridedArray::<T>::col_major_uninit(b.dims()) };
321        strided_kernel::map_into(&mut mapped.view_mut(), b, &map_b)?;
322        let reduced = trace::reduce_trace_axes(&mapped.view(), &right_trace)?;
323        (Some(reduced), false)
324    } else {
325        (None, true)
326    };
327    let b_view: StridedView<T> = match b_buf.as_ref() {
328        Some(buf) => buf.view(),
329        None => b.clone(),
330    };
331
332    let a_perm = a_view.permute(&plan.left_perm)?;
333    let b_perm = b_view.permute(&plan.right_perm)?;
334    let mut c_perm = c.permute(&plan.c_to_internal_perm)?;
335
336    // Element-wise fast path
337    if plan.sum.is_empty() && plan.lo.is_empty() && plan.ro.is_empty() && beta == T::zero() {
338        let mul_fn = move |a_val: T, b_val: T| -> T {
339            let a_c = if use_map_a { map_a(a_val) } else { a_val };
340            let b_c = if use_map_b { map_b(b_val) } else { b_val };
341            alpha * a_c * b_c
342        };
343        zip_map2_into(&mut c_perm, &a_perm, &b_perm, mul_fn)?;
344        return Ok(());
345    }
346
347    let final_map_a: Box<dyn Fn(T) -> T> = if use_map_a {
348        Box::new(map_a)
349    } else {
350        Box::new(|x| x)
351    };
352    let final_map_b: Box<dyn Fn(T) -> T> = if use_map_b {
353        Box::new(map_b)
354    } else {
355        Box::new(|x| x)
356    };
357
358    bgemm_naive::bgemm_strided_into_with_map(
359        &mut c_perm,
360        &a_perm,
361        &b_perm,
362        plan.batch.len(),
363        plan.lo.len(),
364        plan.ro.len(),
365        plan.sum.len(),
366        alpha,
367        beta,
368        final_map_a,
369        final_map_b,
370    )?;
371
372    Ok(())
373}
374
375/// Binary einsum with a pluggable GEMM backend.
376///
377/// Like [`einsum2_into`] but works with any `ScalarBase` type and dispatches
378/// to the caller-provided GEMM backend `B`. Views must use `Identity` element
379/// operations (the default).
380///
381/// External crates can implement [`Backend`] for custom scalar types
382/// (e.g., tropical semiring) and pass the backend here.
383pub fn einsum2_with_backend_into<T, B, ID>(
384    c: StridedViewMut<T>,
385    a: &StridedView<T>,
386    b: &StridedView<T>,
387    ic: &[ID],
388    ia: &[ID],
389    ib: &[ID],
390    alpha: T,
391    beta: T,
392) -> Result<()>
393where
394    T: ScalarBase,
395    B: Backend<T>,
396    ID: AxisId,
397{
398    let plan = Einsum2Plan::new(ia, ib, ic)?;
399    validate_dimensions::<ID>(&plan, a.dims(), b.dims(), c.dims(), ia, ib, ic)?;
400
401    // Trace reduction (plain sum, Identity views)
402    let left_trace = trace::find_trace_indices(ia, ib, ic);
403    let a_buf = if !left_trace.is_empty() {
404        Some(trace::reduce_trace_axes(a, &left_trace)?)
405    } else {
406        None
407    };
408    let a_view: StridedView<T> = match a_buf.as_ref() {
409        Some(buf) => buf.view(),
410        None => a.clone(),
411    };
412
413    let right_trace = trace::find_trace_indices(ib, ia, ic);
414    let b_buf = if !right_trace.is_empty() {
415        Some(trace::reduce_trace_axes(b, &right_trace)?)
416    } else {
417        None
418    };
419    let b_view: StridedView<T> = match b_buf.as_ref() {
420        Some(buf) => buf.view(),
421        None => b.clone(),
422    };
423
424    // No conjugation for generic backend path
425    einsum2_dispatch::<T, B, _>(c, &a_view, &b_view, &plan, alpha, beta, false, false, None)
426}
427
428/// Build the conj materialization function pointer for the active backend.
429///
430/// When the backend requires conj to be materialized into data (e.g. CBLAS),
431/// returns `Some(conj_apply)`. Otherwise returns `None` (backend handles conj
432/// via transpose flags or similar).
433#[cfg(any(feature = "faer", feature = "blas", feature = "blas-inject"))]
434fn make_conj_fn<T: Scalar>() -> Option<fn(T) -> T> {
435    if <backend::ActiveBackend as Backend<T>>::MATERIALIZES_CONJ {
436        Some(|x| Conj::apply(x))
437    } else {
438        None
439    }
440}
441
442fn scale_or_zero_strided_mut<T: ScalarBase>(c: &mut StridedViewMut<T>, beta: T) {
443    if c.is_empty() {
444        return;
445    }
446
447    let dims = c.dims().to_vec();
448    let strides = c.strides().to_vec();
449    let ptr = c.as_mut_ptr();
450    let zero = T::zero();
451
452    fn visit<T: ScalarBase>(
453        ptr: *mut T,
454        dims: &[usize],
455        strides: &[isize],
456        axis: usize,
457        offset: isize,
458        beta: T,
459        zero: T,
460    ) {
461        if axis == dims.len() {
462            unsafe {
463                let dst = ptr.offset(offset);
464                if beta == zero {
465                    *dst = zero;
466                } else {
467                    *dst = beta * *dst;
468                }
469            }
470            return;
471        }
472
473        for i in 0..dims[axis] {
474            visit(
475                ptr,
476                dims,
477                strides,
478                axis + 1,
479                offset + i as isize * strides[axis],
480                beta,
481                zero,
482            );
483        }
484    }
485
486    visit(ptr, &dims, &strides, 0, 0, beta, zero);
487}
488
489/// Internal GEMM dispatch, generic over backend.
490///
491/// Called after trace reduction with plain Identity views. Handles:
492/// 1. Permutation to canonical order
493/// 2. Element-wise fast path (if applicable)
494/// 3. Contiguous preparation via `prepare_input_view`
495/// 4. GEMM via `B::bgemm_contiguous_into`
496/// 5. Finalize (copy-back if needed)
497///
498/// `conj_fn` is the materialization function for backends that need conj
499/// applied to data before GEMM. Pass `None` when conj_a/conj_b are both false
500/// or when the backend handles conj natively (via flags).
501pub(crate) fn einsum2_dispatch<T, B, ID>(
502    c: StridedViewMut<T>,
503    a: &StridedView<T>,
504    b: &StridedView<T>,
505    plan: &Einsum2Plan<ID>,
506    alpha: T,
507    beta: T,
508    conj_a: bool,
509    conj_b: bool,
510    conj_fn: Option<fn(T) -> T>,
511) -> Result<()>
512where
513    T: ScalarBase,
514    B: Backend<T>,
515    ID: AxisId,
516{
517    // 1. Permute to canonical order
518    let a_perm = a.permute(&plan.left_perm)?;
519    let b_perm = b.permute(&plan.right_perm)?;
520    let mut c_perm = c.permute(&plan.c_to_internal_perm)?;
521
522    if c_perm.is_empty() {
523        return Ok(());
524    }
525
526    // 2. Fast path: element-wise (all batch, no contraction)
527    if plan.sum.is_empty() && plan.lo.is_empty() && plan.ro.is_empty() && beta == T::zero() {
528        if !conj_a && !conj_b && alpha == T::one() {
529            zip_map2_into(&mut c_perm, &a_perm, &b_perm, |a_val, b_val| a_val * b_val)?;
530        } else if !conj_a && !conj_b {
531            let mul_fn = move |a_val: T, b_val: T| -> T { alpha * a_val * b_val };
532            zip_map2_into(&mut c_perm, &a_perm, &b_perm, mul_fn)?;
533        } else {
534            let conj_fn = conj_fn.unwrap_or(|x| x);
535            let mul_fn = move |a_val: T, b_val: T| -> T {
536                let a_c = if conj_a { conj_fn(a_val) } else { a_val };
537                let b_c = if conj_b { conj_fn(b_val) } else { b_val };
538                alpha * a_c * b_c
539            };
540            zip_map2_into(&mut c_perm, &a_perm, &b_perm, mul_fn)?;
541        }
542        return Ok(());
543    }
544
545    // 3. Prepare contiguous operands
546    let n_lo = plan.lo.len();
547    let n_ro = plan.ro.len();
548    let n_sum = plan.sum.len();
549    let use_pool = true;
550    let materialize = if B::MATERIALIZES_CONJ { conj_fn } else { None };
551
552    let a_op = contiguous::prepare_input_view(
553        &a_perm,
554        n_lo,
555        n_sum,
556        conj_a,
557        B::REQUIRES_UNIT_STRIDE,
558        use_pool,
559        materialize,
560    )?;
561    let b_op = contiguous::prepare_input_view(
562        &b_perm,
563        n_sum,
564        n_ro,
565        conj_b,
566        B::REQUIRES_UNIT_STRIDE,
567        use_pool,
568        materialize,
569    )?;
570    let mut c_op = contiguous::prepare_output_view(
571        &mut c_perm,
572        n_lo,
573        n_ro,
574        beta,
575        B::REQUIRES_UNIT_STRIDE,
576        use_pool,
577    )?;
578
579    // Compute fused dimension sizes
580    let lo_dims = &a_perm.dims()[..n_lo];
581    let sum_dims = &a_perm.dims()[n_lo..n_lo + n_sum];
582    let batch_dims = &a_perm.dims()[n_lo + n_sum..];
583    let ro_dims = &b_perm.dims()[n_sum..n_sum + n_ro];
584    if sum_dims.iter().any(|&dim| dim == 0) {
585        scale_or_zero_strided_mut(&mut c_perm, beta);
586        return Ok(());
587    }
588    let m: usize = lo_dims.iter().product::<usize>().max(1);
589    let k: usize = sum_dims.iter().product::<usize>().max(1);
590    let n: usize = ro_dims.iter().product::<usize>().max(1);
591
592    // 4. GEMM — dispatched through trait
593    B::bgemm_contiguous_into(&mut c_op, &a_op, &b_op, batch_dims, m, n, k, alpha, beta)?;
594
595    // 5. Finalize
596    c_op.finalize_into(&mut c_perm)?;
597
598    Ok(())
599}
600
601/// Binary einsum accepting owned inputs for zero-copy optimization.
602///
603/// Same semantics as [`einsum2_into`] but accepts owned `StridedArray` inputs.
604/// When inputs have non-contiguous strides after permutation, ownership
605/// transfer avoids allocating separate buffers. For contiguous inputs,
606/// the behavior is identical.
607///
608/// `conj_a` and `conj_b` indicate whether to conjugate elements of A/B.
609#[cfg(any(feature = "faer", feature = "blas", feature = "blas-inject"))]
610pub fn einsum2_into_owned<T: Scalar, ID: AxisId>(
611    c: StridedViewMut<T>,
612    a: StridedArray<T>,
613    b: StridedArray<T>,
614    ic: &[ID],
615    ia: &[ID],
616    ib: &[ID],
617    alpha: T,
618    beta: T,
619    conj_a: bool,
620    conj_b: bool,
621) -> Result<()>
622where
623    backend::ActiveBackend: Backend<T>,
624{
625    // 1. Build plan
626    let plan = Einsum2Plan::new(ia, ib, ic)?;
627
628    // 2. Validate dimensions
629    validate_dimensions::<ID>(&plan, a.dims(), b.dims(), c.dims(), ia, ib, ic)?;
630
631    // 3. Trace reduction: reduce trace axes if present.
632    //    When trace reduction occurs, conjugation is applied during reduction,
633    //    so the conj flag becomes false. Otherwise keep the caller's flag.
634    let left_trace = trace::find_trace_indices(ia, ib, ic);
635    let (a_for_gemm, conj_a_final) = if !left_trace.is_empty() {
636        (trace::reduce_trace_axes(&a.view(), &left_trace)?, false)
637    } else {
638        (a, conj_a)
639    };
640
641    let right_trace = trace::find_trace_indices(ib, ia, ic);
642    let (b_for_gemm, conj_b_final) = if !right_trace.is_empty() {
643        (trace::reduce_trace_axes(&b.view(), &right_trace)?, false)
644    } else {
645        (b, conj_b)
646    };
647
648    // 4. Permute to canonical order (metadata-only on owned arrays)
649    let a_perm = a_for_gemm.permuted(&plan.left_perm)?;
650    let b_perm = b_for_gemm.permuted(&plan.right_perm)?;
651    let mut c_perm = c.permute(&plan.c_to_internal_perm)?;
652
653    let n_lo = plan.lo.len();
654    let n_ro = plan.ro.len();
655    let n_sum = plan.sum.len();
656
657    // 5. Fast path: element-wise (all batch, no contraction)
658    if plan.sum.is_empty() && plan.lo.is_empty() && plan.ro.is_empty() && beta == T::zero() {
659        let mul_fn = move |a_val: T, b_val: T| -> T {
660            let a_c = if conj_a_final {
661                Conj::apply(a_val)
662            } else {
663                a_val
664            };
665            let b_c = if conj_b_final {
666                Conj::apply(b_val)
667            } else {
668                b_val
669            };
670            alpha * a_c * b_c
671        };
672        zip_map2_into(&mut c_perm, &a_perm.view(), &b_perm.view(), mul_fn)?;
673        return Ok(());
674    }
675
676    // 6. Extract dimension sizes BEFORE consuming arrays via prepare_input_owned
677    let a_dims_perm = a_perm.dims().to_vec();
678    let b_dims_perm = b_perm.dims().to_vec();
679
680    let lo_dims = &a_dims_perm[..n_lo];
681    let sum_dims = &a_dims_perm[n_lo..n_lo + n_sum];
682    let batch_dims = a_dims_perm[n_lo + n_sum..].to_vec();
683    let ro_dims = &b_dims_perm[n_sum..n_sum + n_ro];
684    let m: usize = lo_dims.iter().product::<usize>().max(1);
685    let k: usize = sum_dims.iter().product::<usize>().max(1);
686    let n: usize = ro_dims.iter().product::<usize>().max(1);
687
688    // 7. Prepare contiguous operands (owned path -- avoids extra copies)
689    let conj_fn = make_conj_fn::<T>();
690    let materialize = if <backend::ActiveBackend as Backend<T>>::MATERIALIZES_CONJ {
691        conj_fn
692    } else {
693        None
694    };
695    let use_pool = true;
696    let unit_stride = <backend::ActiveBackend as Backend<T>>::REQUIRES_UNIT_STRIDE;
697    let a_op = contiguous::prepare_input_owned(
698        a_perm,
699        n_lo,
700        n_sum,
701        conj_a_final,
702        unit_stride,
703        use_pool,
704        materialize,
705    )?;
706    let b_op = contiguous::prepare_input_owned(
707        b_perm,
708        n_sum,
709        n_ro,
710        conj_b_final,
711        unit_stride,
712        use_pool,
713        materialize,
714    )?;
715    let mut c_op =
716        contiguous::prepare_output_view(&mut c_perm, n_lo, n_ro, beta, unit_stride, use_pool)?;
717
718    // 8. GEMM — dispatched through trait
719    backend::ActiveBackend::bgemm_contiguous_into(
720        &mut c_op,
721        &a_op,
722        &b_op,
723        &batch_dims,
724        m,
725        n,
726        k,
727        alpha,
728        beta,
729    )?;
730
731    // 9. Finalize
732    c_op.finalize_into(&mut c_perm)?;
733
734    Ok(())
735}
736
737/// Validate that dimensions match across operands for each axis group.
738fn validate_dimensions<ID: AxisId>(
739    plan: &Einsum2Plan<ID>,
740    a_dims: &[usize],
741    b_dims: &[usize],
742    c_dims: &[usize],
743    ia: &[ID],
744    ib: &[ID],
745    ic: &[ID],
746) -> Result<()> {
747    let find_dim = |labels: &[ID], dims: &[usize], id: &ID| -> usize {
748        labels
749            .iter()
750            .position(|x| x == id)
751            .map(|i| dims[i])
752            .unwrap()
753    };
754
755    // Batch: must match in A, B, and C
756    for id in &plan.batch {
757        let da = find_dim(ia, a_dims, id);
758        let db = find_dim(ib, b_dims, id);
759        let dc = find_dim(ic, c_dims, id);
760        if da != db || da != dc {
761            return Err(EinsumError::DimensionMismatch {
762                axis: format!("{:?}", id),
763                dim_a: da,
764                dim_b: db,
765            });
766        }
767    }
768
769    // Sum: must match in A and B
770    for id in &plan.sum {
771        let da = find_dim(ia, a_dims, id);
772        let db = find_dim(ib, b_dims, id);
773        if da != db {
774            return Err(EinsumError::DimensionMismatch {
775                axis: format!("{:?}", id),
776                dim_a: da,
777                dim_b: db,
778            });
779        }
780    }
781
782    // LO: must match in A and C
783    for id in &plan.lo {
784        let da = find_dim(ia, a_dims, id);
785        let dc = find_dim(ic, c_dims, id);
786        if da != dc {
787            return Err(EinsumError::DimensionMismatch {
788                axis: format!("{:?}", id),
789                dim_a: da,
790                dim_b: dc,
791            });
792        }
793    }
794
795    // RO: must match in B and C
796    for id in &plan.ro {
797        let db = find_dim(ib, b_dims, id);
798        let dc = find_dim(ic, c_dims, id);
799        if db != dc {
800            return Err(EinsumError::DimensionMismatch {
801                axis: format!("{:?}", id),
802                dim_a: db,
803                dim_b: dc,
804            });
805        }
806    }
807
808    Ok(())
809}
810
811#[cfg(test)]
812mod tests {
813    use super::*;
814    use strided_view::StridedArray;
815
816    #[test]
817    fn test_matmul_ij_jk_ik() {
818        // C_ik = A_ij * B_jk
819        let a = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
820            [[1.0, 2.0], [3.0, 4.0]][idx[0]][idx[1]]
821        });
822        let b = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
823            [[5.0, 6.0], [7.0, 8.0]][idx[0]][idx[1]]
824        });
825        let mut c = StridedArray::<f64>::row_major(&[2, 2]);
826
827        einsum2_into(
828            c.view_mut(),
829            &a.view(),
830            &b.view(),
831            &['i', 'k'],
832            &['i', 'j'],
833            &['j', 'k'],
834            1.0,
835            0.0,
836        )
837        .unwrap();
838
839        assert_eq!(c.get(&[0, 0]), 19.0);
840        assert_eq!(c.get(&[0, 1]), 22.0);
841        assert_eq!(c.get(&[1, 0]), 43.0);
842        assert_eq!(c.get(&[1, 1]), 50.0);
843    }
844
845    #[test]
846    fn test_matmul_rect() {
847        // A: 2x3, B: 3x4, C: 2x4
848        let a =
849            StridedArray::<f64>::from_fn_row_major(&[2, 3], |idx| (idx[0] * 3 + idx[1] + 1) as f64);
850        let b =
851            StridedArray::<f64>::from_fn_row_major(&[3, 4], |idx| (idx[0] * 4 + idx[1] + 1) as f64);
852        let mut c = StridedArray::<f64>::row_major(&[2, 4]);
853
854        einsum2_into(
855            c.view_mut(),
856            &a.view(),
857            &b.view(),
858            &['i', 'k'],
859            &['i', 'j'],
860            &['j', 'k'],
861            1.0,
862            0.0,
863        )
864        .unwrap();
865
866        // A = [[1,2,3],[4,5,6]], B = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
867        assert_eq!(c.get(&[0, 0]), 38.0);
868        assert_eq!(c.get(&[1, 3]), 128.0);
869    }
870
871    #[test]
872    fn test_batched_matmul() {
873        // C_bik = A_bij * B_bjk
874        let a = StridedArray::<f64>::from_fn_row_major(&[2, 2, 3], |idx| {
875            (idx[0] * 6 + idx[1] * 3 + idx[2] + 1) as f64
876        });
877        let b = StridedArray::<f64>::from_fn_row_major(&[2, 3, 2], |idx| {
878            (idx[0] * 6 + idx[1] * 2 + idx[2] + 1) as f64
879        });
880        let mut c = StridedArray::<f64>::row_major(&[2, 2, 2]);
881
882        einsum2_into(
883            c.view_mut(),
884            &a.view(),
885            &b.view(),
886            &['b', 'i', 'k'],
887            &['b', 'i', 'j'],
888            &['b', 'j', 'k'],
889            1.0,
890            0.0,
891        )
892        .unwrap();
893
894        // Batch 0: A0=[[1,2,3],[4,5,6]], B0=[[1,2],[3,4],[5,6]]
895        // C0[0,0] = 1*1+2*3+3*5 = 22
896        assert_eq!(c.get(&[0, 0, 0]), 22.0);
897    }
898
899    #[test]
900    fn test_batched_matmul_col_major_output() {
901        // C_bik = A_bij * B_bjk with col-major output (same layout as opteinsum)
902        let a_data = vec![1.0, 0.0, 0.0, 1.0, 2.0, 0.0, 0.0, 2.0];
903        let b_data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
904        let a = StridedArray::<f64>::from_parts(a_data, &[2, 2, 2], &[4, 2, 1], 0).unwrap();
905        let b = StridedArray::<f64>::from_parts(b_data, &[2, 2, 2], &[4, 2, 1], 0).unwrap();
906        let mut c = StridedArray::<f64>::col_major(&[2, 2, 2]);
907
908        einsum2_into(
909            c.view_mut(),
910            &a.view(),
911            &b.view(),
912            &['b', 'i', 'k'],
913            &['b', 'i', 'j'],
914            &['b', 'j', 'k'],
915            1.0,
916            0.0,
917        )
918        .unwrap();
919
920        // batch 0: I * [[1,2],[3,4]] = [[1,2],[3,4]]
921        assert_eq!(c.get(&[0, 0, 0]), 1.0);
922        assert_eq!(c.get(&[0, 0, 1]), 2.0);
923        assert_eq!(c.get(&[0, 1, 0]), 3.0);
924        assert_eq!(c.get(&[0, 1, 1]), 4.0);
925        // batch 1: 2I * [[5,6],[7,8]] = [[10,12],[14,16]]
926        assert_eq!(c.get(&[1, 0, 0]), 10.0);
927        assert_eq!(c.get(&[1, 1, 1]), 16.0);
928    }
929
930    #[test]
931    fn test_outer_product() {
932        // C_ij = A_i * B_j
933        let a = StridedArray::<f64>::from_fn_row_major(&[3], |idx| (idx[0] + 1) as f64);
934        let b = StridedArray::<f64>::from_fn_row_major(&[4], |idx| (idx[0] + 1) as f64);
935        let mut c = StridedArray::<f64>::row_major(&[3, 4]);
936
937        einsum2_into(
938            c.view_mut(),
939            &a.view(),
940            &b.view(),
941            &['i', 'j'],
942            &['i'],
943            &['j'],
944            1.0,
945            0.0,
946        )
947        .unwrap();
948
949        assert_eq!(c.get(&[0, 0]), 1.0);
950        assert_eq!(c.get(&[2, 3]), 12.0);
951    }
952
953    #[test]
954    fn test_dot_product() {
955        // C = A_i * B_i (scalar output)
956        let a = StridedArray::<f64>::from_fn_row_major(&[3], |idx| (idx[0] + 1) as f64);
957        let b = StridedArray::<f64>::from_fn_row_major(&[3], |idx| (idx[0] + 1) as f64);
958        let mut c = StridedArray::<f64>::row_major(&[]);
959
960        einsum2_into(
961            c.view_mut(),
962            &a.view(),
963            &b.view(),
964            &[] as &[char],
965            &['i'],
966            &['i'],
967            1.0,
968            0.0,
969        )
970        .unwrap();
971
972        // 1*1 + 2*2 + 3*3 = 14
973        assert_eq!(c.get(&[]), 14.0);
974    }
975
976    #[test]
977    fn test_alpha_beta() {
978        // C = 2*A*B + 3*C_old
979        let a = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
980            [[1.0, 0.0], [0.0, 1.0]][idx[0]][idx[1]] // identity
981        });
982        let b = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
983            [[1.0, 2.0], [3.0, 4.0]][idx[0]][idx[1]]
984        });
985        let mut c = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
986            [[10.0, 20.0], [30.0, 40.0]][idx[0]][idx[1]]
987        });
988
989        einsum2_into(
990            c.view_mut(),
991            &a.view(),
992            &b.view(),
993            &['i', 'k'],
994            &['i', 'j'],
995            &['j', 'k'],
996            2.0,
997            3.0,
998        )
999        .unwrap();
1000
1001        // C = 2*I*B + 3*C_old
1002        assert_eq!(c.get(&[0, 0]), 32.0); // 2*1 + 3*10
1003        assert_eq!(c.get(&[1, 1]), 128.0); // 2*4 + 3*40
1004    }
1005
1006    #[test]
1007    fn test_transposed_output() {
1008        // C_ki = A_ij * B_jk (output transposed)
1009        let a = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
1010            [[1.0, 2.0], [3.0, 4.0]][idx[0]][idx[1]]
1011        });
1012        let b = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
1013            [[5.0, 6.0], [7.0, 8.0]][idx[0]][idx[1]]
1014        });
1015        let mut c = StridedArray::<f64>::row_major(&[2, 2]);
1016
1017        einsum2_into(
1018            c.view_mut(),
1019            &a.view(),
1020            &b.view(),
1021            &['k', 'i'], // C indexed as (k, i) instead of (i, k)
1022            &['i', 'j'],
1023            &['j', 'k'],
1024            1.0,
1025            0.0,
1026        )
1027        .unwrap();
1028
1029        // Normal matmul result: C_ik = [[19,22],[43,50]]
1030        // But C is indexed as (k, i), so C[k, i] = (A*B)[i, k]
1031        assert_eq!(c.get(&[0, 0]), 19.0); // C[k=0, i=0]
1032        assert_eq!(c.get(&[0, 1]), 43.0); // C[k=0, i=1]
1033        assert_eq!(c.get(&[1, 0]), 22.0); // C[k=1, i=0]
1034        assert_eq!(c.get(&[1, 1]), 50.0); // C[k=1, i=1]
1035    }
1036
1037    #[test]
1038    fn test_left_trace() {
1039        // C_k = sum_j (sum_i A_ij) * B_jk
1040        // left_trace=[i], sum=[j], ro=[k]
1041        let a =
1042            StridedArray::<f64>::from_fn_row_major(&[2, 3], |idx| (idx[0] * 3 + idx[1] + 1) as f64);
1043        // A = [[1,2,3],[4,5,6]]
1044        // sum over i: [5, 7, 9]
1045        let b =
1046            StridedArray::<f64>::from_fn_row_major(&[3, 2], |idx| (idx[0] * 2 + idx[1] + 1) as f64);
1047        // B = [[1,2],[3,4],[5,6]]
1048        let mut c = StridedArray::<f64>::row_major(&[2]);
1049
1050        einsum2_into(
1051            c.view_mut(),
1052            &a.view(),
1053            &b.view(),
1054            &['k'],
1055            &['i', 'j'],
1056            &['j', 'k'],
1057            1.0,
1058            0.0,
1059        )
1060        .unwrap();
1061
1062        // C_k = sum_j [5,7,9][j] * B[j,k]
1063        // C[0] = 5*1 + 7*3 + 9*5 = 5 + 21 + 45 = 71
1064        // C[1] = 5*2 + 7*4 + 9*6 = 10 + 28 + 54 = 92
1065        assert_eq!(c.get(&[0]), 71.0);
1066        assert_eq!(c.get(&[1]), 92.0);
1067    }
1068
1069    #[test]
1070    fn test_u32_labels() {
1071        // Same as matmul but with u32 labels
1072        let a = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
1073            [[1.0, 2.0], [3.0, 4.0]][idx[0]][idx[1]]
1074        });
1075        let b = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
1076            [[5.0, 6.0], [7.0, 8.0]][idx[0]][idx[1]]
1077        });
1078        let mut c = StridedArray::<f64>::row_major(&[2, 2]);
1079
1080        einsum2_into(
1081            c.view_mut(),
1082            &a.view(),
1083            &b.view(),
1084            &[0u32, 2],
1085            &[0u32, 1],
1086            &[1u32, 2],
1087            1.0,
1088            0.0,
1089        )
1090        .unwrap();
1091
1092        assert_eq!(c.get(&[0, 0]), 19.0);
1093        assert_eq!(c.get(&[1, 1]), 50.0);
1094    }
1095
1096    #[test]
1097    fn test_complex_matmul() {
1098        use num_complex::Complex64;
1099        let i = Complex64::i();
1100
1101        // A = [[1+i, 2], [3, 4-i]]
1102        let a_vals = [
1103            [1.0 + i, Complex64::new(2.0, 0.0)],
1104            [Complex64::new(3.0, 0.0), 4.0 - i],
1105        ];
1106        let a = StridedArray::<Complex64>::from_fn_row_major(&[2, 2], |idx| a_vals[idx[0]][idx[1]]);
1107
1108        // B = [[1, i], [0, 1]]
1109        let b_vals = [
1110            [Complex64::new(1.0, 0.0), i],
1111            [Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)],
1112        ];
1113        let b = StridedArray::<Complex64>::from_fn_row_major(&[2, 2], |idx| b_vals[idx[0]][idx[1]]);
1114
1115        let mut c = StridedArray::<Complex64>::row_major(&[2, 2]);
1116
1117        einsum2_into(
1118            c.view_mut(),
1119            &a.view(),
1120            &b.view(),
1121            &['i', 'k'],
1122            &['i', 'j'],
1123            &['j', 'k'],
1124            Complex64::new(1.0, 0.0),
1125            Complex64::new(0.0, 0.0),
1126        )
1127        .unwrap();
1128
1129        // C = A * B
1130        // C[0,0] = (1+i)*1 + 2*0 = 1+i
1131        // C[0,1] = (1+i)*i + 2*1 = i+i²+2 = i-1+2 = 1+i
1132        // C[1,0] = 3*1 + (4-i)*0 = 3
1133        // C[1,1] = 3*i + (4-i)*1 = 3i+4-i = 4+2i
1134        assert_eq!(c.get(&[0, 0]), 1.0 + i);
1135        assert_eq!(c.get(&[0, 1]), 1.0 + i);
1136        assert_eq!(c.get(&[1, 0]), Complex64::new(3.0, 0.0));
1137        assert_eq!(c.get(&[1, 1]), 4.0 + 2.0 * i);
1138    }
1139
1140    #[test]
1141    fn test_complex_matmul_with_conj() {
1142        use num_complex::Complex64;
1143        let i = Complex64::i();
1144
1145        // A = [[1+i, 2i], [3, 4-i]]
1146        let a_vals = [[1.0 + i, 2.0 * i], [Complex64::new(3.0, 0.0), 4.0 - i]];
1147        let a = StridedArray::<Complex64>::from_fn_row_major(&[2, 2], |idx| a_vals[idx[0]][idx[1]]);
1148
1149        // B = identity
1150        let b = StridedArray::<Complex64>::from_fn_row_major(&[2, 2], |idx| {
1151            if idx[0] == idx[1] {
1152                Complex64::new(1.0, 0.0)
1153            } else {
1154                Complex64::new(0.0, 0.0)
1155            }
1156        });
1157
1158        let mut c = StridedArray::<Complex64>::row_major(&[2, 2]);
1159
1160        // C = conj(A) * B = conj(A)
1161        let a_conj = a.view().conj();
1162        einsum2_into(
1163            c.view_mut(),
1164            &a_conj,
1165            &b.view(),
1166            &['i', 'k'],
1167            &['i', 'j'],
1168            &['j', 'k'],
1169            Complex64::new(1.0, 0.0),
1170            Complex64::new(0.0, 0.0),
1171        )
1172        .unwrap();
1173
1174        // conj(A) = [[1-i, -2i], [3, 4+i]]
1175        assert_eq!(c.get(&[0, 0]), 1.0 - i);
1176        assert_eq!(c.get(&[0, 1]), -2.0 * i);
1177        assert_eq!(c.get(&[1, 0]), Complex64::new(3.0, 0.0));
1178        assert_eq!(c.get(&[1, 1]), 4.0 + i);
1179    }
1180
1181    #[test]
1182    fn test_complex_matmul_with_conj_both() {
1183        use num_complex::Complex64;
1184        let i = Complex64::i();
1185
1186        // A = [[1+i, 0], [0, 2-i]]
1187        let a_vals = [
1188            [1.0 + i, Complex64::new(0.0, 0.0)],
1189            [Complex64::new(0.0, 0.0), 2.0 - i],
1190        ];
1191        let a = StridedArray::<Complex64>::from_fn_row_major(&[2, 2], |idx| a_vals[idx[0]][idx[1]]);
1192
1193        // B = [[1, i], [0, 1+i]]
1194        let b_vals = [
1195            [Complex64::new(1.0, 0.0), i],
1196            [Complex64::new(0.0, 0.0), 1.0 + i],
1197        ];
1198        let b = StridedArray::<Complex64>::from_fn_row_major(&[2, 2], |idx| b_vals[idx[0]][idx[1]]);
1199
1200        let mut c = StridedArray::<Complex64>::row_major(&[2, 2]);
1201
1202        // C = conj(A) * conj(B)
1203        let a_conj = a.view().conj();
1204        let b_conj = b.view().conj();
1205        einsum2_into(
1206            c.view_mut(),
1207            &a_conj,
1208            &b_conj,
1209            &['i', 'k'],
1210            &['i', 'j'],
1211            &['j', 'k'],
1212            Complex64::new(1.0, 0.0),
1213            Complex64::new(0.0, 0.0),
1214        )
1215        .unwrap();
1216
1217        // conj(A) = [[1-i, 0], [0, 2+i]]
1218        // conj(B) = [[1, -i], [0, 1-i]]
1219        // C = conj(A) * conj(B)
1220        // C[0,0] = (1-i)*1 + 0*0 = 1-i
1221        // C[0,1] = (1-i)*(-i) + 0*(1-i) = -i+i² = -i-1 = -(1+i)
1222        // C[1,0] = 0*1 + (2+i)*0 = 0
1223        // C[1,1] = 0*(-i) + (2+i)*(1-i) = 2-2i+i-i² = 2-i+1 = 3-i
1224        assert_eq!(c.get(&[0, 0]), 1.0 - i);
1225        assert_eq!(c.get(&[0, 1]), -(1.0 + i));
1226        assert_eq!(c.get(&[1, 0]), Complex64::new(0.0, 0.0));
1227        assert_eq!(c.get(&[1, 1]), 3.0 - i);
1228    }
1229
1230    #[test]
1231    fn test_elementwise_hadamard() {
1232        // C_ijk = A_ijk * B_ijk — all batch, no contraction
1233        let a = StridedArray::<f64>::from_fn_row_major(&[3, 4, 5], |idx| {
1234            (idx[0] * 20 + idx[1] * 5 + idx[2] + 1) as f64
1235        });
1236        let b = StridedArray::<f64>::from_fn_row_major(&[3, 4, 5], |idx| {
1237            (idx[0] * 20 + idx[1] * 5 + idx[2] + 1) as f64 * 0.1
1238        });
1239        let mut c = StridedArray::<f64>::row_major(&[3, 4, 5]);
1240
1241        einsum2_into(
1242            c.view_mut(),
1243            &a.view(),
1244            &b.view(),
1245            &['i', 'j', 'k'],
1246            &['i', 'j', 'k'],
1247            &['i', 'j', 'k'],
1248            1.0,
1249            0.0,
1250        )
1251        .unwrap();
1252
1253        // Spot check: C[0,0,0] = 1 * 0.1 = 0.1
1254        assert!((c.get(&[0, 0, 0]) - 0.1).abs() < 1e-12);
1255        // C[2,3,4] = 60 * 6.0 = 360
1256        assert!((c.get(&[2, 3, 4]) - 360.0).abs() < 1e-10);
1257    }
1258
1259    #[test]
1260    fn test_elementwise_hadamard_with_alpha() {
1261        let a =
1262            StridedArray::<f64>::from_fn_row_major(&[2, 3], |idx| (idx[0] * 3 + idx[1] + 1) as f64);
1263        let b =
1264            StridedArray::<f64>::from_fn_row_major(&[2, 3], |idx| (idx[0] * 3 + idx[1] + 1) as f64);
1265        let mut c = StridedArray::<f64>::row_major(&[2, 3]);
1266
1267        einsum2_into(
1268            c.view_mut(),
1269            &a.view(),
1270            &b.view(),
1271            &['i', 'j'],
1272            &['i', 'j'],
1273            &['i', 'j'],
1274            2.0,
1275            0.0,
1276        )
1277        .unwrap();
1278
1279        // C[0,0] = 2.0 * 1 * 1 = 2.0
1280        assert_eq!(c.get(&[0, 0]), 2.0);
1281        // C[1,2] = 2.0 * 6 * 6 = 72.0
1282        assert_eq!(c.get(&[1, 2]), 72.0);
1283    }
1284
1285    #[cfg(any(feature = "faer", feature = "blas", feature = "blas-inject"))]
1286    #[test]
1287    fn test_einsum2_owned_matmul() {
1288        let a = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
1289            [[1.0, 2.0], [3.0, 4.0]][idx[0]][idx[1]]
1290        });
1291        let b = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
1292            [[5.0, 6.0], [7.0, 8.0]][idx[0]][idx[1]]
1293        });
1294        let mut c = StridedArray::<f64>::row_major(&[2, 2]);
1295
1296        einsum2_into_owned(
1297            c.view_mut(),
1298            a,
1299            b,
1300            &['i', 'k'],
1301            &['i', 'j'],
1302            &['j', 'k'],
1303            1.0,
1304            0.0,
1305            false,
1306            false,
1307        )
1308        .unwrap();
1309
1310        assert_eq!(c.get(&[0, 0]), 19.0);
1311        assert_eq!(c.get(&[0, 1]), 22.0);
1312        assert_eq!(c.get(&[1, 0]), 43.0);
1313        assert_eq!(c.get(&[1, 1]), 50.0);
1314    }
1315
1316    #[cfg(any(feature = "faer", feature = "blas", feature = "blas-inject"))]
1317    #[test]
1318    fn test_einsum2_owned_batched() {
1319        let a = StridedArray::<f64>::from_fn_row_major(&[2, 2, 3], |idx| {
1320            (idx[0] * 6 + idx[1] * 3 + idx[2] + 1) as f64
1321        });
1322        let b = StridedArray::<f64>::from_fn_row_major(&[2, 3, 2], |idx| {
1323            (idx[0] * 6 + idx[1] * 2 + idx[2] + 1) as f64
1324        });
1325        let mut c = StridedArray::<f64>::row_major(&[2, 2, 2]);
1326
1327        einsum2_into_owned(
1328            c.view_mut(),
1329            a,
1330            b,
1331            &['b', 'i', 'k'],
1332            &['b', 'i', 'j'],
1333            &['b', 'j', 'k'],
1334            1.0,
1335            0.0,
1336            false,
1337            false,
1338        )
1339        .unwrap();
1340
1341        // Batch 0: A0=[[1,2,3],[4,5,6]], B0=[[1,2],[3,4],[5,6]]
1342        // C0[0,0] = 1*1+2*3+3*5 = 22
1343        assert_eq!(c.get(&[0, 0, 0]), 22.0);
1344    }
1345
1346    #[cfg(any(feature = "faer", feature = "blas", feature = "blas-inject"))]
1347    #[test]
1348    fn test_einsum2_owned_alpha_beta() {
1349        let a = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
1350            [[1.0, 0.0], [0.0, 1.0]][idx[0]][idx[1]]
1351        });
1352        let b = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
1353            [[1.0, 2.0], [3.0, 4.0]][idx[0]][idx[1]]
1354        });
1355        let mut c = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
1356            [[10.0, 20.0], [30.0, 40.0]][idx[0]][idx[1]]
1357        });
1358
1359        einsum2_into_owned(
1360            c.view_mut(),
1361            a,
1362            b,
1363            &['i', 'k'],
1364            &['i', 'j'],
1365            &['j', 'k'],
1366            2.0,
1367            3.0,
1368            false,
1369            false,
1370        )
1371        .unwrap();
1372
1373        // C = 2*I*B + 3*C_old
1374        assert_eq!(c.get(&[0, 0]), 32.0); // 2*1 + 3*10
1375        assert_eq!(c.get(&[1, 1]), 128.0); // 2*4 + 3*40
1376    }
1377
1378    #[cfg(any(feature = "faer", feature = "blas", feature = "blas-inject"))]
1379    #[test]
1380    fn test_einsum2_owned_elementwise() {
1381        // All batch, no contraction -- element-wise fast path
1382        let a =
1383            StridedArray::<f64>::from_fn_row_major(&[2, 3], |idx| (idx[0] * 3 + idx[1] + 1) as f64);
1384        let b =
1385            StridedArray::<f64>::from_fn_row_major(&[2, 3], |idx| (idx[0] * 3 + idx[1] + 1) as f64);
1386        let mut c = StridedArray::<f64>::row_major(&[2, 3]);
1387
1388        einsum2_into_owned(
1389            c.view_mut(),
1390            a,
1391            b,
1392            &['i', 'j'],
1393            &['i', 'j'],
1394            &['i', 'j'],
1395            2.0,
1396            0.0,
1397            false,
1398            false,
1399        )
1400        .unwrap();
1401
1402        assert_eq!(c.get(&[0, 0]), 2.0); // 2 * 1 * 1
1403        assert_eq!(c.get(&[1, 2]), 72.0); // 2 * 6 * 6
1404    }
1405
1406    #[cfg(any(feature = "faer", feature = "blas", feature = "blas-inject"))]
1407    #[test]
1408    fn test_einsum2_owned_left_trace() {
1409        // C_k = sum_j (sum_i A_ij) * B_jk
1410        // left_trace=[i], sum=[j], ro=[k]
1411        let a =
1412            StridedArray::<f64>::from_fn_row_major(&[2, 3], |idx| (idx[0] * 3 + idx[1] + 1) as f64);
1413        // A = [[1,2,3],[4,5,6]]
1414        // sum over i: [5, 7, 9]
1415        let b =
1416            StridedArray::<f64>::from_fn_row_major(&[3, 2], |idx| (idx[0] * 2 + idx[1] + 1) as f64);
1417        // B = [[1,2],[3,4],[5,6]]
1418        let mut c = StridedArray::<f64>::row_major(&[2]);
1419
1420        einsum2_into_owned(
1421            c.view_mut(),
1422            a,
1423            b,
1424            &['k'],
1425            &['i', 'j'],
1426            &['j', 'k'],
1427            1.0,
1428            0.0,
1429            false,
1430            false,
1431        )
1432        .unwrap();
1433
1434        // C[0] = 5*1 + 7*3 + 9*5 = 5 + 21 + 45 = 71
1435        // C[1] = 5*2 + 7*4 + 9*6 = 10 + 28 + 54 = 92
1436        assert_eq!(c.get(&[0]), 71.0);
1437        assert_eq!(c.get(&[1]), 92.0);
1438    }
1439
1440    #[test]
1441    fn test_einsum2_naive_matmul() {
1442        // einsum2_naive_into with identity maps = regular matmul
1443        let a = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
1444            [[1.0, 2.0], [3.0, 4.0]][idx[0]][idx[1]]
1445        });
1446        let b = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
1447            [[5.0, 6.0], [7.0, 8.0]][idx[0]][idx[1]]
1448        });
1449        let mut c = StridedArray::<f64>::row_major(&[2, 2]);
1450
1451        einsum2_naive_into(
1452            c.view_mut(),
1453            &a.view(),
1454            &b.view(),
1455            &['i', 'k'],
1456            &['i', 'j'],
1457            &['j', 'k'],
1458            1.0,
1459            0.0,
1460            |x| x,
1461            |x| x,
1462        )
1463        .unwrap();
1464
1465        assert_eq!(c.get(&[0, 0]), 19.0);
1466        assert_eq!(c.get(&[0, 1]), 22.0);
1467        assert_eq!(c.get(&[1, 0]), 43.0);
1468        assert_eq!(c.get(&[1, 1]), 50.0);
1469    }
1470
1471    #[test]
1472    fn test_einsum2_naive_elementwise() {
1473        // Element-wise (all batch) with identity maps
1474        let a =
1475            StridedArray::<f64>::from_fn_row_major(&[2, 3], |idx| (idx[0] * 3 + idx[1] + 1) as f64);
1476        let b =
1477            StridedArray::<f64>::from_fn_row_major(&[2, 3], |idx| (idx[0] * 3 + idx[1] + 1) as f64);
1478        let mut c = StridedArray::<f64>::row_major(&[2, 3]);
1479
1480        einsum2_naive_into(
1481            c.view_mut(),
1482            &a.view(),
1483            &b.view(),
1484            &['i', 'j'],
1485            &['i', 'j'],
1486            &['i', 'j'],
1487            2.0,
1488            0.0,
1489            |x| x,
1490            |x| x,
1491        )
1492        .unwrap();
1493
1494        assert_eq!(c.get(&[0, 0]), 2.0); // 2 * 1 * 1
1495        assert_eq!(c.get(&[1, 2]), 72.0); // 2 * 6 * 6
1496    }
1497
1498    #[test]
1499    fn test_einsum2_naive_custom_type() {
1500        // Custom scalar type that does NOT implement ElementOpApply.
1501        // This demonstrates that einsum2_naive_into works with custom types.
1502        use num_traits::{One, Zero};
1503
1504        #[derive(Debug, Clone, Copy, PartialEq)]
1505        struct MyVal(f64);
1506
1507        impl Default for MyVal {
1508            fn default() -> Self {
1509                MyVal(0.0)
1510            }
1511        }
1512
1513        impl std::ops::Add for MyVal {
1514            type Output = Self;
1515            fn add(self, rhs: Self) -> Self {
1516                MyVal(self.0 + rhs.0)
1517            }
1518        }
1519
1520        impl std::ops::Mul for MyVal {
1521            type Output = Self;
1522            fn mul(self, rhs: Self) -> Self {
1523                MyVal(self.0 * rhs.0)
1524            }
1525        }
1526
1527        impl Zero for MyVal {
1528            fn zero() -> Self {
1529                MyVal(0.0)
1530            }
1531            fn is_zero(&self) -> bool {
1532                self.0 == 0.0
1533            }
1534        }
1535
1536        impl One for MyVal {
1537            fn one() -> Self {
1538                MyVal(1.0)
1539            }
1540        }
1541
1542        // C_ik = A_ij * B_jk (matrix multiply with custom type)
1543        let a = StridedArray::from_parts(
1544            vec![MyVal(1.0), MyVal(2.0), MyVal(3.0), MyVal(4.0)],
1545            &[2, 2],
1546            &[2, 1],
1547            0,
1548        )
1549        .unwrap();
1550        let b = StridedArray::from_parts(
1551            vec![MyVal(5.0), MyVal(6.0), MyVal(7.0), MyVal(8.0)],
1552            &[2, 2],
1553            &[2, 1],
1554            0,
1555        )
1556        .unwrap();
1557        let mut c = StridedArray::<MyVal>::col_major(&[2, 2]);
1558
1559        einsum2_naive_into(
1560            c.view_mut(),
1561            &a.view(),
1562            &b.view(),
1563            &['i', 'k'],
1564            &['i', 'j'],
1565            &['j', 'k'],
1566            MyVal(1.0),
1567            MyVal(0.0),
1568            |x| x,
1569            |x| x,
1570        )
1571        .unwrap();
1572
1573        // C = [[1*5+2*7, 1*6+2*8], [3*5+4*7, 3*6+4*8]]
1574        //   = [[19, 22], [43, 50]]
1575        assert_eq!(c.get(&[0, 0]), MyVal(19.0));
1576        assert_eq!(c.get(&[0, 1]), MyVal(22.0));
1577        assert_eq!(c.get(&[1, 0]), MyVal(43.0));
1578        assert_eq!(c.get(&[1, 1]), MyVal(50.0));
1579    }
1580
1581    #[test]
1582    fn test_einsum2_naive_left_trace() {
1583        // C_k = sum_j (sum_i A_ij) * B_jk with map_a = identity
1584        let a =
1585            StridedArray::<f64>::from_fn_row_major(&[2, 3], |idx| (idx[0] * 3 + idx[1] + 1) as f64);
1586        let b =
1587            StridedArray::<f64>::from_fn_row_major(&[3, 2], |idx| (idx[0] * 2 + idx[1] + 1) as f64);
1588        let mut c = StridedArray::<f64>::row_major(&[2]);
1589
1590        einsum2_naive_into(
1591            c.view_mut(),
1592            &a.view(),
1593            &b.view(),
1594            &['k'],
1595            &['i', 'j'],
1596            &['j', 'k'],
1597            1.0,
1598            0.0,
1599            |x| x,
1600            |x| x,
1601        )
1602        .unwrap();
1603
1604        assert_eq!(c.get(&[0]), 71.0);
1605        assert_eq!(c.get(&[1]), 92.0);
1606    }
1607
1608    /// Test backend that delegates to naive GEMM loops.
1609    struct TestNaiveBackend;
1610
1611    impl Backend<f64> for TestNaiveBackend {
1612        const MATERIALIZES_CONJ: bool = false;
1613        const REQUIRES_UNIT_STRIDE: bool = false;
1614
1615        fn bgemm_contiguous_into(
1616            c: &mut contiguous::ContiguousOperandMut<f64>,
1617            a: &contiguous::ContiguousOperand<f64>,
1618            b: &contiguous::ContiguousOperand<f64>,
1619            batch_dims: &[usize],
1620            m: usize,
1621            n: usize,
1622            k: usize,
1623            alpha: f64,
1624            beta: f64,
1625        ) -> strided_view::Result<()> {
1626            // Delegate to the contiguous naive GEMM loop
1627            let a_ptr = a.ptr();
1628            let b_ptr = b.ptr();
1629            let c_ptr = c.ptr();
1630            let a_rs = a.row_stride();
1631            let a_cs = a.col_stride();
1632            let b_rs = b.row_stride();
1633            let b_cs = b.col_stride();
1634            let c_rs = c.row_stride();
1635            let c_cs = c.col_stride();
1636
1637            let mut batch_idx = crate::util::MultiIndex::new(batch_dims);
1638            while batch_idx.next().is_some() {
1639                let a_base = batch_idx.offset(a.batch_strides());
1640                let b_base = batch_idx.offset(b.batch_strides());
1641                let c_base = batch_idx.offset(c.batch_strides());
1642
1643                for i in 0..m {
1644                    for j in 0..n {
1645                        let mut acc = 0.0f64;
1646                        for l in 0..k {
1647                            let a_val = unsafe {
1648                                *a_ptr.offset(a_base + i as isize * a_rs + l as isize * a_cs)
1649                            };
1650                            let b_val = unsafe {
1651                                *b_ptr.offset(b_base + l as isize * b_rs + j as isize * b_cs)
1652                            };
1653                            acc += a_val * b_val;
1654                        }
1655                        unsafe {
1656                            let c_elem =
1657                                c_ptr.offset(c_base + i as isize * c_rs + j as isize * c_cs);
1658                            if beta == 0.0 {
1659                                *c_elem = alpha * acc;
1660                            } else {
1661                                *c_elem = alpha * acc + beta * (*c_elem);
1662                            }
1663                        }
1664                    }
1665                }
1666            }
1667            Ok(())
1668        }
1669    }
1670
1671    #[test]
1672    fn test_einsum2_with_backend_matmul() {
1673        let a = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
1674            [[1.0, 2.0], [3.0, 4.0]][idx[0]][idx[1]]
1675        });
1676        let b = StridedArray::<f64>::from_fn_row_major(&[2, 2], |idx| {
1677            [[5.0, 6.0], [7.0, 8.0]][idx[0]][idx[1]]
1678        });
1679        let mut c = StridedArray::<f64>::row_major(&[2, 2]);
1680
1681        einsum2_with_backend_into::<_, TestNaiveBackend, _>(
1682            c.view_mut(),
1683            &a.view(),
1684            &b.view(),
1685            &['i', 'k'],
1686            &['i', 'j'],
1687            &['j', 'k'],
1688            1.0,
1689            0.0,
1690        )
1691        .unwrap();
1692
1693        assert_eq!(c.get(&[0, 0]), 19.0);
1694        assert_eq!(c.get(&[0, 1]), 22.0);
1695        assert_eq!(c.get(&[1, 0]), 43.0);
1696        assert_eq!(c.get(&[1, 1]), 50.0);
1697    }
1698
1699    #[test]
1700    fn test_einsum2_with_backend_elementwise() {
1701        let a =
1702            StridedArray::<f64>::from_fn_row_major(&[2, 3], |idx| (idx[0] * 3 + idx[1] + 1) as f64);
1703        let b =
1704            StridedArray::<f64>::from_fn_row_major(&[2, 3], |idx| (idx[0] * 3 + idx[1] + 1) as f64);
1705        let mut c = StridedArray::<f64>::row_major(&[2, 3]);
1706
1707        einsum2_with_backend_into::<_, TestNaiveBackend, _>(
1708            c.view_mut(),
1709            &a.view(),
1710            &b.view(),
1711            &['i', 'j'],
1712            &['i', 'j'],
1713            &['i', 'j'],
1714            2.0,
1715            0.0,
1716        )
1717        .unwrap();
1718
1719        assert_eq!(c.get(&[0, 0]), 2.0); // 2 * 1 * 1
1720        assert_eq!(c.get(&[1, 2]), 72.0); // 2 * 6 * 6
1721    }
1722
1723    #[test]
1724    fn test_einsum2_with_backend_custom_type() {
1725        use num_traits::{One, Zero};
1726
1727        #[derive(Debug, Clone, Copy, PartialEq)]
1728        struct Tropical(f64);
1729
1730        impl Default for Tropical {
1731            fn default() -> Self {
1732                Tropical(0.0)
1733            }
1734        }
1735
1736        impl std::ops::Add for Tropical {
1737            type Output = Self;
1738            fn add(self, rhs: Self) -> Self {
1739                Tropical(self.0 + rhs.0)
1740            }
1741        }
1742
1743        impl std::ops::Mul for Tropical {
1744            type Output = Self;
1745            fn mul(self, rhs: Self) -> Self {
1746                Tropical(self.0 * rhs.0)
1747            }
1748        }
1749
1750        impl Zero for Tropical {
1751            fn zero() -> Self {
1752                Tropical(0.0)
1753            }
1754            fn is_zero(&self) -> bool {
1755                self.0 == 0.0
1756            }
1757        }
1758
1759        impl One for Tropical {
1760            fn one() -> Self {
1761                Tropical(1.0)
1762            }
1763        }
1764
1765        struct TropicalBackend;
1766
1767        impl Backend<Tropical> for TropicalBackend {
1768            const MATERIALIZES_CONJ: bool = false;
1769            const REQUIRES_UNIT_STRIDE: bool = false;
1770
1771            fn bgemm_contiguous_into(
1772                c: &mut contiguous::ContiguousOperandMut<Tropical>,
1773                a: &contiguous::ContiguousOperand<Tropical>,
1774                b: &contiguous::ContiguousOperand<Tropical>,
1775                batch_dims: &[usize],
1776                m: usize,
1777                n: usize,
1778                k: usize,
1779                alpha: Tropical,
1780                beta: Tropical,
1781            ) -> strided_view::Result<()> {
1782                // Simple naive GEMM for Tropical type
1783                let a_ptr = a.ptr();
1784                let b_ptr = b.ptr();
1785                let c_ptr = c.ptr();
1786                let a_rs = a.row_stride();
1787                let a_cs = a.col_stride();
1788                let b_rs = b.row_stride();
1789                let b_cs = b.col_stride();
1790                let c_rs = c.row_stride();
1791                let c_cs = c.col_stride();
1792
1793                let mut batch_idx = crate::util::MultiIndex::new(batch_dims);
1794                while batch_idx.next().is_some() {
1795                    let a_base = batch_idx.offset(a.batch_strides());
1796                    let b_base = batch_idx.offset(b.batch_strides());
1797                    let c_base = batch_idx.offset(c.batch_strides());
1798
1799                    for i in 0..m {
1800                        for j in 0..n {
1801                            let mut acc = Tropical::zero();
1802                            for l in 0..k {
1803                                let a_val = unsafe {
1804                                    *a_ptr.offset(a_base + i as isize * a_rs + l as isize * a_cs)
1805                                };
1806                                let b_val = unsafe {
1807                                    *b_ptr.offset(b_base + l as isize * b_rs + j as isize * b_cs)
1808                                };
1809                                acc = acc + a_val * b_val;
1810                            }
1811                            unsafe {
1812                                let c_elem =
1813                                    c_ptr.offset(c_base + i as isize * c_rs + j as isize * c_cs);
1814                                if beta == Tropical::zero() {
1815                                    *c_elem = alpha * acc;
1816                                } else {
1817                                    *c_elem = alpha * acc + beta * (*c_elem);
1818                                }
1819                            }
1820                        }
1821                    }
1822                }
1823                Ok(())
1824            }
1825        }
1826
1827        let a = StridedArray::from_parts(
1828            vec![Tropical(1.0), Tropical(2.0), Tropical(3.0), Tropical(4.0)],
1829            &[2, 2],
1830            &[2, 1],
1831            0,
1832        )
1833        .unwrap();
1834        let b = StridedArray::from_parts(
1835            vec![Tropical(5.0), Tropical(6.0), Tropical(7.0), Tropical(8.0)],
1836            &[2, 2],
1837            &[2, 1],
1838            0,
1839        )
1840        .unwrap();
1841        let mut c = StridedArray::<Tropical>::col_major(&[2, 2]);
1842
1843        einsum2_with_backend_into::<_, TropicalBackend, _>(
1844            c.view_mut(),
1845            &a.view(),
1846            &b.view(),
1847            &['i', 'k'],
1848            &['i', 'j'],
1849            &['j', 'k'],
1850            Tropical(1.0),
1851            Tropical(0.0),
1852        )
1853        .unwrap();
1854
1855        // C = [[1*5+2*7, 1*6+2*8], [3*5+4*7, 3*6+4*8]]
1856        //   = [[19, 22], [43, 50]]
1857        assert_eq!(c.get(&[0, 0]), Tropical(19.0));
1858        assert_eq!(c.get(&[0, 1]), Tropical(22.0));
1859        assert_eq!(c.get(&[1, 0]), Tropical(43.0));
1860        assert_eq!(c.get(&[1, 1]), Tropical(50.0));
1861    }
1862}