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