1use std::iter::Sum;
39use std::ops::AddAssign;
40
41use mdarray::{Array, Dim, DynRank, Layout, Shape, Slice, View};
42use num_complex::ComplexFloat;
43use num_traits::{MulAdd, One, Zero};
44
45pub trait Contract<T> {
47 fn matmul<'a, D0, D1, D2, La, Lb>(
58 &self,
59 a: &'a Slice<T, (D0, D1), La>,
60 b: &'a Slice<T, (D1, D2), Lb>,
61 ) -> impl MatmulBuilder<'a, T, D0, D1, D2, La, Lb>
62 where
63 D0: Dim,
64 D1: Dim,
65 D2: Dim,
66 La: Layout,
67 Lb: Layout;
68
69 fn contract_all<'a, Sa, Sb, La, Lb>(
73 &self,
74 a: &'a Slice<T, Sa, La>,
75 b: &'a Slice<T, Sb, Lb>,
76 ) -> T
77 where
78 T: 'a,
79 Sa: Shape,
80 Sb: Shape,
81 La: Layout,
82 Lb: Layout;
83
84 fn contract_n<'a, Sa, Sb, La, Lb>(
88 &self,
89 a: &'a Slice<T, Sa, La>,
90 b: &'a Slice<T, Sb, Lb>,
91 n: usize,
92 ) -> impl ContractBuilder<'a, T, Sa, Sb, La, Lb>
93 where
94 T: 'a,
95 Sa: Shape,
96 Sb: Shape,
97 La: Layout,
98 Lb: Layout;
99
100 fn contract_pairs<'a, Sa, Sb, La, Lb>(
105 &self,
106 a: &'a Slice<T, Sa, La>,
107 b: &'a Slice<T, Sb, Lb>,
108 axes_a: &'a [usize],
109 axes_b: &'a [usize],
110 ) -> impl ContractBuilder<'a, T, Sa, Sb, La, Lb>
111 where
112 T: 'a,
113 Sa: Shape,
114 Sb: Shape,
115 La: Layout,
116 Lb: Layout;
117
118 fn contract<'a, Sa, Sb, La, Lb>(
137 &self,
138 a: &'a Slice<T, Sa, La>,
139 b: &'a Slice<T, Sb, Lb>,
140 indices_a: &'a [u8],
141 indices_b: &'a [u8],
142 indices_c: &'a [u8],
143 ) -> impl ContractBuilder<'a, T, Sa, Sb, La, Lb>
144 where
145 T: 'a,
146 Sa: Shape,
147 Sb: Shape,
148 La: Layout,
149 Lb: Layout;
150}
151
152pub trait MatmulBuilder<'a, T, D0, D1, D2, La, Lb>
154where
155 T: 'a,
156 D0: Dim,
157 D1: Dim,
158 D2: Dim,
159 La: 'a + Layout,
160 Lb: 'a + Layout,
161{
162 fn scale(self, factor: T) -> Self;
164
165 fn eval(self) -> Array<T, (D0, D2)>;
167
168 fn write<Lc: Layout>(self, c: &mut Slice<T, (D0, D2), Lc>);
170
171 fn add_to<Lc: Layout>(self, c: &mut Slice<T, (D0, D2), Lc>);
173
174 fn add_to_scaled<Lc: Layout>(self, c: &mut Slice<T, (D0, D2), Lc>, beta: T);
177}
178
179pub trait ContractBuilder<'a, T, Sa, Sb, La, Lb>
181where
182 T: 'a,
183 La: Layout,
184 Lb: Layout,
185{
186 fn scale(self, factor: T) -> Self;
188
189 fn eval(self) -> Array<T, DynRank>;
191
192 fn write<Sc: Shape, Lc: Layout>(self, c: &mut Slice<T, Sc, Lc>);
194
195 fn add_to<Sc: Shape, Lc: Layout>(self, c: &mut Slice<T, Sc, Lc>);
197
198 fn add_to_scaled<Sc: Shape, Lc: Layout>(self, c: &mut Slice<T, Sc, Lc>, beta: T);
201}
202
203#[doc(hidden)]
207#[derive(Clone)]
208pub enum Axes<'a> {
209 All,
210 LastFirst { k: usize },
211 Specific(&'a [usize], &'a [usize]),
212 SpecificOwned(Vec<usize>, Vec<usize>),
213}
214
215#[doc(hidden)]
216pub struct ContractAxes {
217 pub keep_size_a: usize,
218 pub keep_size_b: usize,
219 pub contract_size: usize,
220 pub keep_shape_a: Vec<usize>,
221 pub keep_shape_b: Vec<usize>,
222 pub order_a: Vec<usize>,
223 pub order_b: Vec<usize>,
224}
225
226#[doc(hidden)]
230pub fn extract_axes<T, Sa, Sb, La, Lb>(
231 axes: Axes,
232 a: &Slice<T, Sa, La>,
233 b: &Slice<T, Sb, Lb>,
234) -> ContractAxes
235where
236 T: Zero + ComplexFloat + MulAdd<Output = T>,
237 La: Layout,
238 Lb: Layout,
239 Sa: Shape,
240 Sb: Shape,
241{
242 let rank_a = a.rank();
243 let rank_b = b.rank();
244
245 let axes_a_storage: Option<Vec<usize>>;
246 let axes_b_storage: Option<Vec<usize>>;
247
248 let (axes_a, axes_b): (&[usize], &[usize]) = match axes {
249 Axes::All => {
250 axes_a_storage = Some((0..rank_a).collect());
251 axes_b_storage = Some((0..rank_b).collect());
252 (
253 axes_a_storage.as_deref().unwrap(),
254 axes_b_storage.as_deref().unwrap(),
255 )
256 }
257 Axes::LastFirst { k } => {
258 axes_a_storage = Some(((rank_a - k)..rank_a).collect());
259 axes_b_storage = Some((0..k).collect());
260 (
261 axes_a_storage.as_deref().unwrap(),
262 axes_b_storage.as_deref().unwrap(),
263 )
264 }
265 Axes::Specific(ax_a, ax_b) => (ax_a, ax_b),
266 Axes::SpecificOwned(ax_a, ax_b) => {
267 axes_a_storage = Some(ax_a);
268 axes_b_storage = Some(ax_b);
269 (
270 axes_a_storage.as_deref().unwrap(),
271 axes_b_storage.as_deref().unwrap(),
272 )
273 }
274 };
275
276 assert_eq!(
277 axes_a.len(),
278 axes_b.len(),
279 "Axis count mismatch: {} (tensor A) vs {} (tensor B)",
280 axes_a.len(),
281 axes_b.len()
282 );
283
284 let mut contract_size = 1;
285 for (&a_ax, &b_ax) in axes_a.iter().zip(axes_b) {
286 assert_eq!(
287 a.dim(a_ax),
288 b.dim(b_ax),
289 "Dimension mismatch at contraction: A[axis {}] = {} ≠ B[axis {}] = {}",
290 a_ax,
291 a.dim(a_ax),
292 b_ax,
293 b.dim(b_ax)
294 );
295 contract_size *= a.dim(a_ax);
296 }
297
298 let mut keep_shape_a = Vec::new();
299 let mut keep_size_a = 1;
300 let mut order_a = Vec::with_capacity(rank_a);
301
302 for i in 0..rank_a {
303 if !axes_a.contains(&i) {
304 keep_shape_a.push(a.dim(i));
305 keep_size_a *= a.dim(i);
306 order_a.push(i);
307 }
308 }
309 order_a.extend_from_slice(axes_a);
310
311 let mut keep_shape_b = Vec::new();
312 let mut keep_size_b = 1;
313 let mut order_b = Vec::with_capacity(rank_b);
314 order_b.extend_from_slice(axes_b);
315
316 for i in 0..rank_b {
317 if !axes_b.contains(&i) {
318 keep_shape_b.push(b.dim(i));
319 keep_size_b *= b.dim(i);
320 order_b.push(i);
321 }
322 }
323
324 ContractAxes {
325 keep_size_a,
326 keep_size_b,
327 contract_size,
328 keep_shape_a,
329 keep_shape_b,
330 order_a,
331 order_b,
332 }
333}
334
335#[doc(hidden)]
336#[macro_export]
337macro_rules! prepare_contraction {
338 ($axes:expr, $a:expr, $b:expr) => {{
339 let ContractAxes {
340 keep_size_a,
341 keep_size_b,
342 contract_size,
343 keep_shape_a,
344 keep_shape_b,
345 order_a,
346 order_b,
347 ..
348 } = extract_axes($axes, $a, $b);
349
350 let trans_a = $a.permute(order_a).to_tensor();
351 let a_2d = trans_a.reshape([keep_size_a, contract_size]).to_tensor();
352
353 let trans_b = $b.permute(order_b).to_tensor();
354 let b_2d = trans_b.reshape([contract_size, keep_size_b]).to_tensor(); (a_2d, b_2d, keep_shape_a, keep_shape_b)
357 }};
358}
359
360#[doc(hidden)]
361#[macro_export]
362macro_rules! finish_contraction {
363 ($ab:expr, $keep_shape_a:expr, $keep_shape_b:expr) => {{
364 let mut keep_shape_a = $keep_shape_a;
365 let keep_shape_b = $keep_shape_b;
366
367 if keep_shape_a.is_empty() && keep_shape_b.is_empty() {
368 mdarray::Array::from_elem((), $ab.into_scalar()).into_dyn()
369 } else if keep_shape_a.is_empty() {
370 $ab.view(0, ..)
371 .reshape(keep_shape_b)
372 .to_owned()
373 .into_dyn()
374 .into()
375 } else if keep_shape_b.is_empty() {
376 $ab.view(.., 0)
377 .reshape(keep_shape_a)
378 .to_owned()
379 .into_dyn()
380 .into()
381 } else {
382 keep_shape_a.extend(keep_shape_b);
383 $ab.reshape(keep_shape_a).to_owned().into_dyn().into()
384 }
385 }};
386}
387
388#[doc(hidden)]
392pub fn _contract<T, La, Lb, Sa, Sb>(
393 bd: impl Contract<T>,
394 a: &Slice<T, Sa, La>,
395 b: &Slice<T, Sb, Lb>,
396 axes: Axes,
397 alpha: T,
398) -> Array<T, DynRank>
399where
400 T: Zero + ComplexFloat + MulAdd<Output = T>,
401 La: Layout,
402 Lb: Layout,
403 Sa: Shape,
404 Sb: Shape,
405{
406 if let Some(shape) = _empty_contract_shape(a, b, &axes) {
407 return Array::from_elem(shape, T::zero());
408 }
409
410 let (a_2d, b_2d, keep_shape_a, keep_shape_b) = prepare_contraction!(axes, a, b);
411
412 let ab = bd.matmul(&a_2d, &b_2d).scale(alpha).eval();
413
414 finish_contraction!(ab, keep_shape_a, keep_shape_b)
415}
416
417#[doc(hidden)]
421pub fn _empty_contract_shape<T, Sa, Sb, La, Lb>(
422 a: &Slice<T, Sa, La>,
423 b: &Slice<T, Sb, Lb>,
424 axes: &Axes<'_>,
425) -> Option<Vec<usize>>
426where
427 T: Zero + ComplexFloat + MulAdd<Output = T>,
428 Sa: Shape,
429 Sb: Shape,
430 La: Layout,
431 Lb: Layout,
432{
433 if !a.is_empty() && !b.is_empty() {
434 return None;
435 }
436 let axes = extract_axes(axes.clone(), a, b);
438 let mut shape = axes.keep_shape_a;
439 shape.extend(axes.keep_shape_b);
440 Some(shape)
441}
442
443#[doc(hidden)]
444pub fn _empty_einsum_shape<T, Sa, Sb, La, Lb>(
445 a: &Slice<T, Sa, La>,
446 b: &Slice<T, Sb, Lb>,
447 indices_a: &[u8],
448 indices_b: &[u8],
449 indices_c: &[u8],
450) -> Option<Vec<usize>>
451where
452 Sa: Shape,
453 Sb: Shape,
454 La: Layout,
455 Lb: Layout,
456{
457 if !a.is_empty() && !b.is_empty() {
458 return None;
459 }
460 assert_eq!(
461 indices_a.len(),
462 a.rank(),
463 "einsum indices_a length must match A rank"
464 );
465 assert_eq!(
466 indices_b.len(),
467 b.rank(),
468 "einsum indices_b length must match B rank"
469 );
470
471 let mut dimensions = std::collections::HashMap::new();
472 for (label, dim) in indices_a
473 .iter()
474 .copied()
475 .zip((0..a.rank()).map(|i| a.dim(i)))
476 .chain(
477 indices_b
478 .iter()
479 .copied()
480 .zip((0..b.rank()).map(|i| b.dim(i))),
481 )
482 {
483 if let Some(previous) = dimensions.insert(label, dim) {
484 assert_eq!(previous, dim, "dimension mismatch for einsum label {label}");
485 }
486 }
487 let mut seen = std::collections::HashSet::new();
488 Some(
489 indices_c
490 .iter()
491 .map(|label| {
492 assert!(seen.insert(*label), "duplicate output label {label}");
493 *dimensions
494 .get(label)
495 .expect("output label not present in inputs")
496 })
497 .collect(),
498 )
499}
500
501fn _empty_hypercontract_shape<T, Sa, Sb, La, Lb>(
502 a: &Slice<T, Sa, La>,
503 b: &Slice<T, Sb, Lb>,
504 axes_a: &[usize],
505 axes_b: &[usize],
506) -> Option<Vec<usize>>
507where
508 Sa: Shape,
509 Sb: Shape,
510 La: Layout,
511 Lb: Layout,
512{
513 if !a.is_empty() && !b.is_empty() {
514 return None;
515 }
516 let mut dimensions = std::collections::HashMap::new();
517 let mut shape = Vec::new();
518 for (label, dim) in axes_a
519 .iter()
520 .copied()
521 .zip((0..a.rank()).map(|i| a.dim(i)))
522 .chain(axes_b.iter().copied().zip((0..b.rank()).map(|i| b.dim(i))))
523 {
524 if label == FREE_AXIS {
525 shape.push(dim);
526 } else if let Some(previous) = dimensions.insert(label, dim) {
527 assert_eq!(
528 previous, dim,
529 "dimension mismatch for contraction label {label}"
530 );
531 }
532 }
533 Some(shape)
534}
535
536#[doc(hidden)]
537pub const FREE_AXIS: usize = usize::MAX;
538
539#[doc(hidden)]
545pub fn _hypercontract<T>(
546 bd: impl Contract<T>,
547 a: View<'_, T, DynRank>,
548 b: View<'_, T, DynRank>,
549 axes_a: &[usize],
550 axes_b: &[usize],
551) -> Array<T, DynRank>
552where
553 T: Copy + Zero + One + Sum + AddAssign + MulAdd<Output = T> + ComplexFloat,
554{
555 assert_eq!(
556 axes_a.len(),
557 a.rank(),
558 "hypercontract axes_a length ({}) must match A rank ({})",
559 axes_a.len(),
560 a.rank()
561 );
562 assert_eq!(
563 axes_b.len(),
564 b.rank(),
565 "hypercontract axes_b length ({}) must match B rank ({})",
566 axes_b.len(),
567 b.rank()
568 );
569
570 if let Some(shape) = _empty_hypercontract_shape(&a, &b, axes_a, axes_b) {
571 return Array::from_elem(shape, T::zero());
572 }
573
574 let edges = axes_to_hyperedges(axes_a, axes_b);
575
576 let mut a_owned: Option<Array<T, DynRank>> = None;
579 let mut b_owned: Option<Array<T, DynRank>> = None;
580
581 let mut map_a: Vec<usize> = (0..a.shape().dims().len()).collect();
584 let mut map_b: Vec<usize> = (0..b.shape().dims().len()).collect();
585
586 let mut axes_a: Vec<usize> = Vec::new();
589 let mut axes_b: Vec<usize> = Vec::new();
590
591 for edge in &edges {
592 let (idx_a, idx_b) = edge;
593
594 match (idx_a, idx_b) {
595 (Some(axes), None) => {
597 let view = a_owned
598 .as_ref()
599 .map(|o| o.expr())
600 .unwrap_or_else(|| a.clone());
601 a_owned = Some(apply_hypersum(&view, axes, &mut map_a));
602 }
603 (None, Some(axes)) => {
605 let view = b_owned
606 .as_ref()
607 .map(|o| o.expr())
608 .unwrap_or_else(|| b.clone());
609 b_owned = Some(apply_hypersum(&view, axes, &mut map_b));
610 }
611 (Some(axes_a_idx), Some(axes_b_idx)) => {
614 let ax_a = {
615 let view = a_owned
616 .as_ref()
617 .map(|o| o.expr())
618 .unwrap_or_else(|| a.clone());
619 let (arr, ax) = extract_hyperdiag(view, axes_a_idx, &mut map_a);
620 a_owned = Some(arr);
621 ax
622 };
623 let ax_b = {
624 let view = b_owned
625 .as_ref()
626 .map(|o| o.expr())
627 .unwrap_or_else(|| b.clone());
628 let (arr, ax) = extract_hyperdiag(view, axes_b_idx, &mut map_b);
629 b_owned = Some(arr);
630 ax
631 };
632 axes_a.push(ax_a);
633 axes_b.push(ax_b);
634 }
635
636 (None, None) => {}
637 }
638 }
639
640 let final_a = a_owned
642 .as_ref()
643 .map(|o| o.expr())
644 .unwrap_or_else(|| a.clone());
645 let final_b = b_owned
646 .as_ref()
647 .map(|o| o.expr())
648 .unwrap_or_else(|| b.clone());
649
650 _contract(
651 bd,
652 &final_a,
653 &final_b,
654 Axes::SpecificOwned(axes_a, axes_b),
655 T::one(),
656 )
657}
658
659#[doc(hidden)]
661pub fn hyperdiagonal<'a, T, L: Layout>(
662 a: View<'a, T, DynRank, L>,
663 axes: &[usize],
664) -> View<'a, T, DynRank, mdarray::Strided> {
665 let mut axes_sorted = axes.to_vec();
666 axes_sorted.sort_unstable();
667 axes_sorted.dedup();
668
669 let dims = a.shape().dims();
670 let rank = dims.len();
671
672 for &ax in &axes_sorted {
673 assert!(ax < rank, "axis ({ax}) out of bounds for rank {rank}");
674 }
675
676 let m = dims[*axes_sorted.first().unwrap()];
678 for &ax in &axes_sorted {
679 let n = dims[ax];
680 assert!(
681 m == n,
682 "all diagonal axes must have equal size, got {m} and {n}"
683 );
684 }
685
686 let mut out_dims: Vec<usize> = Vec::with_capacity(rank - axes_sorted.len() + 1);
688 let mut out_strides: Vec<isize> = Vec::with_capacity(rank - axes_sorted.len() + 1);
689
690 for (i, item) in dims.iter().enumerate() {
691 if axes_sorted.binary_search(&i).is_ok() {
692 continue;
693 }
694 out_dims.push(*item);
695 out_strides.push(a.stride(i));
696 }
697
698 out_dims.push(m);
700 out_strides.push(axes_sorted.iter().map(|&ax| a.stride(ax)).sum());
701
702 let mapping = mdarray::StridedMapping::new(Shape::from_dims(&out_dims), &out_strides);
703
704 unsafe { View::new_unchecked(a.as_ptr(), mapping) }
713}
714
715#[doc(hidden)]
719pub fn hypersum<T, L: Layout>(a: &View<'_, T, DynRank, L>, axes: &[usize]) -> Array<T, DynRank>
720where
721 T: std::iter::Sum + Copy + Zero + std::ops::AddAssign,
722{
723 let mut axes_sorted = axes.to_vec();
724 axes_sorted.sort_unstable();
725 axes_sorted.dedup();
726
727 let dims = a.shape().dims();
728 let rank = dims.len();
729
730 for &ax in &axes_sorted {
731 assert!(ax < rank, "axis ({ax}) out of bounds for rank {rank}");
732 }
733
734 let out_dims: Vec<usize> = (0..rank)
735 .filter(|i| axes_sorted.binary_search(i).is_err())
736 .map(|i| dims[i])
737 .collect();
738
739 let mut out = Array::from_elem(out_dims, T::zero());
740
741 for idx in odometer(dims) {
742 let out_idx: Vec<usize> = (0..rank)
743 .filter(|i| axes_sorted.binary_search(i).is_err())
744 .map(|i| idx[i])
745 .collect();
746 out[out_idx.as_slice()] += a[idx.as_slice()];
747 }
748
749 out
750}
751
752fn odometer(dims: &[usize]) -> impl Iterator<Item = Vec<usize>> + '_ {
757 let total: usize = dims.iter().product();
758 let mut idx = vec![0usize; dims.len()];
759 let mut first = true;
760
761 (0..total).map(move |_| {
762 if first {
763 first = false;
764 } else {
765 for i in (0..dims.len()).rev() {
766 idx[i] += 1;
767 if idx[i] < dims[i] {
768 break;
769 }
770 idx[i] = 0;
771 }
772 }
773 idx.clone()
774 })
775}
776
777fn update_axis_map(axis_map: &[usize], diag_axes: &[usize], ndim_after: usize) -> Vec<usize> {
779 let removed: Vec<usize> = {
780 let mut v = diag_axes.to_vec();
781 v.sort_unstable();
782 v
783 };
784 let new_diag_pos = ndim_after - 1;
785
786 axis_map
787 .iter()
788 .map(|&cur| {
789 if diag_axes.contains(&cur) {
790 new_diag_pos
791 } else {
792 let shift = removed.iter().filter(|&&r| r < cur).count();
793 cur - shift
794 }
795 })
796 .collect()
797}
798
799fn apply_hypersum<T, L: Layout>(
804 view: &View<'_, T, DynRank, L>,
805 idx: &[usize],
806 axis_map: &mut Vec<usize>,
807) -> Array<T, DynRank>
808where
809 T: Copy + Zero + std::iter::Sum + std::ops::AddAssign,
810{
811 let cur_axes: Vec<usize> = idx.iter().map(|&a| axis_map[a]).collect();
813
814 if cur_axes.len() == 1 {
815 let ax = cur_axes[0];
816 let result = hypersum(view, &[ax]);
817 for cur in axis_map.iter_mut() {
818 if *cur > ax {
819 *cur -= 1;
820 }
821 }
822 result
823 } else {
824 let diag = hyperdiagonal(view.clone(), &cur_axes);
825
826 let diag_ax = diag.shape().dims().len() - 1;
827 *axis_map = update_axis_map(axis_map, &cur_axes, diag.shape().dims().len());
828
829 let result = hypersum(&diag.into_dyn(), &[diag_ax]);
830 for cur in axis_map.iter_mut() {
831 if *cur > diag_ax {
832 *cur -= 1;
833 }
834 }
835 result
836 }
837}
838
839fn extract_hyperdiag<T, L: Layout>(
846 view: View<'_, T, DynRank, L>,
847 idx: &[usize],
848 axis_map: &mut Vec<usize>,
849) -> (Array<T, DynRank>, usize)
850where
851 T: Copy,
852{
853 let cur_axes: Vec<usize> = idx.iter().map(|&a| axis_map[a]).collect();
855
856 if cur_axes.len() == 1 {
857 let ax = cur_axes[0];
859 (view.to_owned().into(), ax)
860 } else {
861 let diag = hyperdiagonal(view, &cur_axes);
862 let diag_ax = diag.shape().dims().len() - 1;
863 *axis_map = update_axis_map(axis_map, &cur_axes, diag.shape().dims().len());
864 (diag.to_owned().into(), diag_ax)
865 }
866}
867
868fn axes_to_hyperedges(
869 axes_a: &[usize],
870 axes_b: &[usize],
871) -> Vec<(Option<Vec<usize>>, Option<Vec<usize>>)> {
872 let mut remap: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
873 let mut edges: Vec<(Option<Vec<usize>>, Option<Vec<usize>>)> = Vec::new();
874
875 for (axis, &label) in axes_a.iter().enumerate() {
876 if label == FREE_AXIS {
877 continue;
878 }
879
880 let edge = *remap.entry(label).or_insert_with(|| {
881 edges.push((None, None));
882 edges.len() - 1
883 });
884 edges[edge].0.get_or_insert_with(Vec::new).push(axis);
885 }
886
887 for (axis, &label) in axes_b.iter().enumerate() {
888 if label == FREE_AXIS {
889 continue;
890 }
891
892 let edge = *remap.entry(label).or_insert_with(|| {
893 edges.push((None, None));
894 edges.len() - 1
895 });
896 edges[edge].1.get_or_insert_with(Vec::new).push(axis);
897 }
898
899 edges
900}
901
902#[doc(hidden)]
903pub fn einsum_to_contract_axes(
904 indices_a: &[u8],
905 indices_b: &[u8],
906 indices_c: &[u8],
907) -> (Vec<usize>, Vec<usize>) {
908 let free: std::collections::HashSet<u8> = indices_c.iter().copied().collect();
909
910 let axes_a = indices_a
911 .iter()
912 .map(|&label| {
913 if free.contains(&label) {
914 FREE_AXIS
915 } else {
916 label as usize
917 }
918 })
919 .collect();
920
921 let axes_b = indices_b
922 .iter()
923 .map(|&label| {
924 if free.contains(&label) {
925 FREE_AXIS
926 } else {
927 label as usize
928 }
929 })
930 .collect();
931
932 (axes_a, axes_b)
933}