Skip to main content

strided_opteinsum/
expr.rs

1use std::collections::{BTreeMap, HashMap};
2#[cfg(not(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject")))))]
3use std::mem::MaybeUninit;
4
5use num_complex::Complex64;
6#[cfg(test)]
7use num_traits::Zero;
8use strided_einsum2::einsum2_into;
9#[cfg(any(feature = "faer", feature = "blas", feature = "blas-inject"))]
10use strided_einsum2::einsum2_into_owned;
11#[cfg(not(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject")))))]
12use strided_einsum2::einsum2_into_uninit;
13use strided_kernel::copy_scale;
14#[cfg(not(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject")))))]
15use strided_kernel::ExecContext;
16use strided_view::{StridedArray, StridedViewMut};
17
18use crate::operand::{EinsumOperand, EinsumScalar, StridedData};
19use crate::parse::{EinsumCode, EinsumNode};
20use crate::single_tensor::single_tensor_einsum;
21
22// ---------------------------------------------------------------------------
23// Buffer pool for intermediate reuse
24// ---------------------------------------------------------------------------
25
26/// Reusable buffer pool for intermediate tensor allocations.
27///
28/// Tracks freed `Vec<f64>` and `Vec<Complex64>` buffers indexed by length.
29/// When a contraction step completes, its input buffers are returned to the
30/// pool. Subsequent steps can reuse these buffers instead of allocating fresh.
31///
32/// Uses `BTreeMap` so `pool_acquire` can find the **smallest buffer ≥ requested
33/// size** (best-fit), improving reuse when intermediate sizes vary slightly.
34///
35/// # Usage
36///
37/// Create a pool and pass it to [`EinsumCode::evaluate_with_pool`] to reuse
38/// buffers across multiple einsum calls:
39///
40/// ```ignore
41/// let mut pool = BufferPool::new();
42/// code.evaluate_with_pool(operands1, None, Some(&mut pool))?;
43/// code.evaluate_with_pool(operands2, None, Some(&mut pool))?;
44/// ```
45///
46/// Pass `None` to let each call allocate and free independently (no pooling).
47pub struct BufferPool {
48    f64_pool: BTreeMap<usize, Vec<Vec<f64>>>,
49    c64_pool: BTreeMap<usize, Vec<Vec<Complex64>>>,
50}
51
52impl BufferPool {
53    /// Create an empty buffer pool.
54    pub fn new() -> Self {
55        Self {
56            f64_pool: BTreeMap::new(),
57            c64_pool: BTreeMap::new(),
58        }
59    }
60}
61
62// ---------------------------------------------------------------------------
63// PoolOps — private trait for type-dispatched buffer pool access
64// ---------------------------------------------------------------------------
65
66/// Private sealed trait for buffer pool acquire/release, implemented for f64
67/// and Complex64. Enables generic `eval_pair_alloc` without exposing
68/// `BufferPool` in the public API.
69trait PoolOps: EinsumScalar {
70    /// Acquire a col-major buffer from the pool (or allocate fresh).
71    ///
72    /// # Safety contract
73    /// The returned array may contain uninitialized data. Callers must write
74    /// every element before reading (e.g. via `einsum2_into` with `beta=0`).
75    #[cfg(not(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject")))))]
76    fn pool_acquire(
77        pool: &mut BufferPool,
78        dims: &[usize],
79    ) -> crate::Result<StridedArray<MaybeUninit<Self>>>;
80
81    /// Acquire initialized storage for the Faer compatibility path. Faer
82    /// cannot currently accept `MaybeUninit<T>` output; reused buffers are
83    /// already initialized, while fresh buffers are initialized once.
84    #[cfg(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject"))))]
85    fn pool_acquire_initialized(
86        pool: &mut BufferPool,
87        dims: &[usize],
88    ) -> crate::Result<StridedArray<Self>>;
89
90    /// Convert a completely overwritten array into initialized storage.
91    ///
92    /// # Safety
93    /// The caller must have received `Ok(())` from the overwrite-only kernel
94    /// for every logical element before calling this method.
95    #[cfg(not(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject")))))]
96    unsafe fn assume_initialized(array: StridedArray<MaybeUninit<Self>>) -> StridedArray<Self>;
97
98    /// Release an owned buffer back to the pool for reuse.
99    /// Views are silently dropped (nothing to recycle).
100    fn pool_release(pool: &mut BufferPool, data: StridedData<'_, Self>);
101}
102
103/// Take the best-fit buffer (smallest capacity >= `total`) from a BTreeMap pool.
104fn take_best_fit<T>(pool: &mut BTreeMap<usize, Vec<Vec<T>>>, total: usize) -> Option<Vec<T>> {
105    // Find the smallest key >= total using BTreeMap range search.
106    let key = *pool.range(total..).next()?.0;
107    let vecs = pool.get_mut(&key)?;
108    let buf = vecs.pop();
109    if vecs.is_empty() {
110        pool.remove(&key);
111    }
112    buf
113}
114
115#[cfg(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject"))))]
116fn acquire_initialized<T: EinsumScalar>(
117    pool: &mut BTreeMap<usize, Vec<Vec<T>>>,
118    dims: &[usize],
119) -> crate::Result<StridedArray<T>> {
120    let total = dims
121        .iter()
122        .try_fold(1usize, |total, &dim| total.checked_mul(dim))
123        .ok_or(strided_view::StridedError::OffsetOverflow)?
124        .max(1);
125    let mut data = take_best_fit(pool, total).unwrap_or_default();
126    if data.len() < total {
127        data.resize_with(total, T::default);
128    } else {
129        data.truncate(total);
130    }
131    Ok(StridedArray::from_parts(
132        data,
133        dims,
134        &strided_view::col_major_strides(dims),
135        0,
136    )?)
137}
138
139impl PoolOps for f64 {
140    #[cfg(not(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject")))))]
141    fn pool_acquire(
142        pool: &mut BufferPool,
143        dims: &[usize],
144    ) -> crate::Result<StridedArray<MaybeUninit<f64>>> {
145        let total = dims
146            .iter()
147            .try_fold(1usize, |total, &dim| total.checked_mul(dim))
148            .ok_or(strided_view::StridedError::OffsetOverflow)?
149            .max(1);
150        // SAFETY: einsum2_into with beta=0 writes every output element before reading.
151        match take_best_fit(&mut pool.f64_pool, total) {
152            Some(buf) => unsafe {
153                let mut buf = std::mem::ManuallyDrop::new(buf);
154                let data = Vec::from_raw_parts(
155                    buf.as_mut_ptr().cast::<MaybeUninit<f64>>(),
156                    buf.len(),
157                    buf.capacity(),
158                );
159                Ok(StridedArray::col_major_from_buffer_uninit(data, dims))
160            },
161            None => Ok(unsafe { StridedArray::col_major_uninit(dims) }),
162        }
163    }
164
165    #[cfg(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject"))))]
166    fn pool_acquire_initialized(
167        pool: &mut BufferPool,
168        dims: &[usize],
169    ) -> crate::Result<StridedArray<Self>> {
170        acquire_initialized(&mut pool.f64_pool, dims)
171    }
172
173    #[cfg(not(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject")))))]
174    unsafe fn assume_initialized(array: StridedArray<MaybeUninit<f64>>) -> StridedArray<f64> {
175        let dims = array.dims().to_vec();
176        let strides = array.strides().to_vec();
177        let offset = 0;
178        let data = array.into_data();
179        let len = data.len();
180        let cap = data.capacity();
181        let ptr = data.as_ptr().cast_mut().cast::<f64>();
182        std::mem::forget(data);
183        StridedArray::from_parts(Vec::from_raw_parts(ptr, len, cap), &dims, &strides, offset)
184            .expect("validated pool layout")
185    }
186
187    fn pool_release(pool: &mut BufferPool, data: StridedData<'_, f64>) {
188        if let StridedData::Owned(arr) = data {
189            let buf = arr.into_data();
190            pool.f64_pool.entry(buf.len()).or_default().push(buf);
191        }
192    }
193}
194
195impl PoolOps for Complex64 {
196    #[cfg(not(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject")))))]
197    fn pool_acquire(
198        pool: &mut BufferPool,
199        dims: &[usize],
200    ) -> crate::Result<StridedArray<MaybeUninit<Complex64>>> {
201        let total = dims
202            .iter()
203            .try_fold(1usize, |total, &dim| total.checked_mul(dim))
204            .ok_or(strided_view::StridedError::OffsetOverflow)?
205            .max(1);
206        // SAFETY: einsum2_into with beta=0 writes every output element before reading.
207        match take_best_fit(&mut pool.c64_pool, total) {
208            Some(buf) => unsafe {
209                let mut buf = std::mem::ManuallyDrop::new(buf);
210                let data = Vec::from_raw_parts(
211                    buf.as_mut_ptr().cast::<MaybeUninit<Complex64>>(),
212                    buf.len(),
213                    buf.capacity(),
214                );
215                Ok(StridedArray::col_major_from_buffer_uninit(data, dims))
216            },
217            None => Ok(unsafe { StridedArray::col_major_uninit(dims) }),
218        }
219    }
220
221    #[cfg(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject"))))]
222    fn pool_acquire_initialized(
223        pool: &mut BufferPool,
224        dims: &[usize],
225    ) -> crate::Result<StridedArray<Self>> {
226        acquire_initialized(&mut pool.c64_pool, dims)
227    }
228
229    #[cfg(not(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject")))))]
230    unsafe fn assume_initialized(
231        array: StridedArray<MaybeUninit<Complex64>>,
232    ) -> StridedArray<Complex64> {
233        let dims = array.dims().to_vec();
234        let strides = array.strides().to_vec();
235        let data = array.into_data();
236        let len = data.len();
237        let cap = data.capacity();
238        let ptr = data.as_ptr().cast_mut().cast::<Complex64>();
239        std::mem::forget(data);
240        StridedArray::from_parts(Vec::from_raw_parts(ptr, len, cap), &dims, &strides, 0)
241            .expect("validated pool layout")
242    }
243
244    fn pool_release(pool: &mut BufferPool, data: StridedData<'_, Complex64>) {
245        if let StridedData::Owned(arr) = data {
246            let buf = arr.into_data();
247            pool.c64_pool.entry(buf.len()).or_default().push(buf);
248        }
249    }
250}
251
252// ---------------------------------------------------------------------------
253// Helper: collect all index chars from a subtree, preserving first-seen order
254// ---------------------------------------------------------------------------
255
256fn collect_all_ids(node: &EinsumNode) -> Vec<char> {
257    let mut result = Vec::new();
258    collect_all_ids_inner(node, &mut result);
259    result
260}
261
262fn collect_all_ids_inner(node: &EinsumNode, result: &mut Vec<char>) {
263    match node {
264        EinsumNode::Leaf { ids, .. } => {
265            for &id in ids {
266                if !result.contains(&id) {
267                    result.push(id);
268                }
269            }
270        }
271        EinsumNode::Contract { args } => {
272            for arg in args {
273                collect_all_ids_inner(arg, result);
274            }
275        }
276    }
277}
278
279// ---------------------------------------------------------------------------
280// Helper: compute which output indices a Contract node should keep
281// ---------------------------------------------------------------------------
282
283/// For a Contract node with `args`, decide which index chars to keep.
284///
285/// An index is kept if it appears in `needed_ids` (what the parent/caller
286/// needs from this node) AND it is actually present in at least one child
287/// subtree.
288fn compute_contract_output_ids(args: &[EinsumNode], needed_ids: &[char]) -> Vec<char> {
289    if args.len() == 2 {
290        let left_ids = collect_all_ids(&args[0]);
291        let right_ids = collect_all_ids(&args[1]);
292        return compute_binary_output_ids(&left_ids, &right_ids, needed_ids);
293    }
294
295    // Walk args in order and collect ids preserving first-seen order
296    let mut all_ids_ordered = Vec::new();
297    for arg in args {
298        for id in collect_all_ids(arg) {
299            if !all_ids_ordered.contains(&id) {
300                all_ids_ordered.push(id);
301            }
302        }
303    }
304
305    // Keep only ids that the parent needs
306    all_ids_ordered
307        .into_iter()
308        .filter(|id| needed_ids.contains(id))
309        .collect()
310}
311
312/// Compute the set of ids that a child of a Contract node needs to provide.
313///
314/// A child needs to keep an index if:
315///   - It is in the Contract's own `output_ids` (parent needs it), OR
316///   - It is shared with at least one sibling subtree (contraction index).
317///
318/// The child is only responsible for indices in its own subtree, but this
319/// function returns the full needed set; the child will naturally intersect
320/// with its own ids.
321fn compute_child_needed_ids(
322    output_ids: &[char],
323    child_idx: usize,
324    args: &[EinsumNode],
325) -> Vec<char> {
326    let mut needed: Vec<char> = output_ids.to_vec();
327
328    // Add indices shared between this child and any sibling
329    let child_ids = collect_all_ids(&args[child_idx]);
330    for (j, arg) in args.iter().enumerate() {
331        if j == child_idx {
332            continue;
333        }
334        let sibling_ids = collect_all_ids(arg);
335        for &id in &child_ids {
336            if sibling_ids.contains(&id) && !needed.contains(&id) {
337                needed.push(id);
338            }
339        }
340    }
341
342    needed
343}
344
345// ---------------------------------------------------------------------------
346// Pairwise contraction (borrows operands)
347// ---------------------------------------------------------------------------
348
349fn out_dims_from_map(
350    dim_map: &HashMap<char, usize>,
351    output_ids: &[char],
352    size_dict: &HashMap<char, usize>,
353) -> crate::Result<Vec<usize>> {
354    let mut out_dims = Vec::with_capacity(output_ids.len());
355    for &id in output_ids {
356        if let Some(&dim) = dim_map.get(&id) {
357            out_dims.push(dim);
358        } else if let Some(&dim) = size_dict.get(&id) {
359            out_dims.push(dim);
360        } else {
361            return Err(crate::EinsumError::OrphanOutputAxis(id.to_string()));
362        }
363    }
364    Ok(out_dims)
365}
366
367/// Look up output dimensions directly from left/right ids without HashMap allocation.
368fn out_dims_from_ids(
369    left_ids: &[char],
370    left_dims: &[usize],
371    right_ids: &[char],
372    right_dims: &[usize],
373    output_ids: &[char],
374    size_dict: &HashMap<char, usize>,
375) -> crate::Result<Vec<usize>> {
376    let mut out_dims = Vec::with_capacity(output_ids.len());
377    for &id in output_ids {
378        if let Some(pos) = left_ids.iter().position(|&c| c == id) {
379            out_dims.push(left_dims[pos]);
380        } else if let Some(pos) = right_ids.iter().position(|&c| c == id) {
381            out_dims.push(right_dims[pos]);
382        } else if let Some(&dim) = size_dict.get(&id) {
383            out_dims.push(dim);
384        } else {
385            return Err(crate::EinsumError::OrphanOutputAxis(id.to_string()));
386        }
387    }
388    Ok(out_dims)
389}
390
391/// Compute binary contraction output id order.
392///
393/// Uses canonical `[lo, ro, batch]` order:
394/// - lo: ids only in left and needed
395/// - ro: ids only in right and needed
396/// - batch: ids in both and needed
397fn compute_binary_output_ids(
398    left_ids: &[char],
399    right_ids: &[char],
400    needed_ids: &[char],
401) -> Vec<char> {
402    let mut out = Vec::new();
403    for &id in left_ids {
404        if needed_ids.contains(&id) && !right_ids.contains(&id) && !out.contains(&id) {
405            out.push(id);
406        }
407    }
408    for &id in right_ids {
409        if needed_ids.contains(&id) && !left_ids.contains(&id) && !out.contains(&id) {
410            out.push(id);
411        }
412    }
413    for &id in left_ids {
414        if needed_ids.contains(&id) && right_ids.contains(&id) && !out.contains(&id) {
415            out.push(id);
416        }
417    }
418    out
419}
420
421/// Faer compatibility path until the upstream overwrite API exists.
422#[cfg(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject"))))]
423fn eval_pair_alloc<T: PoolOps>(
424    ld: StridedData<'_, T>,
425    left_ids: &[char],
426    rd: StridedData<'_, T>,
427    right_ids: &[char],
428    output_ids: &[char],
429    pool: &mut BufferPool,
430    size_dict: &HashMap<char, usize>,
431) -> crate::Result<EinsumOperand<'static>> {
432    let out_dims = out_dims_from_ids(
433        left_ids,
434        ld.dims(),
435        right_ids,
436        rd.dims(),
437        output_ids,
438        size_dict,
439    )?;
440    let mut c_arr = T::pool_acquire_initialized(pool, &out_dims)?;
441    let (left, right) = (ld, rd);
442    match (left, right) {
443        (StridedData::Owned(a), StridedData::Owned(b)) => {
444            einsum2_into_owned(
445                c_arr.view_mut(),
446                a,
447                b,
448                output_ids,
449                left_ids,
450                right_ids,
451                T::one(),
452                T::zero(),
453                false,
454                false,
455            )?;
456        }
457        (left, right) => {
458            let a_view = left.as_view();
459            let b_view = right.as_view();
460            einsum2_into(
461                c_arr.view_mut(),
462                &a_view,
463                &b_view,
464                output_ids,
465                left_ids,
466                right_ids,
467                T::one(),
468                T::zero(),
469            )?;
470            T::pool_release(pool, left);
471            T::pool_release(pool, right);
472        }
473    }
474    Ok(T::wrap_array(c_arr))
475}
476
477/// Generic inner function for pairwise contraction with buffer pool.
478///
479/// Acquires an output buffer, runs `einsum2_into`, and releases input buffers
480/// back to the pool.
481#[cfg(not(all(feature = "faer", not(any(feature = "blas", feature = "blas-inject")))))]
482fn eval_pair_alloc<T: PoolOps>(
483    ld: StridedData<'_, T>,
484    left_ids: &[char],
485    rd: StridedData<'_, T>,
486    right_ids: &[char],
487    output_ids: &[char],
488    pool: &mut BufferPool,
489    size_dict: &HashMap<char, usize>,
490) -> crate::Result<EinsumOperand<'static>> {
491    let out_dims = out_dims_from_ids(
492        left_ids,
493        ld.dims(),
494        right_ids,
495        rd.dims(),
496        output_ids,
497        size_dict,
498    )?;
499    let mut c_arr = T::pool_acquire(pool, &out_dims)?;
500    match (ld, rd) {
501        // Preserve ownership so strided-einsum2 can use prepare_input_owned
502        // and avoid extra materialization in prepare_input_view.
503        (StridedData::Owned(a), StridedData::Owned(b)) => {
504            let dims = c_arr.dims().to_vec();
505            let strides = c_arr.strides().to_vec();
506            let mut out = strided_view::RawStridedMut::new(c_arr.data_mut(), &dims, &strides, 0)?;
507            strided_einsum2::einsum2_into_owned_uninit(
508                &mut out,
509                a,
510                b,
511                output_ids,
512                left_ids,
513                right_ids,
514                T::one(),
515                &ExecContext::serial(),
516            )?;
517        }
518        (ld, rd) => {
519            let a_view = ld.as_view();
520            let b_view = rd.as_view();
521            let dims = c_arr.dims().to_vec();
522            let strides = c_arr.strides().to_vec();
523            let mut out = strided_view::RawStridedMut::new(c_arr.data_mut(), &dims, &strides, 0)?;
524            einsum2_into_uninit(
525                &mut out,
526                &a_view,
527                &b_view,
528                output_ids,
529                left_ids,
530                right_ids,
531                T::one(),
532                &ExecContext::serial(),
533            )?;
534            T::pool_release(pool, ld);
535            T::pool_release(pool, rd);
536        }
537    }
538    let c_arr = unsafe { T::assume_initialized(c_arr) };
539    Ok(T::wrap_array(c_arr))
540}
541
542/// Contract two operands, consuming them by value. Promotes to c64 if types are mixed.
543///
544/// Uses view-based `einsum2_into` so that owned input buffers can be released
545/// back to the `pool` for reuse by subsequent contraction steps.
546fn eval_pair(
547    left: EinsumOperand<'_>,
548    left_ids: &[char],
549    right: EinsumOperand<'_>,
550    right_ids: &[char],
551    output_ids: &[char],
552    pool: &mut BufferPool,
553    size_dict: &HashMap<char, usize>,
554) -> crate::Result<EinsumOperand<'static>> {
555    match (left, right) {
556        (EinsumOperand::F64(ld), EinsumOperand::F64(rd)) => {
557            eval_pair_alloc(ld, left_ids, rd, right_ids, output_ids, pool, size_dict)
558        }
559        (EinsumOperand::C64(ld), EinsumOperand::C64(rd)) => {
560            eval_pair_alloc(ld, left_ids, rd, right_ids, output_ids, pool, size_dict)
561        }
562        (left, right) => {
563            // Mixed types: promote both to c64 by consuming, then recurse (hits C64/C64 branch)
564            let left_c64 = left.to_c64_owned();
565            let right_c64 = right.to_c64_owned();
566            eval_pair(
567                left_c64, left_ids, right_c64, right_ids, output_ids, pool, size_dict,
568            )
569        }
570    }
571}
572
573// ---------------------------------------------------------------------------
574// Pairwise contraction into user-provided output (zero-copy for final step)
575// ---------------------------------------------------------------------------
576
577/// Contract two operands directly into a user-provided output buffer.
578///
579/// Unlike `eval_pair`, this writes into `output` with alpha/beta scaling
580/// instead of allocating a fresh array. Used for the final contraction in
581/// `evaluate_into`.
582fn eval_pair_into<T: EinsumScalar>(
583    left: EinsumOperand<'_>,
584    left_ids: &[char],
585    right: EinsumOperand<'_>,
586    right_ids: &[char],
587    output: StridedViewMut<T>,
588    output_ids: &[char],
589    alpha: T,
590    beta: T,
591) -> crate::Result<()> {
592    let left_data = T::extract_data(left)?;
593    let right_data = T::extract_data(right)?;
594
595    match (left_data, right_data) {
596        (StridedData::Owned(a), StridedData::Owned(b)) => {
597            #[cfg(any(feature = "faer", feature = "blas", feature = "blas-inject"))]
598            einsum2_into_owned(
599                output, a, b, output_ids, left_ids, right_ids, alpha, beta, false, false,
600            )?;
601            #[cfg(not(any(feature = "faer", feature = "blas", feature = "blas-inject")))]
602            einsum2_into(
603                output,
604                &a.view(),
605                &b.view(),
606                output_ids,
607                left_ids,
608                right_ids,
609                alpha,
610                beta,
611            )?;
612        }
613        (StridedData::Owned(a), StridedData::View(b)) => {
614            einsum2_into(
615                output,
616                &a.view(),
617                &b,
618                output_ids,
619                left_ids,
620                right_ids,
621                alpha,
622                beta,
623            )?;
624        }
625        (StridedData::View(a), StridedData::Owned(b)) => {
626            einsum2_into(
627                output,
628                &a,
629                &b.view(),
630                output_ids,
631                left_ids,
632                right_ids,
633                alpha,
634                beta,
635            )?;
636        }
637        (StridedData::View(a), StridedData::View(b)) => {
638            einsum2_into(output, &a, &b, output_ids, left_ids, right_ids, alpha, beta)?;
639        }
640    }
641    Ok(())
642}
643
644// ---------------------------------------------------------------------------
645// Accumulate helper for single-tensor results
646// ---------------------------------------------------------------------------
647
648/// Write `output = alpha * result + beta * output`.
649///
650/// `result` must already have the same shape as `output`.
651fn accumulate_into<T: EinsumScalar>(
652    output: &mut StridedViewMut<T>,
653    result: &StridedArray<T>,
654    alpha: T,
655    beta: T,
656) -> crate::Result<()> {
657    let result_view = result.view();
658    if beta == T::zero() {
659        if alpha == T::one() {
660            strided_kernel::copy_into(output, &result_view)?;
661        } else {
662            copy_scale(output, &result_view, alpha)?;
663        }
664    } else {
665        // General case: output = alpha * result + beta * output
666        // axpy does: output += alpha * result, so we need to scale output by beta first.
667        // We use a temporary to avoid aliasing issues.
668        let dims = output.dims().to_vec();
669        let mut temp = StridedArray::<T>::col_major(&dims);
670        strided_kernel::copy_into(&mut temp.view_mut(), &result_view)?;
671        // temp now holds result data in col-major layout
672        // output = beta * output + alpha * temp
673        // Using zip_map2_into would need output as both src and dest.
674        // Instead: copy_scale output into a second temp, then zip_map2_into.
675        // But simpler: use axpy which reads+writes dest.
676        // axpy(dest, src, alpha) does: dest[i] = dest[i] + alpha * src[i]
677        // So: first scale output by beta, then axpy with alpha.
678        // "scale output by beta" = copy_scale into temp2, copy back. Or just
679        // use a different approach: compute full result in temp, copy to output.
680        //
681        // Simplest correct approach for this rare path:
682        let mut output_copy = StridedArray::<T>::col_major(&dims);
683        strided_kernel::copy_into(&mut output_copy.view_mut(), &output.as_view())?;
684        strided_kernel::zip_map2_into(output, &temp.view(), &output_copy.view(), |r, o| {
685            alpha * r + beta * o
686        })?;
687    }
688    Ok(())
689}
690
691// ---------------------------------------------------------------------------
692// Single-tensor dispatch (borrows operand)
693// ---------------------------------------------------------------------------
694
695/// Generic inner function for single-tensor einsum.
696fn eval_single_typed<T: EinsumScalar>(
697    data: &StridedData<'_, T>,
698    input_ids: &[char],
699    output_ids: &[char],
700    size_dict: &HashMap<char, usize>,
701) -> crate::Result<EinsumOperand<'static>> {
702    let view = data.as_view();
703    let result = single_tensor_einsum(&view, input_ids, output_ids, Some(size_dict))?;
704    Ok(T::wrap_array(result))
705}
706
707fn eval_single(
708    operand: &EinsumOperand<'_>,
709    input_ids: &[char],
710    output_ids: &[char],
711    size_dict: &HashMap<char, usize>,
712) -> crate::Result<EinsumOperand<'static>> {
713    match operand {
714        EinsumOperand::F64(data) => eval_single_typed(data, input_ids, output_ids, size_dict),
715        EinsumOperand::C64(data) => eval_single_typed(data, input_ids, output_ids, size_dict),
716    }
717}
718
719// ---------------------------------------------------------------------------
720// Permutation helpers
721// ---------------------------------------------------------------------------
722
723/// Check if the transformation from input_ids to output_ids is a pure
724/// permutation (same set of chars, same length, no repeated indices).
725fn is_permutation_only(input_ids: &[char], output_ids: &[char]) -> bool {
726    if input_ids.len() != output_ids.len() {
727        return false;
728    }
729    // Check no repeated indices in input (linear scan)
730    for (i, &id) in input_ids.iter().enumerate() {
731        if input_ids[..i].contains(&id) {
732            return false; // repeated index = trace, not permutation
733        }
734    }
735    // Check all output ids appear in input
736    for &id in output_ids {
737        if !input_ids.contains(&id) {
738            return false;
739        }
740    }
741    true
742}
743
744/// Compute the permutation that maps input_ids ordering to output_ids ordering.
745fn compute_permutation(input_ids: &[char], output_ids: &[char]) -> Vec<usize> {
746    output_ids
747        .iter()
748        .map(|oid| input_ids.iter().position(|iid| iid == oid).unwrap())
749        .collect()
750}
751
752// ---------------------------------------------------------------------------
753// Execute omeco NestedEinsum tree
754// ---------------------------------------------------------------------------
755
756/// Execute an omeco-optimized contraction tree by contracting pairs according
757/// to the tree structure.
758///
759/// `children` is a Vec of Option-wrapped (operand, ids) pairs. The omeco
760/// `NestedEinsum::Leaf` variant references children by index; we `.take()`
761/// each entry to move ownership out exactly once.
762fn execute_nested<'a>(
763    nested: &omeco::NestedEinsum<char>,
764    children: &mut Vec<Option<(EinsumOperand<'a>, Vec<char>)>>,
765    pool: &mut BufferPool,
766    size_dict: &HashMap<char, usize>,
767) -> crate::Result<(EinsumOperand<'a>, Vec<char>)> {
768    match nested {
769        omeco::NestedEinsum::Leaf { tensor_index } => {
770            let slot = children.get_mut(*tensor_index).ok_or_else(|| {
771                crate::EinsumError::Internal(format!(
772                    "optimizer referenced child index {} out of bounds",
773                    tensor_index
774                ))
775            })?;
776            let (op, ids) = slot.take().ok_or_else(|| {
777                crate::EinsumError::Internal(format!(
778                    "child operand {} was already consumed",
779                    tensor_index
780                ))
781            })?;
782            Ok((op, ids))
783        }
784        omeco::NestedEinsum::Node { args, eins } => {
785            if args.len() != 2 {
786                return Err(crate::EinsumError::Internal(format!(
787                    "optimizer produced non-binary node with {} children",
788                    args.len()
789                )));
790            }
791            let (left, left_ids) = execute_nested(&args[0], children, pool, size_dict)?;
792            let (right, right_ids) = execute_nested(&args[1], children, pool, size_dict)?;
793            let output_ids: Vec<char> = eins.iy.clone();
794            let result = eval_pair(
795                left,
796                &left_ids,
797                right,
798                &right_ids,
799                &output_ids,
800                pool,
801                size_dict,
802            )?;
803            Ok((result, output_ids))
804        }
805    }
806}
807
808/// Execute an omeco-optimized contraction tree, writing the root contraction
809/// directly into a user-provided output buffer.
810///
811/// Inner (non-root) contractions use normal `execute_nested` / `eval_pair`.
812/// Only the root `Node`'s contraction is written directly into `output`.
813fn execute_nested_into<'a, T: EinsumScalar>(
814    nested: &omeco::NestedEinsum<char>,
815    children: &mut Vec<Option<(EinsumOperand<'a>, Vec<char>)>>,
816    output: StridedViewMut<T>,
817    output_ids: &[char],
818    alpha: T,
819    beta: T,
820    pool: &mut BufferPool,
821    size_dict: &HashMap<char, usize>,
822) -> crate::Result<()> {
823    match nested {
824        omeco::NestedEinsum::Node { args, eins: _ } => {
825            if args.len() != 2 {
826                return Err(crate::EinsumError::Internal(format!(
827                    "optimizer produced non-binary node with {} children",
828                    args.len()
829                )));
830            }
831            // Evaluate children normally (they allocate temporaries)
832            let (left, left_ids) = execute_nested(&args[0], children, pool, size_dict)?;
833            let (right, right_ids) = execute_nested(&args[1], children, pool, size_dict)?;
834            // Root contraction writes directly into user's output
835            eval_pair_into(
836                left, &left_ids, right, &right_ids, output, output_ids, alpha, beta,
837            )
838        }
839        omeco::NestedEinsum::Leaf { tensor_index } => {
840            // Root is a single leaf — extract and accumulate into output
841            let slot = children.get_mut(*tensor_index).ok_or_else(|| {
842                crate::EinsumError::Internal(format!(
843                    "optimizer referenced child index {} out of bounds",
844                    tensor_index
845                ))
846            })?;
847            let (op, op_ids) = slot.take().ok_or_else(|| {
848                crate::EinsumError::Internal(format!(
849                    "child operand {} was already consumed",
850                    tensor_index
851                ))
852            })?;
853            let data = T::extract_data(op)?;
854            let arr = data.into_array();
855            // Permute if needed
856            if op_ids != output_ids {
857                let perm = compute_permutation(&op_ids, output_ids);
858                let permuted = arr.permuted(&perm)?;
859                accumulate_into(&mut { output }, &permuted, alpha, beta)?;
860            } else {
861                accumulate_into(&mut { output }, &arr, alpha, beta)?;
862            }
863            Ok(())
864        }
865    }
866}
867
868// ---------------------------------------------------------------------------
869// Recursive evaluation
870// ---------------------------------------------------------------------------
871
872/// Recursively evaluate an `EinsumNode`, returning the result operand and
873/// the index chars labelling its axes.
874///
875/// Leaf nodes return borrowed views directly (no copy). Contract nodes
876/// always produce freshly allocated results (`'static` coerced to `'a`).
877///
878/// `needed_ids` tells this node which indices the caller needs in the result.
879/// For the root call this is the final output indices of the einsum.
880fn eval_node<'a>(
881    node: &EinsumNode,
882    operands: &mut Vec<Option<EinsumOperand<'a>>>,
883    needed_ids: &[char],
884    pool: &mut BufferPool,
885    size_dict: &HashMap<char, usize>,
886) -> crate::Result<(EinsumOperand<'a>, Vec<char>)> {
887    match node {
888        EinsumNode::Leaf { ids, tensor_index } => {
889            let found = operands.len();
890            let slot = operands.get_mut(*tensor_index).ok_or_else(|| {
891                crate::EinsumError::OperandCountMismatch {
892                    expected: tensor_index + 1,
893                    found,
894                }
895            })?;
896            let op = slot.take().ok_or_else(|| {
897                crate::EinsumError::Internal(format!(
898                    "operand {} was already consumed",
899                    tensor_index
900                ))
901            })?;
902            // Return borrowed view directly — no to_owned_static() copy.
903            Ok((op, ids.clone()))
904        }
905        EinsumNode::Contract { args } => {
906            // Determine which indices this Contract node should output.
907            let node_output_ids = compute_contract_output_ids(args, needed_ids);
908
909            match args.len() {
910                0 => unreachable!("empty Contract node"),
911                1 => {
912                    // Single-tensor operation.
913                    let child_needed = compute_child_needed_ids(&node_output_ids, 0, args);
914                    let (child_op, child_ids) =
915                        eval_node(&args[0], operands, &child_needed, pool, size_dict)?;
916
917                    // Identity passthrough: no allocation needed.
918                    if child_ids == node_output_ids {
919                        return Ok((child_op, node_output_ids));
920                    }
921
922                    // Permutation-only passthrough: metadata reorder, no data copy.
923                    if is_permutation_only(&child_ids, &node_output_ids) {
924                        let perm = compute_permutation(&child_ids, &node_output_ids);
925                        return Ok((child_op.permuted(&perm)?, node_output_ids));
926                    }
927
928                    // General case: trace, reduction, repeat, duplicate, etc.
929                    let result = eval_single(&child_op, &child_ids, &node_output_ids, size_dict)?;
930                    Ok((result, node_output_ids))
931                }
932                2 => {
933                    // Binary contraction.
934                    let left_needed = compute_child_needed_ids(&node_output_ids, 0, args);
935                    let right_needed = compute_child_needed_ids(&node_output_ids, 1, args);
936                    let (left, left_ids) =
937                        eval_node(&args[0], operands, &left_needed, pool, size_dict)?;
938                    let (right, right_ids) =
939                        eval_node(&args[1], operands, &right_needed, pool, size_dict)?;
940                    let result = eval_pair(
941                        left,
942                        &left_ids,
943                        right,
944                        &right_ids,
945                        &node_output_ids,
946                        pool,
947                        size_dict,
948                    )?;
949                    Ok((result, node_output_ids))
950                }
951                _ => {
952                    // 3+ children: use omeco greedy optimizer to find
953                    // an efficient pairwise contraction order.
954
955                    // 1. Evaluate all children to get their operands and ids
956                    let mut children: Vec<Option<(EinsumOperand<'a>, Vec<char>)>> = Vec::new();
957                    for (i, arg) in args.iter().enumerate() {
958                        let child_needed = compute_child_needed_ids(&node_output_ids, i, args);
959                        let (op, ids) = eval_node(arg, operands, &child_needed, pool, size_dict)?;
960                        children.push(Some((op, ids)));
961                    }
962
963                    // 2. Build dimension sizes map from evaluated operands
964                    let mut dim_sizes: HashMap<char, usize> = HashMap::new();
965                    for child_opt in &children {
966                        if let Some((op, ids)) = child_opt {
967                            for (j, &id) in ids.iter().enumerate() {
968                                dim_sizes.insert(id, op.dims()[j]);
969                            }
970                        }
971                    }
972
973                    // 3. Build omeco EinCode from child ids and node output ids
974                    let input_ids: Vec<Vec<char>> = children
975                        .iter()
976                        .map(|c| c.as_ref().unwrap().1.clone())
977                        .collect();
978                    let code = omeco::EinCode::new(input_ids, node_output_ids.clone());
979
980                    // 4. Optimize using omeco greedy method
981                    let optimizer = omeco::GreedyMethod::default();
982                    let nested = omeco::CodeOptimizer::optimize(&optimizer, &code, &dim_sizes)
983                        .ok_or_else(|| {
984                            crate::EinsumError::Internal(
985                                "optimizer failed to produce a plan".into(),
986                            )
987                        })?;
988
989                    // 5. Execute the nested contraction tree
990                    let (result, result_ids) =
991                        execute_nested(&nested, &mut children, pool, size_dict)?;
992                    Ok((result, result_ids))
993                }
994            }
995        }
996    }
997}
998
999// ---------------------------------------------------------------------------
1000// Public API
1001// ---------------------------------------------------------------------------
1002
1003impl EinsumCode {
1004    /// Evaluate the einsum contraction tree with the given operands.
1005    ///
1006    /// Borrowed view operands are propagated through the tree without copying.
1007    /// The result lifetime matches the input operand lifetime: if all inputs
1008    /// are owned (`'static`), the result is also `'static`.
1009    ///
1010    /// Pass `size_dict` to specify sizes for output indices not present in any
1011    /// input (generative outputs like `"->ii"` or `"i->ij"`).
1012    /// Evaluate the einsum contraction tree with the given operands.
1013    ///
1014    /// Equivalent to `evaluate_with_pool(operands, size_dict, None)`.
1015    pub fn evaluate<'a>(
1016        &self,
1017        operands: Vec<EinsumOperand<'a>>,
1018        size_dict: Option<&HashMap<char, usize>>,
1019    ) -> crate::Result<EinsumOperand<'a>> {
1020        self.evaluate_with_pool(operands, size_dict, None)
1021    }
1022
1023    /// Evaluate the einsum contraction tree, optionally reusing a buffer pool.
1024    ///
1025    /// Pass `Some(&mut pool)` to reuse intermediate buffers across calls.
1026    /// Pass `None` to use a fresh temporary pool (buffers freed on return).
1027    pub fn evaluate_with_pool<'a>(
1028        &self,
1029        operands: Vec<EinsumOperand<'a>>,
1030        size_dict: Option<&HashMap<char, usize>>,
1031        pool: Option<&mut BufferPool>,
1032    ) -> crate::Result<EinsumOperand<'a>> {
1033        let expected = leaf_count(&self.root);
1034        if operands.len() != expected {
1035            return Err(crate::EinsumError::OperandCountMismatch {
1036                expected,
1037                found: operands.len(),
1038            });
1039        }
1040
1041        let mut ops: Vec<Option<EinsumOperand<'a>>> = operands.into_iter().map(Some).collect();
1042        let mut temp_pool;
1043        let pool = match pool {
1044            Some(p) => p,
1045            None => {
1046                temp_pool = BufferPool::new();
1047                &mut temp_pool
1048            }
1049        };
1050
1051        // Build unified size_dict: operand-inferred sizes + user-provided overrides
1052        let mut unified = build_dim_map(&self.root, &ops);
1053        if let Some(sd) = size_dict {
1054            merge_size_dict(&mut unified, sd)?;
1055        }
1056
1057        let (result, result_ids) =
1058            eval_node(&self.root, &mut ops, &self.output_ids, pool, &unified)?;
1059
1060        // If the result ids already match the desired output, we're done.
1061        if result_ids == self.output_ids {
1062            return Ok(result);
1063        }
1064
1065        // Permutation-only: reorder metadata, no data copy.
1066        if is_permutation_only(&result_ids, &self.output_ids) {
1067            let perm = compute_permutation(&result_ids, &self.output_ids);
1068            return Ok(result.permuted(&perm)?);
1069        }
1070
1071        // General fallback: reduce/trace/repeat/duplicate to match the final output_ids.
1072        let adjusted = eval_single(&result, &result_ids, &self.output_ids, &unified)?;
1073        Ok(adjusted)
1074    }
1075}
1076
1077fn leaf_count(node: &EinsumNode) -> usize {
1078    match node {
1079        EinsumNode::Leaf { .. } => 1,
1080        EinsumNode::Contract { args } => args.iter().map(leaf_count).sum(),
1081    }
1082}
1083
1084/// Build a dimension map from operands and the parsed tree.
1085///
1086/// Maps each index char to its dimension size by walking the tree's Leaf nodes
1087/// and matching them to the corresponding operands.
1088fn build_dim_map(
1089    node: &EinsumNode,
1090    operands: &[Option<EinsumOperand<'_>>],
1091) -> HashMap<char, usize> {
1092    let mut dim_map = HashMap::new();
1093    build_dim_map_inner(node, operands, &mut dim_map);
1094    dim_map
1095}
1096
1097/// Merge user-provided size_dict into the unified map.
1098///
1099/// Returns an error if a label appears in both maps with different sizes.
1100fn merge_size_dict(
1101    unified: &mut HashMap<char, usize>,
1102    user: &HashMap<char, usize>,
1103) -> crate::Result<()> {
1104    for (&label, &size) in user {
1105        if let Some(&existing) = unified.get(&label) {
1106            if existing != size {
1107                return Err(crate::EinsumError::DimensionMismatch {
1108                    axis: label.to_string(),
1109                    dim_a: existing,
1110                    dim_b: size,
1111                });
1112            }
1113        } else {
1114            unified.insert(label, size);
1115        }
1116    }
1117    Ok(())
1118}
1119
1120fn build_dim_map_inner(
1121    node: &EinsumNode,
1122    operands: &[Option<EinsumOperand<'_>>],
1123    dim_map: &mut HashMap<char, usize>,
1124) {
1125    match node {
1126        EinsumNode::Leaf { ids, tensor_index } => {
1127            if let Some(Some(op)) = operands.get(*tensor_index) {
1128                for (i, &id) in ids.iter().enumerate() {
1129                    dim_map.insert(id, op.dims()[i]);
1130                }
1131            }
1132        }
1133        EinsumNode::Contract { args } => {
1134            for arg in args {
1135                build_dim_map_inner(arg, operands, dim_map);
1136            }
1137        }
1138    }
1139}
1140
1141impl EinsumCode {
1142    /// Evaluate the einsum contraction tree, writing the result directly into
1143    /// a user-provided output buffer with alpha/beta scaling.
1144    ///
1145    /// `output = alpha * einsum(operands) + beta * output`
1146    ///
1147    /// The output element type `T` must match the computation: use `f64` when
1148    /// all operands are real, `Complex64` when any operand is complex.
1149    /// If `T = f64` but any operand is complex, returns `TypeMismatch` error.
1150    /// If `T = Complex64`, real operands are promoted automatically.
1151    ///
1152    /// Pass `size_dict` to specify sizes for output indices not present in any
1153    /// input (generative outputs like `"->ii"` or `"i->ij"`).
1154    pub fn evaluate_into<T: EinsumScalar>(
1155        &self,
1156        operands: Vec<EinsumOperand<'_>>,
1157        output: StridedViewMut<T>,
1158        alpha: T,
1159        beta: T,
1160        size_dict: Option<&HashMap<char, usize>>,
1161    ) -> crate::Result<()> {
1162        self.evaluate_into_with_pool(operands, output, alpha, beta, size_dict, None)
1163    }
1164
1165    /// Evaluate the einsum contraction tree, writing the result into a
1166    /// pre-allocated output buffer with alpha/beta scaling and optional
1167    /// buffer pool reuse.
1168    ///
1169    /// `output = alpha * einsum(operands) + beta * output`
1170    ///
1171    /// Pass `Some(&mut pool)` to reuse intermediate buffers across calls.
1172    /// Pass `None` to use a fresh temporary pool (buffers freed on return).
1173    pub fn evaluate_into_with_pool<T: EinsumScalar>(
1174        &self,
1175        operands: Vec<EinsumOperand<'_>>,
1176        mut output: StridedViewMut<T>,
1177        alpha: T,
1178        beta: T,
1179        size_dict: Option<&HashMap<char, usize>>,
1180        pool: Option<&mut BufferPool>,
1181    ) -> crate::Result<()> {
1182        let expected = leaf_count(&self.root);
1183        if operands.len() != expected {
1184            return Err(crate::EinsumError::OperandCountMismatch {
1185                expected,
1186                found: operands.len(),
1187            });
1188        }
1189
1190        // Validate output type compatibility
1191        let mut ops: Vec<Option<EinsumOperand<'_>>> = operands.into_iter().map(Some).collect();
1192        T::validate_operands(&ops)?;
1193
1194        // Build unified size_dict: operand-inferred sizes + user-provided overrides
1195        let mut unified = build_dim_map(&self.root, &ops);
1196        if let Some(sd) = size_dict {
1197            merge_size_dict(&mut unified, sd)?;
1198        }
1199
1200        // Compute expected output shape
1201        let expected_dims = out_dims_from_map(&unified, &self.output_ids, &unified)?;
1202        if output.dims() != expected_dims.as_slice() {
1203            return Err(crate::EinsumError::OutputShapeMismatch {
1204                expected: expected_dims,
1205                got: output.dims().to_vec(),
1206            });
1207        }
1208
1209        let mut temp_pool;
1210        let pool = match pool {
1211            Some(p) => p,
1212            None => {
1213                temp_pool = BufferPool::new();
1214                &mut temp_pool
1215            }
1216        };
1217
1218        match &self.root {
1219            EinsumNode::Leaf { ids, tensor_index } => {
1220                // Single operand: extract, permute/trace, accumulate
1221                let op = ops[*tensor_index].take().ok_or_else(|| {
1222                    crate::EinsumError::Internal("operand already consumed".into())
1223                })?;
1224                let single_result = eval_single(&op, ids, &self.output_ids, &unified)?;
1225                let data = T::extract_data(single_result)?;
1226                accumulate_into(&mut output, &data.into_array(), alpha, beta)?;
1227            }
1228            EinsumNode::Contract { args } => match args.len() {
1229                0 => unreachable!("empty Contract node"),
1230                1 => {
1231                    // Single child: evaluate, then accumulate
1232                    let child_needed = compute_child_needed_ids(&self.output_ids, 0, args);
1233                    let (child_op, child_ids) =
1234                        eval_node(&args[0], &mut ops, &child_needed, pool, &unified)?;
1235
1236                    if child_ids == self.output_ids {
1237                        // Identity: just accumulate
1238                        let data = T::extract_data(child_op)?;
1239                        accumulate_into(&mut output, &data.into_array(), alpha, beta)?;
1240                    } else if is_permutation_only(&child_ids, &self.output_ids) {
1241                        // Permutation: permute the data, then accumulate
1242                        let perm = compute_permutation(&child_ids, &self.output_ids);
1243                        let data = T::extract_data(child_op)?;
1244                        let arr = data.into_array();
1245                        let permuted = arr.permuted(&perm)?;
1246                        accumulate_into(&mut output, &permuted, alpha, beta)?;
1247                    } else {
1248                        // General: trace/reduction/repeat/duplicate
1249                        let result =
1250                            eval_single(&child_op, &child_ids, &self.output_ids, &unified)?;
1251                        let data = T::extract_data(result)?;
1252                        accumulate_into(&mut output, &data.into_array(), alpha, beta)?;
1253                    }
1254                }
1255                2 => {
1256                    // Binary contraction: write directly into output
1257                    let left_needed = compute_child_needed_ids(&self.output_ids, 0, args);
1258                    let right_needed = compute_child_needed_ids(&self.output_ids, 1, args);
1259                    let (left, left_ids) =
1260                        eval_node(&args[0], &mut ops, &left_needed, pool, &unified)?;
1261                    let (right, right_ids) =
1262                        eval_node(&args[1], &mut ops, &right_needed, pool, &unified)?;
1263                    eval_pair_into(
1264                        left,
1265                        &left_ids,
1266                        right,
1267                        &right_ids,
1268                        output,
1269                        &self.output_ids,
1270                        alpha,
1271                        beta,
1272                    )?;
1273                }
1274                _ => {
1275                    // 3+ children: use omeco, final contraction into output
1276                    let node_output_ids = compute_contract_output_ids(args, &self.output_ids);
1277
1278                    let mut children: Vec<Option<(EinsumOperand<'_>, Vec<char>)>> = Vec::new();
1279                    for (i, arg) in args.iter().enumerate() {
1280                        let child_needed = compute_child_needed_ids(&node_output_ids, i, args);
1281                        let (op, ids) = eval_node(arg, &mut ops, &child_needed, pool, &unified)?;
1282                        children.push(Some((op, ids)));
1283                    }
1284
1285                    let mut dim_sizes: HashMap<char, usize> = HashMap::new();
1286                    for child_opt in &children {
1287                        if let Some((op, ids)) = child_opt {
1288                            for (j, &id) in ids.iter().enumerate() {
1289                                dim_sizes.insert(id, op.dims()[j]);
1290                            }
1291                        }
1292                    }
1293
1294                    let input_ids: Vec<Vec<char>> = children
1295                        .iter()
1296                        .map(|c| c.as_ref().unwrap().1.clone())
1297                        .collect();
1298                    let code = omeco::EinCode::new(input_ids, self.output_ids.clone());
1299
1300                    let optimizer = omeco::GreedyMethod::default();
1301                    let nested = omeco::CodeOptimizer::optimize(&optimizer, &code, &dim_sizes)
1302                        .ok_or_else(|| {
1303                            crate::EinsumError::Internal(
1304                                "optimizer failed to produce a plan".into(),
1305                            )
1306                        })?;
1307
1308                    execute_nested_into(
1309                        &nested,
1310                        &mut children,
1311                        output,
1312                        &self.output_ids,
1313                        alpha,
1314                        beta,
1315                        pool,
1316                        &unified,
1317                    )?;
1318                }
1319            },
1320        }
1321
1322        Ok(())
1323    }
1324}
1325
1326// ---------------------------------------------------------------------------
1327// Tests
1328// ---------------------------------------------------------------------------
1329
1330#[cfg(test)]
1331mod tests {
1332    use super::*;
1333    use crate::parse::parse_einsum;
1334    use approx::assert_abs_diff_eq;
1335    use strided_view::{row_major_strides, StridedArray};
1336
1337    fn make_f64(dims: &[usize], data: Vec<f64>) -> EinsumOperand<'static> {
1338        let strides = row_major_strides(dims);
1339        StridedArray::from_parts(data, dims, &strides, 0)
1340            .unwrap()
1341            .into()
1342    }
1343
1344    #[test]
1345    fn test_binary_output_ids_canonical_lo_ro_batch_order() {
1346        let out = compute_binary_output_ids(&['b', 'a', 'x'], &['x', 'c', 'a'], &['b', 'c', 'a']);
1347        assert_eq!(out, vec!['b', 'c', 'a']);
1348    }
1349
1350    #[test]
1351    fn test_matmul() {
1352        let code = parse_einsum("ij,jk->ik").unwrap();
1353        let a = make_f64(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
1354        let b = make_f64(&[2, 2], vec![5.0, 6.0, 7.0, 8.0]);
1355        let result = code.evaluate(vec![a, b], None).unwrap();
1356        match result {
1357            EinsumOperand::F64(data) => {
1358                let arr = data.as_array();
1359                assert_eq!(arr.dims(), &[2, 2]);
1360                assert_abs_diff_eq!(arr.get(&[0, 0]), 19.0);
1361                assert_abs_diff_eq!(arr.get(&[0, 1]), 22.0);
1362                assert_abs_diff_eq!(arr.get(&[1, 0]), 43.0);
1363                assert_abs_diff_eq!(arr.get(&[1, 1]), 50.0);
1364            }
1365            _ => panic!("expected F64"),
1366        }
1367    }
1368
1369    #[test]
1370    fn test_nested_three_tensor() {
1371        let code = parse_einsum("(ij,jk),kl->il").unwrap();
1372        // A = [[1,0],[0,1]] (identity), B = [[1,2],[3,4]], C = [[5,6],[7,8]]
1373        let a = make_f64(&[2, 2], vec![1.0, 0.0, 0.0, 1.0]);
1374        let b = make_f64(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
1375        let c = make_f64(&[2, 2], vec![5.0, 6.0, 7.0, 8.0]);
1376        // A*B = B, B*C = [[19,22],[43,50]]
1377        let result = code.evaluate(vec![a, b, c], None).unwrap();
1378        match result {
1379            EinsumOperand::F64(data) => {
1380                let arr = data.as_array();
1381                assert_eq!(arr.dims(), &[2, 2]);
1382                assert_abs_diff_eq!(arr.get(&[0, 0]), 19.0);
1383                assert_abs_diff_eq!(arr.get(&[0, 1]), 22.0);
1384                assert_abs_diff_eq!(arr.get(&[1, 0]), 43.0);
1385                assert_abs_diff_eq!(arr.get(&[1, 1]), 50.0);
1386            }
1387            _ => panic!("expected F64"),
1388        }
1389    }
1390
1391    #[test]
1392    fn test_outer_product() {
1393        let code = parse_einsum("i,j->ij").unwrap();
1394        let a = make_f64(&[3], vec![1.0, 2.0, 3.0]);
1395        let b = make_f64(&[2], vec![10.0, 20.0]);
1396        let result = code.evaluate(vec![a, b], None).unwrap();
1397        match result {
1398            EinsumOperand::F64(data) => {
1399                let arr = data.as_array();
1400                assert_eq!(arr.dims(), &[3, 2]);
1401                assert_abs_diff_eq!(arr.get(&[0, 0]), 10.0);
1402                assert_abs_diff_eq!(arr.get(&[2, 1]), 60.0);
1403            }
1404            _ => panic!("expected F64"),
1405        }
1406    }
1407
1408    #[test]
1409    fn test_dot_product() {
1410        let code = parse_einsum("i,i->").unwrap();
1411        let a = make_f64(&[3], vec![1.0, 2.0, 3.0]);
1412        let b = make_f64(&[3], vec![4.0, 5.0, 6.0]);
1413        let result = code.evaluate(vec![a, b], None).unwrap();
1414        match result {
1415            EinsumOperand::F64(data) => {
1416                // 1*4 + 2*5 + 3*6 = 32
1417                assert_abs_diff_eq!(data.as_array().data()[0], 32.0);
1418            }
1419            _ => panic!("expected F64"),
1420        }
1421    }
1422
1423    #[test]
1424    fn test_single_tensor_permute() {
1425        let code = parse_einsum("ij->ji").unwrap();
1426        let a = make_f64(&[2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1427        let result = code.evaluate(vec![a], None).unwrap();
1428        match result {
1429            EinsumOperand::F64(data) => {
1430                let arr = data.as_array();
1431                assert_eq!(arr.dims(), &[3, 2]);
1432                assert_abs_diff_eq!(arr.get(&[0, 0]), 1.0);
1433                assert_abs_diff_eq!(arr.get(&[0, 1]), 4.0);
1434            }
1435            _ => panic!("expected F64"),
1436        }
1437    }
1438
1439    #[test]
1440    fn test_single_tensor_trace() {
1441        let code = parse_einsum("ii->").unwrap();
1442        let a = make_f64(&[3, 3], vec![1.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0]);
1443        let result = code.evaluate(vec![a], None).unwrap();
1444        match result {
1445            EinsumOperand::F64(data) => {
1446                assert_abs_diff_eq!(data.as_array().data()[0], 6.0);
1447            }
1448            _ => panic!("expected F64"),
1449        }
1450    }
1451
1452    #[test]
1453    fn test_three_tensor_flat_omeco() {
1454        // ij,jk,kl->il -- flat 3-tensor, should use omeco
1455        let code = parse_einsum("ij,jk,kl->il").unwrap();
1456        let a = make_f64(&[2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1457        let b = make_f64(&[3, 2], vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0]);
1458        // c = identity; AB = [[4,2],[10,5]], AB*I = [[4,2],[10,5]]
1459        let c = make_f64(&[2, 2], vec![1.0, 0.0, 0.0, 1.0]);
1460        let result = code.evaluate(vec![a, b, c], None).unwrap();
1461        match result {
1462            EinsumOperand::F64(data) => {
1463                let arr = data.as_array();
1464                assert_eq!(arr.dims(), &[2, 2]);
1465                assert_abs_diff_eq!(arr.get(&[0, 0]), 4.0, epsilon = 1e-10);
1466                assert_abs_diff_eq!(arr.get(&[0, 1]), 2.0, epsilon = 1e-10);
1467                assert_abs_diff_eq!(arr.get(&[1, 0]), 10.0, epsilon = 1e-10);
1468                assert_abs_diff_eq!(arr.get(&[1, 1]), 5.0, epsilon = 1e-10);
1469            }
1470            _ => panic!("expected F64"),
1471        }
1472    }
1473
1474    #[test]
1475    fn test_four_tensor_flat_omeco() {
1476        // ij,jk,kl,lm->im -- 4-tensor chain
1477        let code = parse_einsum("ij,jk,kl,lm->im").unwrap();
1478        let a = make_f64(&[2, 2], vec![1.0, 0.0, 0.0, 1.0]); // identity
1479        let b = make_f64(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
1480        let c = make_f64(&[2, 2], vec![1.0, 0.0, 0.0, 1.0]); // identity
1481        let d = make_f64(&[2, 2], vec![5.0, 6.0, 7.0, 8.0]);
1482        // I*B = B, B*I = B, B*D = [[19,22],[43,50]]
1483        let result = code.evaluate(vec![a, b, c, d], None).unwrap();
1484        match result {
1485            EinsumOperand::F64(data) => {
1486                let arr = data.as_array();
1487                assert_eq!(arr.dims(), &[2, 2]);
1488                assert_abs_diff_eq!(arr.get(&[0, 0]), 19.0, epsilon = 1e-10);
1489                assert_abs_diff_eq!(arr.get(&[1, 1]), 50.0, epsilon = 1e-10);
1490            }
1491            _ => panic!("expected F64"),
1492        }
1493    }
1494
1495    #[test]
1496    fn test_orphan_output_axis_returns_error() {
1497        let code = parse_einsum("ij,jk->iz").unwrap();
1498        let a = make_f64(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
1499        let b = make_f64(&[2, 2], vec![5.0, 6.0, 7.0, 8.0]);
1500        let err = code.evaluate(vec![a, b], None).unwrap_err();
1501        assert!(matches!(err, crate::EinsumError::OrphanOutputAxis(ref s) if s == "z"));
1502    }
1503
1504    #[test]
1505    fn test_operand_count_mismatch_too_few() {
1506        let code = parse_einsum("ij,jk->ik").unwrap();
1507        let a = make_f64(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
1508        let err = code.evaluate(vec![a], None).unwrap_err();
1509        assert!(matches!(
1510            err,
1511            crate::EinsumError::OperandCountMismatch {
1512                expected: 2,
1513                found: 1
1514            }
1515        ));
1516    }
1517
1518    #[test]
1519    fn test_operand_count_mismatch_too_many() {
1520        let code = parse_einsum("ij->ji").unwrap();
1521        let a = make_f64(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
1522        let b = make_f64(&[2, 2], vec![5.0, 6.0, 7.0, 8.0]);
1523        let err = code.evaluate(vec![a, b], None).unwrap_err();
1524        assert!(matches!(
1525            err,
1526            crate::EinsumError::OperandCountMismatch {
1527                expected: 1,
1528                found: 2
1529            }
1530        ));
1531    }
1532
1533    // -----------------------------------------------------------------------
1534    // evaluate_into tests
1535    // -----------------------------------------------------------------------
1536
1537    #[test]
1538    fn test_into_matmul() {
1539        let code = parse_einsum("ij,jk->ik").unwrap();
1540        let a = make_f64(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
1541        let b = make_f64(&[2, 2], vec![5.0, 6.0, 7.0, 8.0]);
1542        let mut c = StridedArray::<f64>::col_major(&[2, 2]);
1543        code.evaluate_into(vec![a, b], c.view_mut(), 1.0, 0.0, None)
1544            .unwrap();
1545        assert_abs_diff_eq!(c.get(&[0, 0]), 19.0);
1546        assert_abs_diff_eq!(c.get(&[0, 1]), 22.0);
1547        assert_abs_diff_eq!(c.get(&[1, 0]), 43.0);
1548        assert_abs_diff_eq!(c.get(&[1, 1]), 50.0);
1549    }
1550
1551    #[test]
1552    fn test_into_matmul_alpha_beta() {
1553        // C = 2 * A*B + 3 * C_old
1554        let code = parse_einsum("ij,jk->ik").unwrap();
1555        let a = make_f64(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
1556        let b = make_f64(&[2, 2], vec![5.0, 6.0, 7.0, 8.0]);
1557        // A*B = [[19,22],[43,50]]
1558        // C_old = [[1,1],[1,1]]
1559        // result = 2*[[19,22],[43,50]] + 3*[[1,1],[1,1]] = [[41,47],[89,103]]
1560        let mut c = StridedArray::<f64>::col_major(&[2, 2]);
1561        for v in c.data_mut().iter_mut() {
1562            *v = 1.0;
1563        }
1564        code.evaluate_into(vec![a, b], c.view_mut(), 2.0, 3.0, None)
1565            .unwrap();
1566        assert_abs_diff_eq!(c.get(&[0, 0]), 41.0, epsilon = 1e-10);
1567        assert_abs_diff_eq!(c.get(&[0, 1]), 47.0, epsilon = 1e-10);
1568        assert_abs_diff_eq!(c.get(&[1, 0]), 89.0, epsilon = 1e-10);
1569        assert_abs_diff_eq!(c.get(&[1, 1]), 103.0, epsilon = 1e-10);
1570    }
1571
1572    #[test]
1573    fn test_into_single_tensor_permute() {
1574        let code = parse_einsum("ij->ji").unwrap();
1575        let a = make_f64(&[2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1576        let mut c = StridedArray::<f64>::col_major(&[3, 2]);
1577        code.evaluate_into(vec![a], c.view_mut(), 1.0, 0.0, None)
1578            .unwrap();
1579        assert_eq!(c.dims(), &[3, 2]);
1580        assert_abs_diff_eq!(c.get(&[0, 0]), 1.0);
1581        assert_abs_diff_eq!(c.get(&[0, 1]), 4.0);
1582        assert_abs_diff_eq!(c.get(&[1, 0]), 2.0);
1583        assert_abs_diff_eq!(c.get(&[2, 1]), 6.0);
1584    }
1585
1586    #[test]
1587    fn test_into_single_tensor_trace() {
1588        let code = parse_einsum("ii->").unwrap();
1589        let a = make_f64(&[3, 3], vec![1.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0]);
1590        let mut c = StridedArray::<f64>::col_major(&[]);
1591        code.evaluate_into(vec![a], c.view_mut(), 1.0, 0.0, None)
1592            .unwrap();
1593        assert_abs_diff_eq!(c.data()[0], 6.0);
1594    }
1595
1596    #[test]
1597    fn test_into_three_tensor_omeco() {
1598        let code = parse_einsum("ij,jk,kl->il").unwrap();
1599        let a = make_f64(&[2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1600        let b = make_f64(&[3, 2], vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0]);
1601        let c_op = make_f64(&[2, 2], vec![1.0, 0.0, 0.0, 1.0]);
1602        let mut out = StridedArray::<f64>::col_major(&[2, 2]);
1603        code.evaluate_into(vec![a, b, c_op], out.view_mut(), 1.0, 0.0, None)
1604            .unwrap();
1605        assert_abs_diff_eq!(out.get(&[0, 0]), 4.0, epsilon = 1e-10);
1606        assert_abs_diff_eq!(out.get(&[0, 1]), 2.0, epsilon = 1e-10);
1607        assert_abs_diff_eq!(out.get(&[1, 0]), 10.0, epsilon = 1e-10);
1608        assert_abs_diff_eq!(out.get(&[1, 1]), 5.0, epsilon = 1e-10);
1609    }
1610
1611    #[test]
1612    fn test_into_nested() {
1613        let code = parse_einsum("(ij,jk),kl->il").unwrap();
1614        let a = make_f64(&[2, 2], vec![1.0, 0.0, 0.0, 1.0]);
1615        let b = make_f64(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
1616        let c_op = make_f64(&[2, 2], vec![5.0, 6.0, 7.0, 8.0]);
1617        let mut out = StridedArray::<f64>::col_major(&[2, 2]);
1618        code.evaluate_into(vec![a, b, c_op], out.view_mut(), 1.0, 0.0, None)
1619            .unwrap();
1620        assert_abs_diff_eq!(out.get(&[0, 0]), 19.0, epsilon = 1e-10);
1621        assert_abs_diff_eq!(out.get(&[0, 1]), 22.0, epsilon = 1e-10);
1622        assert_abs_diff_eq!(out.get(&[1, 0]), 43.0, epsilon = 1e-10);
1623        assert_abs_diff_eq!(out.get(&[1, 1]), 50.0, epsilon = 1e-10);
1624    }
1625
1626    #[test]
1627    fn test_into_dot_product() {
1628        let code = parse_einsum("i,i->").unwrap();
1629        let a = make_f64(&[3], vec![1.0, 2.0, 3.0]);
1630        let b = make_f64(&[3], vec![4.0, 5.0, 6.0]);
1631        let mut c = StridedArray::<f64>::col_major(&[]);
1632        code.evaluate_into(vec![a, b], c.view_mut(), 1.0, 0.0, None)
1633            .unwrap();
1634        assert_abs_diff_eq!(c.data()[0], 32.0);
1635    }
1636
1637    #[test]
1638    fn test_into_type_mismatch_f64_output_c64_input() {
1639        let code = parse_einsum("ij->ji").unwrap();
1640        let c64_data = vec![
1641            Complex64::new(1.0, 0.0),
1642            Complex64::new(2.0, 0.0),
1643            Complex64::new(3.0, 0.0),
1644            Complex64::new(4.0, 0.0),
1645        ];
1646        let strides = row_major_strides(&[2, 2]);
1647        let arr = StridedArray::from_parts(c64_data, &[2, 2], &strides, 0).unwrap();
1648        let op = EinsumOperand::C64(StridedData::Owned(arr));
1649        let mut out = StridedArray::<f64>::col_major(&[2, 2]);
1650        let err = code
1651            .evaluate_into(vec![op], out.view_mut(), 1.0, 0.0, None)
1652            .unwrap_err();
1653        assert!(matches!(err, crate::EinsumError::TypeMismatch { .. }));
1654    }
1655
1656    #[test]
1657    fn test_into_shape_mismatch() {
1658        let code = parse_einsum("ij,jk->ik").unwrap();
1659        let a = make_f64(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
1660        let b = make_f64(&[2, 2], vec![5.0, 6.0, 7.0, 8.0]);
1661        let mut out = StridedArray::<f64>::col_major(&[3, 3]); // wrong shape
1662        let err = code
1663            .evaluate_into(vec![a, b], out.view_mut(), 1.0, 0.0, None)
1664            .unwrap_err();
1665        assert!(matches!(
1666            err,
1667            crate::EinsumError::OutputShapeMismatch { .. }
1668        ));
1669    }
1670
1671    #[test]
1672    fn test_into_c64_output() {
1673        let code = parse_einsum("ij,jk->ik").unwrap();
1674        let c64 = |r| Complex64::new(r, 0.0);
1675        let a_data = vec![c64(1.0), c64(2.0), c64(3.0), c64(4.0)];
1676        let b_data = vec![c64(5.0), c64(6.0), c64(7.0), c64(8.0)];
1677        let strides = row_major_strides(&[2, 2]);
1678        let a = EinsumOperand::C64(StridedData::Owned(
1679            StridedArray::from_parts(a_data, &[2, 2], &strides, 0).unwrap(),
1680        ));
1681        let b = EinsumOperand::C64(StridedData::Owned(
1682            StridedArray::from_parts(b_data, &[2, 2], &strides, 0).unwrap(),
1683        ));
1684        let mut out = StridedArray::<Complex64>::col_major(&[2, 2]);
1685        code.evaluate_into(
1686            vec![a, b],
1687            out.view_mut(),
1688            c64(1.0),
1689            Complex64::zero(),
1690            None,
1691        )
1692        .unwrap();
1693        assert_abs_diff_eq!(out.get(&[0, 0]).re, 19.0);
1694        assert_abs_diff_eq!(out.get(&[1, 1]).re, 50.0);
1695    }
1696
1697    #[test]
1698    fn test_into_mixed_types_c64_output() {
1699        // f64 + c64 operands -> c64 output (f64 gets promoted)
1700        let code = parse_einsum("ij,jk->ik").unwrap();
1701        let a = make_f64(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
1702        let c64 = |r| Complex64::new(r, 0.0);
1703        let b_data = vec![c64(5.0), c64(6.0), c64(7.0), c64(8.0)];
1704        let strides = row_major_strides(&[2, 2]);
1705        let b = EinsumOperand::C64(StridedData::Owned(
1706            StridedArray::from_parts(b_data, &[2, 2], &strides, 0).unwrap(),
1707        ));
1708        let mut out = StridedArray::<Complex64>::col_major(&[2, 2]);
1709        code.evaluate_into(
1710            vec![a, b],
1711            out.view_mut(),
1712            c64(1.0),
1713            Complex64::zero(),
1714            None,
1715        )
1716        .unwrap();
1717        assert_abs_diff_eq!(out.get(&[0, 0]).re, 19.0);
1718        assert_abs_diff_eq!(out.get(&[1, 1]).re, 50.0);
1719    }
1720}