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
22pub struct BufferPool {
48 f64_pool: BTreeMap<usize, Vec<Vec<f64>>>,
49 c64_pool: BTreeMap<usize, Vec<Vec<Complex64>>>,
50}
51
52impl BufferPool {
53 pub fn new() -> Self {
55 Self {
56 f64_pool: BTreeMap::new(),
57 c64_pool: BTreeMap::new(),
58 }
59 }
60}
61
62trait PoolOps: EinsumScalar {
70 #[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 #[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 #[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 fn pool_release(pool: &mut BufferPool, data: StridedData<'_, Self>);
101}
102
103fn take_best_fit<T>(pool: &mut BTreeMap<usize, Vec<Vec<T>>>, total: usize) -> Option<Vec<T>> {
105 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 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 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
252fn 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
279fn 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 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 all_ids_ordered
307 .into_iter()
308 .filter(|id| needed_ids.contains(id))
309 .collect()
310}
311
312fn 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 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
345fn 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
367fn 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
391fn 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#[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#[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 (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
542fn 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 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
573fn 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
644fn 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 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 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
691fn 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
719fn is_permutation_only(input_ids: &[char], output_ids: &[char]) -> bool {
726 if input_ids.len() != output_ids.len() {
727 return false;
728 }
729 for (i, &id) in input_ids.iter().enumerate() {
731 if input_ids[..i].contains(&id) {
732 return false; }
734 }
735 for &id in output_ids {
737 if !input_ids.contains(&id) {
738 return false;
739 }
740 }
741 true
742}
743
744fn 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
752fn 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
808fn 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 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 eval_pair_into(
836 left, &left_ids, right, &right_ids, output, output_ids, alpha, beta,
837 )
838 }
839 omeco::NestedEinsum::Leaf { tensor_index } => {
840 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 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
868fn 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 Ok((op, ids.clone()))
904 }
905 EinsumNode::Contract { args } => {
906 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 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 if child_ids == node_output_ids {
919 return Ok((child_op, node_output_ids));
920 }
921
922 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 let result = eval_single(&child_op, &child_ids, &node_output_ids, size_dict)?;
930 Ok((result, node_output_ids))
931 }
932 2 => {
933 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 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 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 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 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 let (result, result_ids) =
991 execute_nested(&nested, &mut children, pool, size_dict)?;
992 Ok((result, result_ids))
993 }
994 }
995 }
996 }
997}
998
999impl EinsumCode {
1004 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 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 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 result_ids == self.output_ids {
1062 return Ok(result);
1063 }
1064
1065 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 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
1084fn 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
1097fn 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 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 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 let mut ops: Vec<Option<EinsumOperand<'_>>> = operands.into_iter().map(Some).collect();
1192 T::validate_operands(&ops)?;
1193
1194 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 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 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 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 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 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 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 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 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#[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 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 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 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 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 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 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]); 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]); let d = make_f64(&[2, 2], vec![5.0, 6.0, 7.0, 8.0]);
1482 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 #[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 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 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]); 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 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}