1use std::fmt::Display;
2
3use crate::graph::{Graph, LMBext, LmbIndex, LoopMomentumBasis};
4use crate::integrands::process::evaluators::SingleOrAllOrientations;
5use crate::momentum::{FourMomentum, Polarization, Rotatable, Rotation, SignOrZero, ThreeMomentum};
6
7use crate::momentum::signature::LoopSignature;
8use crate::utils::hyperdual_utils::new_constant;
9use crate::utils::{ArbPrec, F, FloatLike, Length, PrecisionUpgradable};
10use crate::{DependentMomentaConstructor, define_index, settings::runtime::kinematic::Externals};
11use bincode_trait_derive::{Decode, Encode};
12use color_eyre::Result;
13use derive_more::{From, Into};
14use eyre::eyre;
15use linnet::half_edge::HedgeGraph;
16use linnet::half_edge::involution::{EdgeIndex, EdgeVec, Orientation};
17use linnet::half_edge::nodestore::NodeStorageOps;
18use linnet::half_edge::subgraph::subset::SubSet;
19use linnet::half_edge::subgraph::{
20 Inclusion, InternalSubGraph, ModifySubSet, SuBitGraph, SubSetLike, SubSetOps,
21};
22use linnet::half_edge::typed_vec::IndexLike;
23use serde::{Deserialize, Serialize};
24use std::ops::{Add, Index, IndexMut, Sub};
25use symbolica::domains::dual::HyperDual;
26use tabled::settings::Style;
27
28use typed_index_collections::TiVec;
29
30#[derive(
31 From,
32 Into,
33 Copy,
34 Clone,
35 Hash,
36 Eq,
37 Ord,
38 PartialEq,
39 PartialOrd,
40 bincode::Encode,
41 bincode::Decode,
42 Debug,
43 Serialize,
44 Deserialize,
45)]
46pub struct LoopIndex(pub usize);
47
48define_index!(
67 pub struct ExternalIndex;
68);
69
70impl Display for LoopIndex {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 write!(f, "{}", self.0)
79 }
80}
81
82impl Display for ExternalIndex {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 write!(f, "{}", self.0)
85 }
86}
87
88#[derive(From, Into, Serialize, Deserialize, Clone, PartialEq, Debug, Encode, Decode)]
89pub struct LoopMomenta<T>(pub Vec<ThreeMomentum<T>>);
90
91impl<T: Display> Display for LoopMomenta<T> {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 let mut table_builder = tabled::builder::Builder::new();
94 for (i, mom) in self.0.iter().enumerate() {
95 let mut row = Vec::new();
96 row.push(i.to_string());
97 for m in mom {
98 row.push(m.to_string());
99 }
100 table_builder.push_record(row);
101 }
102
103 write!(f, "{}", table_builder.build().with(Style::modern_rounded()))
104 }
105}
106
107impl<T> Length for LoopMomenta<T> {
108 fn is_empty(&self) -> bool {
109 self.0.is_empty()
110 }
111
112 fn len(&self) -> usize {
113 self.0.len()
114 }
115}
116
117pub type Subspace<'a> = Option<&'a [LoopIndex]>; #[derive(Clone, Debug, bincode::Encode, bincode::Decode)]
120pub struct SubspaceData {
121 subgraph: InternalSubGraph,
122 #[bincode(with_serde)]
123 complement_subgraph: SuBitGraph,
124 lmb: LmbIndex,
125 lmb_indices: Vec<LoopIndex>,
126}
127
128impl SubspaceData {
129 pub fn solve_signature(
133 &self,
134 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
135 ) -> Vec<(EdgeIndex, Vec<(EdgeIndex, SignOrZero)>)> {
136 let lmb = self.get_lmb(all_lmbs);
137 let mut signature = self
138 .lmb_indices
139 .iter()
140 .copied()
141 .map(|loop_index| {
142 let defining_edge = lmb.loop_edges[loop_index];
143 let orientation = lmb.edge_signatures[defining_edge].internal[loop_index];
144 assert!(
145 orientation.is_sign(),
146 "a defining edge must carry its own loop momentum"
147 );
148 let mut cycle = lmb
149 .edge_signatures
150 .iter()
151 .filter_map(|(edge, signature)| {
152 let sign = signature.internal[loop_index] * orientation;
153 sign.is_sign().then_some((edge, sign))
154 })
155 .collect::<Vec<_>>();
156 cycle.sort_by_key(|(edge, _)| *edge);
157 (defining_edge, cycle)
158 })
159 .collect::<Vec<_>>();
160 signature.sort_by_key(|(edge, _)| *edge);
161 signature
162 }
163
164 #[cfg(test)]
165 pub(crate) fn has_same_embedding(&self, other: &Self) -> bool {
166 self.lmb == other.lmb
167 && self.lmb_indices == other.lmb_indices
168 && self.subgraph.filter == other.subgraph.filter
169 && self.complement_subgraph == other.complement_subgraph
170 }
171
172 pub(crate) fn has_equivalent_embedding(
173 &self,
174 other: &Self,
175 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
176 ) -> bool {
177 self.solve_signature(all_lmbs) == other.solve_signature(all_lmbs)
180 }
181
182 pub(crate) fn is_mergable_with(
183 &self,
184 other: &Self,
185 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
186 ) -> bool {
187 [(self, other), (other, self)]
191 .into_iter()
192 .all(|(active, fixed)| {
193 let lmb = active.get_lmb(all_lmbs);
194 fixed.iter_basis_edges(all_lmbs).all(|edge| {
195 active.iter_lmb_indices().all(|loop_index| {
196 lmb.edge_signatures[edge].internal[loop_index] == SignOrZero::Zero
197 })
198 })
199 })
200 }
201
202 #[cfg(test)]
208 pub(crate) fn union_in_common_parent<'a>(
209 subspaces: impl IntoIterator<Item = &'a Self>,
210 graph: &Graph,
211 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
212 ) -> Result<Self> {
213 let mut subspaces = subspaces.into_iter();
214 let first = subspaces
215 .next()
216 .ok_or_else(|| eyre!("Cannot build a common threshold subspace from an empty set"))?;
217 let parent_lmb_index = first.lmb;
218 let mut active_subgraph = first.subgraph.filter.clone();
219 let mut expected_indices = first
220 .lmb_indices
221 .iter()
222 .copied()
223 .collect::<std::collections::BTreeSet<_>>();
224
225 for subspace in subspaces {
226 if subspace.lmb != parent_lmb_index {
227 return Err(eyre!(
228 "Threshold subspaces use different parent LMBs {} and {}",
229 usize::from(parent_lmb_index),
230 usize::from(subspace.lmb),
231 ));
232 }
233 active_subgraph = active_subgraph.union(&subspace.subgraph.filter);
234 expected_indices.extend(subspace.lmb_indices.iter().copied());
235 }
236
237 let mut union =
238 Self::new_with_user_selected_lmb(active_subgraph, parent_lmb_index, graph, all_lmbs)?;
239 let resolved_indices = union
240 .lmb_indices
241 .iter()
242 .copied()
243 .collect::<std::collections::BTreeSet<_>>();
244 if !expected_indices.is_subset(&resolved_indices) {
245 return Err(eyre!(
246 "Common threshold subspace resolved active indices {:?}, which do not contain the requested union {:?}",
247 resolved_indices,
248 expected_indices,
249 ));
250 }
251 union.lmb_indices = expected_indices.into_iter().collect();
256 Ok(union)
257 }
258
259 fn cleaned_filter_pessimist<E, V, H, N: NodeStorageOps<NodeData = V>>(
260 mut filter: SuBitGraph,
261 graph: &HedgeGraph<E, V, H, N>,
262 ) -> InternalSubGraph {
263 let mut to_remove = SuBitGraph::empty(filter.size());
264
265 for i in filter.included_iter() {
266 if !filter.includes(&graph.inv(i)) {
267 to_remove.add(i);
268 }
269 }
270 filter.subtract_with(&to_remove);
271
272 InternalSubGraph {
273 filter,
274 loopcount: None,
275 }
276 }
277
278 pub(crate) fn new_with_user_selected_lmb(
279 subgraph: SuBitGraph,
280 lmb: LmbIndex,
281 graph: &Graph,
282 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
283 ) -> Result<Self> {
284 let bridges = graph.bridges_of(&subgraph);
285
286 let mut subgraph = Self::cleaned_filter_pessimist(subgraph.subtract(&bridges), graph);
287 subgraph.set_loopcount(graph);
288
289 let edges_in_subgraph = graph
290 .iter_edges_of(&subgraph.filter)
291 .map(|ed| ed.1)
292 .collect::<Vec<_>>();
293 let complement_subgraph = graph.full_filter().subtract(&subgraph.filter);
294
295 let parent_lmb = &all_lmbs[lmb];
296 let compatible_sub_lmb =
297 graph.try_compatible_sub_lmb(&subgraph, graph.full_crown(&subgraph), parent_lmb)?;
298 let mut lmb_indices = compatible_sub_lmb
299 .loop_edges
300 .iter()
301 .map(|edge_index| {
302 parent_lmb
303 .loop_edges
304 .iter_enumerated()
305 .find_map(|(loop_index, parent_edge)| {
306 (parent_edge == edge_index).then_some(loop_index)
307 })
308 .ok_or_else(|| {
309 eyre!(
310 "Compatible subgraph LMB edge {:?} is not a defining edge of parent LMB {:?}",
311 edge_index,
312 parent_lmb.loop_edges,
313 )
314 })
315 })
316 .collect::<Result<Vec<_>>>()?;
317 lmb_indices.sort();
318
319 if lmb_indices.len() != subgraph.loopcount.unwrap()
320 || compatible_sub_lmb.loop_edges.len() != subgraph.loopcount.unwrap()
321 {
322 return Err(color_eyre::eyre::eyre!(
323 "Provided loop momentum basis is not topologically compatible with subgraph, lmb_indices: {:?}, lmb_edges: {:?}, compatible_sub_lmb_edges: {:?}, subgraph loopcount: {}, edges_in_subgraph: {:?}",
324 lmb_indices,
325 parent_lmb.loop_edges,
326 compatible_sub_lmb.loop_edges,
327 subgraph.loopcount.unwrap(),
328 edges_in_subgraph,
329 ));
330 }
331
332 Ok(Self {
333 subgraph,
334 complement_subgraph,
335 lmb,
336 lmb_indices,
337 })
338 }
339
340 pub(crate) fn new_from_parent_basis_edges(
346 requested_basis_edges: &[EdgeIndex],
347 containing_subgraph: &SuBitGraph,
348 parent_lmb_index: LmbIndex,
349 graph: &Graph,
350 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
351 ) -> Result<Self> {
352 if requested_basis_edges.is_empty() {
353 return Err(eyre!(
354 "A threshold-counterterm subspace must contain at least one basis edge"
355 ));
356 }
357
358 let parent_lmb = all_lmbs.get(parent_lmb_index).ok_or_else(|| {
359 eyre!(
360 "Threshold-counterterm parent LMB {} is out of range ({} generated LMBs)",
361 usize::from(parent_lmb_index),
362 all_lmbs.len(),
363 )
364 })?;
365 let mut requested_indices = Vec::with_capacity(requested_basis_edges.len());
366 let mut seen_basis_edges = std::collections::BTreeSet::new();
367 for &edge in requested_basis_edges {
368 if !seen_basis_edges.insert(edge) {
369 return Err(eyre!(
370 "Threshold-counterterm subspace repeats basis edge {edge}"
371 ));
372 }
373 let loop_index = parent_lmb
374 .loop_edges
375 .iter_enumerated()
376 .find_map(|(loop_index, &parent_edge)| {
377 (parent_edge == edge).then_some(loop_index)
378 })
379 .ok_or_else(|| {
380 eyre!(
381 "Threshold-counterterm subspace edge {edge} is not a defining edge of parent LMB {:?}",
382 parent_lmb.loop_edges,
383 )
384 })?;
385 requested_indices.push(loop_index);
386 }
387
388 let mut cycle_union: SuBitGraph = graph.empty_subgraph();
392 for (edge_pair, edge, _) in graph.iter_edges() {
393 if requested_indices.iter().any(|&loop_index| {
394 parent_lmb.edge_signatures[edge].internal[loop_index] != SignOrZero::Zero
395 }) {
396 cycle_union.add(edge_pair);
397 }
398 }
399 let active_subgraph = cycle_union.intersection(containing_subgraph);
400 let subspace =
401 Self::new_with_user_selected_lmb(active_subgraph, parent_lmb_index, graph, all_lmbs)?;
402
403 let resolved_basis_edges = subspace.iter_basis_edges(all_lmbs).collect::<Vec<_>>();
404 let requested_set = requested_basis_edges
405 .iter()
406 .copied()
407 .collect::<std::collections::BTreeSet<_>>();
408 let resolved_set = resolved_basis_edges
409 .iter()
410 .copied()
411 .collect::<std::collections::BTreeSet<_>>();
412 if subspace.loopcount() != requested_basis_edges.len() || resolved_set != requested_set {
413 return Err(eyre!(
414 "Requested threshold-counterterm basis {:?} resolves to basis {:?} with loop count {} in parent LMB {:?}",
415 requested_basis_edges,
416 resolved_basis_edges,
417 subspace.loopcount(),
418 parent_lmb.loop_edges,
419 ));
420 }
421
422 Ok(subspace)
423 }
424
425 #[allow(dead_code)]
427 pub(crate) fn new(
428 subgraph: SuBitGraph,
429 graph: &Graph,
430 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
431 ) -> Result<Self> {
432 let mut errors = Vec::new();
433 for (lmb_index, _) in all_lmbs.iter_enumerated() {
434 match Self::new_with_user_selected_lmb(subgraph.clone(), lmb_index, graph, all_lmbs) {
435 Ok(subspace) => return Ok(subspace),
436 Err(error) => errors.push(format!("LMB {}: {error:#}", usize::from(lmb_index))),
437 }
438 }
439
440 Err(eyre!(
441 "No topology-compatible loop momentum basis found for subgraph:\n{}",
442 errors.join("\n")
443 ))
444 }
445 pub(crate) fn get_lmb<'a>(
446 &self,
447 all_lmbs: &'a TiVec<LmbIndex, LoopMomentumBasis>,
448 ) -> &'a LoopMomentumBasis {
449 &all_lmbs[self.lmb]
450 }
451
452 pub(crate) fn parent_lmb_index(&self) -> LmbIndex {
453 self.lmb
454 }
455
456 pub(crate) fn does_not_contain<'a>(
457 &'a self,
458 edges: &'a [EdgeIndex],
459 graph: &'a Graph,
460 ) -> impl Iterator<Item = EdgeIndex> + 'a {
461 graph
462 .iter_edges_of(&self.complement_subgraph)
463 .map(|ed| ed.1)
464 .filter(|e| edges.contains(e))
465 }
466
467 pub(crate) fn contains<'a>(
468 &'a self,
469 edges: &'a [EdgeIndex],
470 graph: &'a Graph,
471 ) -> impl Iterator<Item = EdgeIndex> + 'a {
472 graph
473 .iter_edges_of(&self.subgraph)
474 .map(|ed| ed.1)
475 .filter(|e| edges.contains(e))
476 }
477
478 pub(crate) fn project_loop_signature<'a>(
479 &'a self,
480 signature: &'a LoopSignature,
481 ) -> impl Iterator<Item = SignOrZero> + 'a {
482 signature.iter_enumerated().map(|(loop_index, sign)| {
483 if self.lmb_indices.contains(&loop_index) {
484 *sign
485 } else {
486 SignOrZero::Zero
487 }
488 })
489 }
490
491 pub(crate) fn project_loop_signature_filtered<'a>(
492 &'a self,
493 signature: &'a LoopSignature,
494 ) -> impl Iterator<Item = SignOrZero> + 'a {
495 signature
496 .iter_enumerated()
497 .filter_map(move |(loop_index, sign)| {
498 if self.lmb_indices.contains(&loop_index) {
499 Some(*sign)
500 } else {
501 None
502 }
503 })
504 }
505
506 pub(crate) fn project_complement_loop_signature<'a>(
507 &'a self,
508 signature: &'a LoopSignature,
509 ) -> impl Iterator<Item = SignOrZero> + 'a {
510 signature.iter_enumerated().map(|(loop_index, sign)| {
511 if !self.lmb_indices.contains(&loop_index) {
512 *sign
513 } else {
514 SignOrZero::Zero
515 }
516 })
517 }
518
519 pub(crate) fn iter_lmb_indices<'a>(&'a self) -> impl Iterator<Item = LoopIndex> + 'a {
520 self.lmb_indices.iter().copied()
521 }
522
523 pub(crate) fn iter_basis_edges<'a>(
524 &'a self,
525 all_lmbs: &'a TiVec<LmbIndex, LoopMomentumBasis>,
526 ) -> impl Iterator<Item = EdgeIndex> + 'a {
527 self.lmb_indices
528 .iter()
529 .map(|&loop_index| self.get_lmb(all_lmbs).loop_edges[loop_index])
530 }
531
532 pub(crate) fn contains_loop_index(&self, loop_index: LoopIndex) -> bool {
533 self.lmb_indices.contains(&loop_index)
534 }
535
536 pub(crate) fn loopcount(&self) -> usize {
537 self.lmb_indices.len()
542 }
543
544 pub(crate) fn as_subspace_simple(&self) -> Subspace<'_> {
545 Some(self.lmb_indices.as_slice())
546 }
547}
548
549impl<T> LoopMomenta<T> {
553 pub(crate) fn iter(&self) -> std::slice::Iter<'_, ThreeMomentum<T>> {
554 self.0.iter()
555 }
556
557 pub(crate) fn first(&self) -> Option<&ThreeMomentum<T>> {
558 self.0.first()
559 }
560
561 pub(crate) fn iter_enumerated(&self) -> impl Iterator<Item = (LoopIndex, &ThreeMomentum<T>)> {
562 self.0.iter().enumerate().map(|(i, m)| (LoopIndex(i), m))
563 }
564}
565
566impl<T> IntoIterator for LoopMomenta<T> {
567 type Item = ThreeMomentum<T>;
568 type IntoIter = std::vec::IntoIter<ThreeMomentum<T>>;
569
570 fn into_iter(self) -> Self::IntoIter {
571 self.0.into_iter()
572 }
573}
574
575impl<T> FromIterator<ThreeMomentum<T>> for LoopMomenta<T> {
576 fn from_iter<I: IntoIterator<Item = ThreeMomentum<T>>>(iter: I) -> Self {
577 LoopMomenta(iter.into_iter().collect())
578 }
579}
580
581impl<T> Index<LoopIndex> for LoopMomenta<T> {
582 type Output = ThreeMomentum<T>;
583
584 fn index(&self, index: LoopIndex) -> &Self::Output {
585 &self.0[index.0]
586 }
587}
588
589impl<T> IndexMut<LoopIndex> for LoopMomenta<T> {
590 fn index_mut(&mut self, index: LoopIndex) -> &mut Self::Output {
591 &mut self.0[index.0]
592 }
593}
594
595impl<T: FloatLike> LoopMomenta<F<T>> {
596 pub(crate) fn hyper_radius_squared(&self, subspace: Subspace) -> F<T> {
597 let zero = self.0[0].px.zero();
598 match subspace {
599 None => self.iter().fold(zero, |acc, x| acc + x.norm_squared()),
600 Some(subspace) => subspace
601 .iter()
602 .fold(zero, |acc, &i| acc + self[i].norm_squared()),
603 }
604 }
605
606 pub(crate) fn rescale(&self, factor: &F<T>, subspace: Subspace) -> Self {
607 match subspace {
608 None => LoopMomenta::from_iter(self.iter().map(|k| k * factor)),
609 Some(subspace) => LoopMomenta::from_iter(self.iter_enumerated().map(|(i, k)| {
611 if subspace.contains(&i) {
612 k * factor
613 } else {
614 k.clone()
615 }
616 })),
617 }
618 }
619
620 pub(crate) fn rescale_with_hyper_dual(
621 &self,
622 factor: &HyperDual<F<T>>,
623 subspace: Subspace,
624 ) -> LoopMomenta<HyperDual<F<T>>> {
625 match subspace {
626 None => LoopMomenta::from_iter(self.iter().map(|k| {
627 ThreeMomentum::new(
628 new_constant(factor, &k.px) * factor,
629 new_constant(factor, &k.py) * factor,
630 new_constant(factor, &k.pz) * factor,
631 )
632 })),
633 Some(subspace) => LoopMomenta::from_iter(self.iter_enumerated().map(|(i, k)| {
634 if subspace.contains(&i) {
635 ThreeMomentum::new(
636 new_constant(factor, &k.px) * factor,
637 new_constant(factor, &k.py) * factor,
638 new_constant(factor, &k.pz) * factor,
639 )
640 } else {
641 ThreeMomentum::new(
642 new_constant(factor, &k.px),
643 new_constant(factor, &k.py),
644 new_constant(factor, &k.pz),
645 )
646 }
647 })),
648 }
649 }
650
651 pub(crate) fn rotate(&self, rotation: &Rotation) -> Self {
652 LoopMomenta::from_iter(self.iter().map(|k| k.rotate(rotation)))
653 }
654
655 pub(crate) fn lmb_transform(
656 &self,
657 from: &LoopMomentumBasis,
658 to: &LoopMomentumBasis,
659 externals: &ExternalThreeMomenta<F<T>>,
660 ) -> Self {
661 LoopMomenta::from_iter(
662 to.loop_edges
663 .iter()
664 .map(|e_id| from.edge_signatures[*e_id].compute_momentum(self, externals)),
665 )
666 }
667}
668
669impl<T: FloatLike> LoopMomenta<HyperDual<F<T>>> {
670 pub fn lmb_transform(
671 &self,
672 from: &LoopMomentumBasis,
673 to: &LoopMomentumBasis,
674 externals: &ExternalThreeMomenta<HyperDual<F<T>>>,
675 ) -> Self {
676 LoopMomenta::from_iter(to.loop_edges.iter().map(|e_id| {
677 from.edge_signatures[*e_id]
678 .try_compute_momentum(&self.0, &externals.raw)
679 .unwrap()
680 }))
681 }
682
683 pub fn rescale(&self, factor: &HyperDual<F<T>>, subspace: Subspace) -> Self {
684 match subspace {
685 None => LoopMomenta::from_iter(self.iter().map(|k| k * factor)),
686 Some(subspace) => LoopMomenta::from_iter(self.iter_enumerated().map(|(i, k)| {
688 if subspace.contains(&i) {
689 k * factor
690 } else {
691 k.clone()
692 }
693 })),
694 }
695 }
696}
697
698impl LoopMomenta<F<f64>> {
699 pub(crate) fn cast<T: FloatLike>(&self) -> LoopMomenta<F<T>> {
700 LoopMomenta::from_iter(self.iter().map(|m| m.map(&|x| F::from_ff64(x))))
701 }
702}
703
704impl<T: FloatLike> Sub<&LoopMomenta<F<T>>> for &LoopMomenta<F<T>> {
705 type Output = LoopMomenta<F<T>>;
706
707 fn sub(self, rhs: &LoopMomenta<F<T>>) -> Self::Output {
708 LoopMomenta::from_iter(self.iter().zip(rhs.iter()).map(|(l, r)| l - r))
709 }
710}
711
712impl<T: FloatLike> Add<&LoopMomenta<F<T>>> for &LoopMomenta<F<T>> {
713 type Output = LoopMomenta<F<T>>;
714
715 fn add(self, rhs: &LoopMomenta<F<T>>) -> Self::Output {
716 LoopMomenta::from_iter(self.iter().zip(rhs.iter()).map(|(l, r)| l + r))
717 }
718}
719
720pub type ExternalThreeMomenta<T> = TiVec<ExternalIndex, ThreeMomentum<T>>;
725pub type ExternalFourMomenta<T> = TiVec<ExternalIndex, FourMomentum<T>>;
727pub type PolarizationVectors<T> = TiVec<ExternalIndex, Polarization<T>>; fn extract_external_spatial<T: Clone>(
745 external_four_momenta: &ExternalFourMomenta<T>,
746) -> ExternalThreeMomenta<T> {
747 ExternalThreeMomenta::from_iter(external_four_momenta.iter().map(|fm| fm.spatial.clone()))
748}
749
750#[derive(Debug, Clone)]
751pub struct BareMomentumSample<T: FloatLike> {
752 pub loop_moms: LoopMomenta<F<T>>,
753 pub dual_loop_moms: Option<LoopMomenta<HyperDual<F<T>>>>,
754 pub loop_mom_cache_id: usize,
755 pub loop_mom_base_cache_id: usize,
757 pub external_moms: ExternalFourMomenta<F<T>>,
758 pub external_mom_cache_id: usize,
759 pub external_mom_base_cache_id: usize,
761 pub jacobian: F<T>,
762 pub orientation: Option<usize>,
763 pub parameterization_branch: Option<usize>,
764}
765
766#[derive(Debug, Clone)]
767pub struct MomentumSample<T: FloatLike> {
768 pub sample: BareMomentumSample<T>,
769 }
771
772impl MomentumSample<ArbPrec> {
773 pub(crate) fn materialize<T: FloatLike>(&self) -> Result<MomentumSample<T>> {
776 let sample = &self.sample;
777 if sample.dual_loop_moms.is_some() {
778 return Err(eyre!(
779 "canonical draw materialization requires scalar source momenta"
780 ));
781 }
782 let spatial = |p: &ThreeMomentum<F<ArbPrec>>| -> Result<ThreeMomentum<F<T>>> {
783 Ok(ThreeMomentum::new(
784 F::from_arb(&p.px.0)?,
785 F::from_arb(&p.py.0)?,
786 F::from_arb(&p.pz.0)?,
787 ))
788 };
789 Ok(MomentumSample {
790 sample: BareMomentumSample {
791 loop_moms: sample
792 .loop_moms
793 .iter()
794 .map(spatial)
795 .collect::<Result<_>>()?,
796 dual_loop_moms: None,
797 loop_mom_cache_id: sample.loop_mom_cache_id,
798 loop_mom_base_cache_id: sample.loop_mom_base_cache_id,
799 external_moms: sample
800 .external_moms
801 .iter()
802 .map(|p| {
803 Ok(FourMomentum {
804 temporal: crate::momentum::Energy {
805 value: F::from_arb(&p.temporal.value.0)?,
806 },
807 spatial: spatial(&p.spatial)?,
808 })
809 })
810 .collect::<Result<_>>()?,
811 external_mom_cache_id: sample.external_mom_cache_id,
812 external_mom_base_cache_id: sample.external_mom_base_cache_id,
813 jacobian: F::from_arb(&sample.jacobian.0)?,
814 orientation: sample.orientation,
815 parameterization_branch: sample.parameterization_branch,
816 },
817 })
818 }
819}
820
821impl<T: FloatLike> Display for MomentumSample<T> {
822 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
823 let mut table = tabled::builder::Builder::new();
824
825 table.push_record(["Sample"]);
826
827 table.push_record(["Loop Momenta", "p_x", "p_y", "p_z"]);
828 for (index, loop_mom) in self.loop_moms().0.iter().enumerate() {
831 table.push_record([
832 index.to_string(),
833 loop_mom.px.to_string(),
834 loop_mom.py.to_string(),
835 loop_mom.pz.to_string(),
836 ]);
837 }
838
839 table.push_record(["External Momenta", "E", "p_x", "p_y", "p_z"]);
840 for (index, external_mom) in self.external_moms().iter_enumerated() {
841 table.push_record([
842 index.to_string(),
843 external_mom.temporal.to_string(),
844 external_mom.spatial.px.to_string(),
845 external_mom.spatial.py.to_string(),
846 external_mom.spatial.pz.to_string(),
847 ]);
848 }
849
850 table.push_record(["Jacobian".into(), format!("{:+e}", self.sample.jacobian)]);
851 table.build().with(Style::rounded()).fmt(f)
852 }
853}
854
855impl<T: FloatLike> BareMomentumSample<T> {
856 #[inline(never)]
857 pub(crate) fn new(
858 loop_moms: LoopMomenta<F<T>>,
859 loop_mom_cache_id: usize,
860 external_moms: &Externals,
861 external_mom_cache_id: usize,
862 jacobian: F<T>,
863 dependent_momenta_constructor: DependentMomentaConstructor,
864 orientation: Option<usize>,
865 ) -> Result<Self> {
866 let external_moms = external_moms.get_dependent_externals(dependent_momenta_constructor)?;
867
868 Ok(Self {
869 loop_moms,
870 dual_loop_moms: None,
871 loop_mom_cache_id,
872 loop_mom_base_cache_id: loop_mom_cache_id, external_mom_cache_id,
874 external_mom_base_cache_id: external_mom_cache_id, external_moms,
876 jacobian,
877 orientation,
878 parameterization_branch: None,
879 })
880 }
881
882 pub(crate) fn one(&self) -> F<T> {
883 if let Some(f) = self.loop_moms.first() {
884 f.px.one()
885 } else if let Some(f) = self.external_moms.first() {
886 f.spatial.px.one()
887 } else {
888 panic!("No momenta in sample")
889 }
890 }
891
892 pub(crate) fn zero(&self) -> F<T> {
893 if let Some(f) = self.loop_moms.first() {
894 f.px.zero()
895 } else if let Some(f) = self.external_moms.first() {
896 f.spatial.px.zero()
897 } else {
898 panic!("No momenta in sample")
899 }
900 }
901
902 #[inline]
904 fn cast_sample<T2: FloatLike>(&self) -> BareMomentumSample<T2>
905 where
906 F<T2>: From<F<T>>,
907 {
908 BareMomentumSample {
909 loop_mom_cache_id: self.loop_mom_cache_id,
910 loop_mom_base_cache_id: self.loop_mom_base_cache_id,
911 external_mom_cache_id: self.external_mom_cache_id,
912 external_mom_base_cache_id: self.external_mom_base_cache_id,
913 loop_moms: self.loop_moms.iter().map(ThreeMomentum::cast).collect(),
914 dual_loop_moms: self
915 .dual_loop_moms
916 .as_ref()
917 .map(|_dlm| todo!("make sure the cast works if there are hyperdual momenta")),
918 external_moms: self.external_moms.iter().map(FourMomentum::cast).collect(),
919 jacobian: self.jacobian.clone().into(),
920 orientation: self.orientation,
921 parameterization_branch: self.parameterization_branch,
922 }
923 }
924
925 pub(crate) fn higher_precision(&self) -> BareMomentumSample<T::Higher>
926 where
927 T::Higher: FloatLike,
928 T::Lower: FloatLike,
929 {
930 BareMomentumSample {
931 loop_moms: self.loop_moms.iter().map(ThreeMomentum::higher).collect(),
932 dual_loop_moms: self.dual_loop_moms.as_ref().map(|dlm| {
933 LoopMomenta::from_iter(dlm.iter().map(|m| m.clone().map(&|x| x.higher())))
934 }),
935 external_moms: self
936 .external_moms
937 .iter()
938 .map(FourMomentum::higher)
939 .collect(),
940 loop_mom_cache_id: self.loop_mom_cache_id,
941 loop_mom_base_cache_id: self.loop_mom_base_cache_id,
942 external_mom_cache_id: self.external_mom_cache_id,
943 external_mom_base_cache_id: self.external_mom_base_cache_id,
944 jacobian: self.jacobian.higher(),
945 orientation: self.orientation,
946 parameterization_branch: self.parameterization_branch,
947 }
948 }
949
950 pub(crate) fn lower_precision(&self) -> BareMomentumSample<T::Lower>
951 where
952 T::Higher: FloatLike,
953 T::Lower: FloatLike,
954 {
955 BareMomentumSample {
956 loop_moms: self.loop_moms.iter().map(ThreeMomentum::lower).collect(),
957 dual_loop_moms: self.dual_loop_moms.as_ref().map(|dlm| {
958 LoopMomenta::from_iter(dlm.iter().map(|m| m.clone().map(&|x| x.lower())))
959 }),
960 external_moms: self.external_moms.iter().map(FourMomentum::lower).collect(),
961 jacobian: self.jacobian.lower(),
962 orientation: self.orientation,
963 loop_mom_cache_id: self.loop_mom_cache_id,
964 loop_mom_base_cache_id: self.loop_mom_base_cache_id,
965 external_mom_cache_id: self.external_mom_cache_id,
966 external_mom_base_cache_id: self.external_mom_base_cache_id,
967 parameterization_branch: self.parameterization_branch,
968 }
969 }
970
971 #[inline]
972 pub(crate) fn rotate(
973 &self,
974 rotation: &Rotation,
975 loop_mom_cache_id: usize,
976 external_mom_cache_id: usize,
977 ) -> Self {
978 Self {
979 loop_moms: self.loop_moms.iter().map(|l| l.rotate(rotation)).collect(),
980 dual_loop_moms: self
981 .dual_loop_moms
982 .as_ref()
983 .map(|dlm| LoopMomenta::from_iter(dlm.iter().map(|l| l.rotate(rotation)))),
984 external_moms: self
985 .external_moms
986 .iter()
987 .map(|l| l.rotate(rotation))
988 .collect(),
989 loop_mom_cache_id,
990 loop_mom_base_cache_id: self.loop_mom_base_cache_id, external_mom_cache_id,
992 external_mom_base_cache_id: self.external_mom_base_cache_id, jacobian: self.jacobian.clone(),
994 orientation: self.orientation,
995 parameterization_branch: self.parameterization_branch,
996 }
997 }
998
999 #[inline]
1000 #[allow(dead_code)]
1001 pub(crate) fn rescaled_loop_momenta(&self, factor: &F<T>, subspace: Subspace) -> Self {
1002 Self {
1003 loop_moms: self.loop_moms.rescale(factor, subspace),
1004 dual_loop_moms: self
1005 .dual_loop_moms
1006 .as_ref()
1007 .map(|dlm| dlm.rescale(&new_constant(&dlm[LoopIndex(0)].px, factor), subspace)),
1008 loop_mom_cache_id: self.loop_mom_cache_id + 1,
1009 loop_mom_base_cache_id: self.loop_mom_base_cache_id, external_moms: self.external_moms.clone(),
1011 external_mom_cache_id: self.external_mom_cache_id,
1012 external_mom_base_cache_id: self.external_mom_base_cache_id, jacobian: self.jacobian.clone(),
1014 orientation: self.orientation,
1015 parameterization_branch: self.parameterization_branch,
1016 }
1017 }
1018
1019 #[inline]
1020 pub(crate) fn lmb_transform(&self, from: &LoopMomentumBasis, to: &LoopMomentumBasis) -> Self {
1021 Self {
1022 loop_moms: self.loop_moms.lmb_transform(
1023 from,
1024 to,
1025 &extract_external_spatial(&self.external_moms),
1026 ),
1027 dual_loop_moms: self.dual_loop_moms.as_ref().map(|dlm| {
1028 let dual_externals = self
1029 .external_moms
1030 .iter()
1031 .map(|four_mom| {
1032 ThreeMomentum::new(
1033 new_constant(&dlm[LoopIndex(0)].px, &four_mom.spatial.px),
1034 new_constant(&dlm[LoopIndex(0)].px, &four_mom.spatial.py),
1035 new_constant(&dlm[LoopIndex(0)].px, &four_mom.spatial.pz),
1036 )
1037 })
1038 .collect();
1039 dlm.lmb_transform(from, to, &dual_externals)
1040 }),
1041 loop_mom_cache_id: self.loop_mom_cache_id + 1,
1042 loop_mom_base_cache_id: self.loop_mom_base_cache_id, external_moms: self.external_moms.clone(),
1044 external_mom_cache_id: self.external_mom_cache_id,
1045 external_mom_base_cache_id: self.external_mom_base_cache_id, jacobian: self.jacobian.clone(),
1047 orientation: self.orientation,
1048 parameterization_branch: self.parameterization_branch,
1049 }
1050 }
1051}
1052
1053impl<T: FloatLike> MomentumSample<T> {
1054 pub(crate) fn orientations<'a, OID: From<usize> + Copy + IndexLike>(
1055 &self,
1056 filter: &'a SubSet<OID>,
1057 orientations: &'a TiVec<OID, EdgeVec<Orientation>>,
1058 ) -> SingleOrAllOrientations<'a, OID>
1059 where
1060 usize: From<OID>,
1061 {
1062 if let Some(o) = self.sample.orientation {
1063 let id = if filter.is_full() {
1064 OID::from(o)
1065 } else {
1066 filter
1067 .included_iter()
1068 .nth(o)
1069 .unwrap_or_else(|| {
1070 panic!(
1071 "sampled orientation index {o} must resolve within the filtered orientation subset"
1072 )
1073 })
1074 };
1075 SingleOrAllOrientations::Single {
1076 id,
1077 orientation: &orientations[id],
1078 }
1079 } else {
1080 SingleOrAllOrientations::All {
1081 all: orientations,
1082 filter,
1083 }
1084 }
1085 }
1086
1087 pub(crate) fn loop_moms(&self) -> &LoopMomenta<F<T>> {
1096 &self.sample.loop_moms
1097 }
1098
1099 pub(crate) fn external_moms(&self) -> &ExternalFourMomenta<F<T>> {
1100 &self.sample.external_moms
1101 }
1102
1103 pub(crate) fn jacobian(&self) -> F<T> {
1104 self.sample.jacobian.clone()
1105 }
1106
1107 pub(crate) fn new(
1108 loop_moms: LoopMomenta<F<T>>,
1109 loop_mom_cache_id: usize,
1110 external_moms: &Externals,
1111 external_mom_cache_id: usize,
1112 jacobian: F<T>,
1113 dependent_momenta_constructor: DependentMomentaConstructor,
1114 orientation: Option<usize>,
1115 ) -> Result<Self> {
1116 Ok(Self {
1118 sample: BareMomentumSample::new(
1119 loop_moms,
1120 loop_mom_cache_id,
1121 external_moms,
1122 external_mom_cache_id,
1123 jacobian,
1124 dependent_momenta_constructor,
1125 orientation,
1126 )?,
1127 })
1129 }
1130
1131 pub(crate) fn one(&self) -> F<T> {
1132 self.sample.one()
1133 }
1134
1135 pub(crate) fn zero(&self) -> F<T> {
1136 self.sample.zero()
1137 }
1138
1139 #[inline]
1141 pub(crate) fn cast_sample<T2: FloatLike>(&self) -> MomentumSample<T2>
1142 where
1143 F<T2>: From<F<T>>,
1144 {
1145 MomentumSample {
1146 sample: self.sample.cast_sample(),
1147 }
1148 }
1149
1150 pub(crate) fn higher_precision(&self) -> MomentumSample<T::Higher>
1151 where
1152 T::Higher: FloatLike + Default,
1153 T::Lower: FloatLike + Default,
1154 {
1155 MomentumSample {
1156 sample: self.sample.higher_precision(),
1157 }
1158 }
1159
1160 pub(crate) fn lower_precision(&self) -> MomentumSample<T::Lower>
1161 where
1162 T::Lower: FloatLike + Default,
1163 T::Higher: FloatLike + Default,
1164 {
1165 MomentumSample {
1166 sample: self.sample.lower_precision(),
1167 }
1168 }
1169
1170 #[inline]
1171 pub(crate) fn rotate(
1172 &self,
1173 rotation: &Rotation,
1174 loop_mom_cache_id: usize,
1175 external_mom_cache_id: usize,
1176 ) -> Self {
1177 Self {
1178 sample: self
1179 .sample
1180 .rotate(rotation, loop_mom_cache_id, external_mom_cache_id),
1181 }
1182 }
1183
1184 #[allow(dead_code)]
1185 #[inline]
1186 pub(crate) fn rescaled_loop_momenta(&self, factor: &F<T>, subspace: Subspace) -> Self {
1187 Self {
1188 sample: self.sample.rescaled_loop_momenta(factor, subspace),
1189 }
1190 }
1191
1192 #[inline]
1193 pub(crate) fn lmb_transform(&self, from: &LoopMomentumBasis, to: &LoopMomentumBasis) -> Self {
1194 Self {
1195 sample: self.sample.lmb_transform(from, to),
1196 }
1197 }
1198}
1199
1200#[cfg(test)]
1201mod subspace_tests {
1202 use super::*;
1203 use crate::{
1204 dot,
1205 graph::{lmb::LMBwithEdges, parse::from_dot::IntoGraph},
1206 initialisation::test_initialise,
1207 };
1208 use typed_index_collections::ti_vec;
1209
1210 #[test]
1211 fn canonical_momentum_materialization_preserves_source_metadata() {
1212 let one = F::<ArbPrec>::default().one();
1213 let mut source = MomentumSample {
1214 sample: BareMomentumSample {
1215 loop_moms: LoopMomenta::from_iter([ThreeMomentum::new(
1216 one.clone(),
1217 -&one,
1218 one.zero(),
1219 )]),
1220 dual_loop_moms: None,
1221 loop_mom_cache_id: 17,
1222 loop_mom_base_cache_id: 11,
1223 external_moms: ti_vec![FourMomentum::from_args(
1224 one.from_i64(4),
1225 one.zero(),
1226 one.clone(),
1227 one.zero()
1228 )],
1229 external_mom_cache_id: 23,
1230 external_mom_base_cache_id: 19,
1231 jacobian: one.from_i64(3),
1232 orientation: Some(7),
1233 parameterization_branch: Some(2),
1234 },
1235 };
1236 fn check<T: FloatLike>(source: &MomentumSample<ArbPrec>) {
1237 let native = source.materialize::<T>().unwrap();
1238 assert_eq!(native.sample.loop_mom_cache_id, 17);
1239 assert_eq!(native.sample.loop_mom_base_cache_id, 11);
1240 assert_eq!(native.sample.external_mom_cache_id, 23);
1241 assert_eq!(native.sample.external_mom_base_cache_id, 19);
1242 assert_eq!(native.sample.orientation, Some(7));
1243 assert_eq!(native.sample.parameterization_branch, Some(2));
1244 assert_eq!(native.sample.jacobian, native.one().from_i64(3));
1245 assert_eq!(native.sample.loop_moms[LoopIndex(0)].py, -native.one());
1246 }
1247 check::<f64>(&source);
1248 check::<crate::utils::QuadFloat>(&source);
1249 check::<ArbPrec>(&source);
1250 source.sample.dual_loop_moms = Some(LoopMomenta(vec![]));
1251 assert!(
1252 source
1253 .materialize::<f64>()
1254 .unwrap_err()
1255 .to_string()
1256 .contains("scalar source")
1257 );
1258 }
1259
1260 #[test]
1261 fn selected_cycle_identity_and_radial_projection_survive_parent_changes() {
1262 test_initialise().unwrap();
1263 let graph: Graph = dot!(digraph selected_cycles {
1264 ext [style=invis]
1265 edge [num=1 mass=0]
1266 node [num=1]
1267 ext->a:0 [id=0]
1268 a->b [id=1 name=a1]
1269 b->a [id=2 name=a2]
1270 a->b [id=3 name=a3]
1271 b->c [id=4 name=b1]
1272 c->b [id=5 name=b2]
1273 b->c [id=6 name=b3]
1274 ext->c:1 [id=7]
1275 })
1276 .unwrap();
1277 let edges = ["a1", "a2", "a3", "b1", "b2", "b3"]
1278 .map(|name| graph.edge_name_to_index(name).unwrap());
1279 let parent = graph
1280 .lmb_with_loop_edges([edges[0], edges[2], edges[3], edges[5]].as_slice())
1281 .unwrap();
1282 let mut alternate = graph
1283 .lmb_with_loop_edges([edges[0], edges[2], edges[4], edges[5]].as_slice())
1284 .unwrap();
1285 let old_slot = alternate
1286 .loop_edges
1287 .iter_enumerated()
1288 .find_map(|(index, edge)| (*edge == edges[0]).then_some(index))
1289 .unwrap();
1290 alternate.swap_loops(
1291 old_slot,
1292 LoopIndex((old_slot.0 + 1) % alternate.loop_edges.len()),
1293 );
1294 let different_cycle = graph
1295 .lmb_with_loop_edges([edges[0], edges[1], edges[3], edges[5]].as_slice())
1296 .unwrap();
1297 let lmbs = ti_vec![parent, alternate, different_cycle];
1298 let subspaces = (0..3)
1299 .map(|index| {
1300 SubspaceData::new_from_parent_basis_edges(
1301 &[edges[0]],
1302 &graph.full_filter(),
1303 LmbIndex::from(index),
1304 &graph,
1305 &lmbs,
1306 )
1307 .unwrap()
1308 })
1309 .collect::<Vec<_>>();
1310 assert_ne!(subspaces[0].lmb_indices, subspaces[1].lmb_indices);
1311 assert_eq!(
1312 subspaces[0].solve_signature(&lmbs),
1313 subspaces[1].solve_signature(&lmbs)
1314 );
1315 assert!(subspaces[0].has_equivalent_embedding(&subspaces[1], &lmbs));
1316 assert_ne!(
1317 subspaces[0].solve_signature(&lmbs),
1318 subspaces[2].solve_signature(&lmbs)
1319 );
1320 assert!(!subspaces[0].has_equivalent_embedding(&subspaces[2], &lmbs));
1321 let independent = SubspaceData::new_from_parent_basis_edges(
1322 &[edges[5]],
1323 &graph.full_filter(),
1324 LmbIndex::from(1),
1325 &graph,
1326 &lmbs,
1327 )
1328 .unwrap();
1329 assert!(subspaces[0].is_mergable_with(&independent, &lmbs));
1330 assert!(!subspaces[0].is_mergable_with(&subspaces[2], &lmbs));
1331
1332 let momenta = LoopMomenta::from_iter(
1333 (0..4).map(|i| ThreeMomentum::new(F(i as f64 + 1.0), F(2.0), F(-3.0))),
1334 );
1335 let externals = [
1336 ThreeMomentum::new(F(3.0), F(2.0), F(1.0)),
1337 ThreeMomentum::new(F(-3.0), F(-2.0), F(-1.0)),
1338 ]
1339 .into_iter()
1340 .collect();
1341 let transformed = momenta.lmb_transform(
1342 &lmbs[LmbIndex::from(0)],
1343 &lmbs[LmbIndex::from(1)],
1344 &externals,
1345 );
1346 let mut centers = [momenta.clone(), momenta.clone()];
1347 for (center, subspace) in centers.iter_mut().zip(&subspaces) {
1348 for momentum in &mut center.0 {
1349 *momentum = ThreeMomentum::new(F(0.0), F(0.0), F(0.0));
1350 }
1351 center[subspace.lmb_indices[0]] = ThreeMomentum::new(F(4.0), F(-5.0), F(6.0));
1352 }
1353 let shifted = &momenta - ¢ers[0];
1354 let alternate_shifted = &transformed - ¢ers[1];
1355 assert_eq!(
1356 shifted.hyper_radius_squared(subspaces[0].as_subspace_simple()),
1357 alternate_shifted.hyper_radius_squared(subspaces[1].as_subspace_simple()),
1358 );
1359 let projected = &shifted.rescale(&F(0.37), subspaces[0].as_subspace_simple()) + ¢ers[0];
1360 let alternate_projected =
1361 &alternate_shifted.rescale(&F(0.37), subspaces[1].as_subspace_simple()) + ¢ers[1];
1362 let expected = projected.lmb_transform(
1363 &lmbs[LmbIndex::from(0)],
1364 &lmbs[LmbIndex::from(1)],
1365 &externals,
1366 );
1367 for (actual, expected) in alternate_projected.iter().zip(expected.iter()) {
1368 assert!((actual - expected).norm_squared() < F(1e-24));
1369 }
1370 }
1371
1372 #[test]
1373 fn explicit_parent_basis_edges_recover_their_fundamental_cycle_subspace() {
1374 test_initialise().unwrap();
1375 let graph: Graph = dot!(digraph explicit_basis_subspace {
1376 ext [style=invis]
1377 edge [num=1 mass=0]
1378 node [num=1]
1379 ext->v1:0 [id=0]
1380 v1->v2 [id=1]
1381 v2->v1 [id=2]
1382 v1->v2 [id=3]
1383 v2->v1 [id=4]
1384 ext->v2:1 [id=5]
1385 })
1386 .unwrap();
1387 let all_lmbs = ti_vec![graph.loop_momentum_basis.clone()];
1388 let requested = [
1389 graph.loop_momentum_basis.loop_edges[LoopIndex(0)],
1390 graph.loop_momentum_basis.loop_edges[LoopIndex(2)],
1391 ];
1392 let containing_subgraph: SuBitGraph = graph.full_filter();
1393
1394 let subspace = SubspaceData::new_from_parent_basis_edges(
1395 &requested,
1396 &containing_subgraph,
1397 LmbIndex::from(0),
1398 &graph,
1399 &all_lmbs,
1400 )
1401 .unwrap();
1402
1403 assert_eq!(subspace.loopcount(), requested.len());
1404 assert_eq!(
1405 subspace
1406 .iter_basis_edges(&all_lmbs)
1407 .collect::<std::collections::BTreeSet<_>>(),
1408 requested
1409 .into_iter()
1410 .collect::<std::collections::BTreeSet<_>>(),
1411 );
1412 let nested_subspace = SubspaceData::new_from_parent_basis_edges(
1413 &[requested[0]],
1414 &containing_subgraph,
1415 LmbIndex::from(0),
1416 &graph,
1417 &all_lmbs,
1418 )
1419 .unwrap();
1420 let common_subspace =
1421 SubspaceData::union_in_common_parent([&nested_subspace, &subspace], &graph, &all_lmbs)
1422 .unwrap();
1423 assert_eq!(
1424 common_subspace
1425 .iter_basis_edges(&all_lmbs)
1426 .collect::<std::collections::BTreeSet<_>>(),
1427 requested
1428 .into_iter()
1429 .collect::<std::collections::BTreeSet<_>>(),
1430 );
1431 let disjoint_subspace = SubspaceData::new_from_parent_basis_edges(
1432 &[requested[1]],
1433 &containing_subgraph,
1434 LmbIndex::from(0),
1435 &graph,
1436 &all_lmbs,
1437 )
1438 .unwrap();
1439 let sparse_common_subspace = SubspaceData::union_in_common_parent(
1440 [&nested_subspace, &disjoint_subspace],
1441 &graph,
1442 &all_lmbs,
1443 )
1444 .unwrap();
1445 assert_eq!(sparse_common_subspace.loopcount(), 2);
1446 assert_eq!(
1447 sparse_common_subspace
1448 .iter_basis_edges(&all_lmbs)
1449 .collect::<std::collections::BTreeSet<_>>(),
1450 requested
1451 .into_iter()
1452 .collect::<std::collections::BTreeSet<_>>(),
1453 );
1454 assert!(
1455 SubspaceData::new_from_parent_basis_edges(
1456 &[requested[0], requested[0]],
1457 &containing_subgraph,
1458 LmbIndex::from(0),
1459 &graph,
1460 &all_lmbs,
1461 )
1462 .unwrap_err()
1463 .to_string()
1464 .contains("repeats basis edge")
1465 );
1466 }
1467}