1use std::collections::{BTreeMap, BTreeSet};
2
3use bincode_trait_derive::{Decode, Encode};
4use color_eyre::eyre::{Result, eyre};
5use linnet::half_edge::involution::EdgeIndex;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use typed_index_collections::TiVec;
9
10use crate::{
11 GammaLoopContext,
12 cff::esurface::{EsurfaceID, RaisedEsurfaceGroup},
13 define_index,
14 graph::{
15 LmbIndex, LoopMomentumBasis,
16 threshold_counterterms::{
17 ThresholdCountertermCut, ThresholdCountertermMultiplier, ThresholdCountertermSpec,
18 ThresholdCountertermThreshold, ThresholdCountertermVariant,
19 },
20 },
21 momentum::sample::SubspaceData,
22};
23
24use super::{CutGroupId, CutId};
25
26define_index! {pub struct ThresholdCountertermVariantId;}
27
28#[derive(
29 Clone,
30 Copy,
31 Debug,
32 PartialEq,
33 Eq,
34 PartialOrd,
35 Ord,
36 Encode,
37 Decode,
38 Serialize,
39 Deserialize,
40 JsonSchema,
41)]
42#[serde(rename_all = "snake_case")]
43pub enum ThresholdCountertermSide {
44 Amplitude,
45 Left,
46 Right,
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, JsonSchema)]
50#[serde(rename_all = "snake_case")]
51pub enum ThresholdCountertermOrigin {
52 Explicit,
53 Autogenerated,
54}
55
56#[derive(Clone, Debug, Encode, Decode)]
58pub struct ResolvedThresholdCountertermAssociation {
59 pub cut_id: Option<CutId>,
60 pub cut_edges: Vec<EdgeIndex>,
61 pub threshold_edges: Vec<EdgeIndex>,
62 pub esurface_id: EsurfaceID,
63 pub subspace: SubspaceData,
66 pub requires_explicit_parent_lmb: bool,
69 pub eligible: bool,
70 pub origin: ThresholdCountertermOrigin,
71}
72
73#[derive(Clone, Debug, Encode, Decode)]
75#[trait_decode(trait = GammaLoopContext)]
76pub struct ResolvedThresholdCountertermVariant {
77 pub name: String,
78 pub group_id: Option<usize>,
79 pub cut_group_id: Option<CutGroupId>,
80 pub associations: Vec<ResolvedThresholdCountertermAssociation>,
81 pub side: ThresholdCountertermSide,
82 pub threshold_esurface_ids: Vec<EsurfaceID>,
83 pub raised_esurface_group: RaisedEsurfaceGroup,
84 pub requested_subspace: Option<Vec<EdgeIndex>>,
85 pub requested_parent_lmb: Option<Vec<EdgeIndex>>,
86 pub resolved_parent_lmb: Vec<EdgeIndex>,
87 pub subspace: SubspaceData,
88 pub subspace_loop_count: usize,
89 pub multiplier: Option<ThresholdCountertermMultiplier>,
90}
91
92#[derive(Clone, Debug, Default, Encode, Decode)]
93#[trait_decode(trait = GammaLoopContext)]
94pub struct ResolvedCutGroupThresholdCounterterms {
95 pub left: Vec<ThresholdCountertermVariantId>,
96 pub right: Vec<ThresholdCountertermVariantId>,
97}
98
99impl ResolvedCutGroupThresholdCounterterms {
100 pub(crate) fn iter_pairs(
101 &self,
102 ) -> impl Iterator<Item = (ThresholdCountertermVariantId, ThresholdCountertermVariantId)> + '_
103 {
104 self.left
105 .iter()
106 .copied()
107 .flat_map(|left| self.right.iter().copied().map(move |right| (left, right)))
108 }
109}
110
111#[derive(Clone, Debug, Encode, Decode)]
113#[trait_decode(trait = GammaLoopContext)]
114pub struct ResolvedThresholdCounterterms {
115 pub legacy_equivalent: bool,
116 pub variants: TiVec<ThresholdCountertermVariantId, ResolvedThresholdCountertermVariant>,
117 pub cross_section_cut_groups: TiVec<CutGroupId, ResolvedCutGroupThresholdCounterterms>,
118}
119
120#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
124pub struct ThresholdCountertermVariantStatus {
125 pub generated: bool,
126 pub active: bool,
127}
128
129#[derive(Clone, Debug, PartialEq, Eq)]
135pub struct ThresholdCountertermEvaluatorRegistration {
136 pub cut_group_id: Option<usize>,
137 pub collection_evaluator_id: usize,
138 pub expression: String,
139 pub variant_ids: Vec<usize>,
140}
141
142#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, JsonSchema)]
143pub struct ThresholdCountertermAssociationMetadata {
144 pub cut_id: Option<usize>,
145 pub cut_edges: Vec<usize>,
146 pub threshold_edges: Vec<usize>,
147 pub esurface_id: usize,
148 pub eligible: bool,
149 pub origin: ThresholdCountertermOrigin,
150}
151
152#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, JsonSchema)]
153pub struct ThresholdCountertermMultiplierMetadata {
154 pub expression: String,
155 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
156 pub function_map: BTreeMap<String, String>,
157 pub symmetrize: bool,
158 pub opaque_derivatives: bool,
159}
160
161#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, JsonSchema)]
163pub struct ThresholdCountertermVariantMetadata {
164 pub variant_id: usize,
165 pub name: String,
166 pub group_id: Option<usize>,
167 pub cut_group_id: Option<usize>,
168 pub associations: Vec<ThresholdCountertermAssociationMetadata>,
169 pub side: ThresholdCountertermSide,
170 pub threshold_esurface_ids: Vec<usize>,
171 pub requested_subspace: Option<Vec<usize>>,
172 pub resolved_subspace: Vec<usize>,
173 pub requested_parent_lmb: Option<Vec<usize>>,
174 pub resolved_parent_lmb: Vec<usize>,
175 pub subspace_loop_count: usize,
176 pub multiplier: Option<ThresholdCountertermMultiplierMetadata>,
177 pub generated: bool,
178 pub active: bool,
179}
180
181#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, JsonSchema)]
184pub struct ThresholdCountertermEvaluatorMetadata {
185 pub evaluator_id: usize,
186 pub cut_group_id: Option<usize>,
187 pub collection_evaluator_id: usize,
188 pub expression: String,
189 pub variant_ids: Vec<usize>,
190}
191
192#[derive(
194 Clone,
195 Copy,
196 Debug,
197 PartialEq,
198 Eq,
199 PartialOrd,
200 Ord,
201 Encode,
202 Decode,
203 Serialize,
204 Deserialize,
205 JsonSchema,
206)]
207#[serde(rename_all = "snake_case")]
208pub enum ThresholdCountertermComponentKind {
209 Local,
210 Integrated,
211 LocalLocal,
212 LocalIntegrated,
213 IntegratedLocal,
214 IntegratedIntegrated,
215}
216
217impl ThresholdCountertermComponentKind {
218 pub fn variant_count(self) -> usize {
219 match self {
220 Self::Local | Self::Integrated => 1,
221 Self::LocalLocal
222 | Self::LocalIntegrated
223 | Self::IntegratedLocal
224 | Self::IntegratedIntegrated => 2,
225 }
226 }
227}
228
229#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, JsonSchema)]
234pub struct ThresholdCountertermComponentMetadata {
235 pub component_id: usize,
236 pub cut_group_id: Option<usize>,
237 pub kind: ThresholdCountertermComponentKind,
238 pub variant_ids: Vec<usize>,
239 pub evaluator_ids: Vec<Option<usize>>,
240 pub sign: i8,
241}
242
243#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, Serialize, Deserialize, JsonSchema)]
248pub struct ThresholdCountertermMetadataRegistry {
249 pub graph_name: String,
250 pub variants: Vec<ThresholdCountertermVariantMetadata>,
251 pub evaluators: Vec<ThresholdCountertermEvaluatorMetadata>,
252 pub components: Vec<ThresholdCountertermComponentMetadata>,
253}
254
255impl ThresholdCountertermMetadataRegistry {
256 pub(crate) fn build(
257 graph_name: impl Into<String>,
258 resolved: &ResolvedThresholdCounterterms,
259 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
260 variant_statuses: &[ThresholdCountertermVariantStatus],
261 mut evaluator_registrations: Vec<ThresholdCountertermEvaluatorRegistration>,
262 ) -> Result<Self> {
263 if variant_statuses.len() != resolved.variants.len() {
264 return Err(eyre!(
265 "threshold-counterterm metadata has {} variant statuses for {} resolved variants",
266 variant_statuses.len(),
267 resolved.variants.len(),
268 ));
269 }
270
271 let variants: Vec<_> = resolved
272 .variants
273 .iter_enumerated()
274 .zip(variant_statuses)
275 .map(
276 |((variant_id, variant), status)| ThresholdCountertermVariantMetadata {
277 variant_id: variant_id.0,
278 name: variant.name.clone(),
279 group_id: variant.group_id,
280 cut_group_id: variant.cut_group_id.map(|id| id.0),
281 associations: variant
282 .associations
283 .iter()
284 .map(|association| ThresholdCountertermAssociationMetadata {
285 cut_id: association.cut_id.map(|id| id.0),
286 cut_edges: association.cut_edges.iter().map(|edge| edge.0).collect(),
287 threshold_edges: association
288 .threshold_edges
289 .iter()
290 .map(|edge| edge.0)
291 .collect(),
292 esurface_id: association.esurface_id.0,
293 eligible: association.eligible,
294 origin: association.origin,
295 })
296 .collect(),
297 side: variant.side,
298 threshold_esurface_ids: variant
299 .threshold_esurface_ids
300 .iter()
301 .map(|id| id.0)
302 .collect(),
303 requested_subspace: variant
304 .requested_subspace
305 .as_ref()
306 .map(|edges| edges.iter().map(|edge| edge.0).collect()),
307 resolved_subspace: variant
308 .subspace
309 .iter_basis_edges(all_lmbs)
310 .map(|edge| edge.0)
311 .collect(),
312 requested_parent_lmb: variant
313 .requested_parent_lmb
314 .as_ref()
315 .map(|edges| edges.iter().map(|edge| edge.0).collect()),
316 resolved_parent_lmb: variant
317 .resolved_parent_lmb
318 .iter()
319 .map(|edge| edge.0)
320 .collect(),
321 subspace_loop_count: variant.subspace_loop_count,
322 multiplier: variant.multiplier.as_ref().map(|multiplier| {
323 ThresholdCountertermMultiplierMetadata {
324 expression: multiplier.expression.clone(),
325 function_map: multiplier.function_map.clone(),
326 symmetrize: multiplier.symmetrize,
327 opaque_derivatives: multiplier.opaque_derivatives,
328 }
329 }),
330 generated: status.generated,
331 active: status.active,
332 },
333 )
334 .collect();
335
336 evaluator_registrations.sort_by_key(|registration| {
337 (
338 registration.cut_group_id,
339 registration.collection_evaluator_id,
340 )
341 });
342 let mut seen_local_evaluators = BTreeSet::new();
343 let mut next_local_evaluator = BTreeMap::<Option<usize>, usize>::new();
344 let mut variant_evaluators = BTreeMap::<(Option<usize>, usize), usize>::new();
345 let mut evaluators = Vec::with_capacity(evaluator_registrations.len());
346 for mut registration in evaluator_registrations {
347 let local_key = (
348 registration.cut_group_id,
349 registration.collection_evaluator_id,
350 );
351 if !seen_local_evaluators.insert(local_key) {
352 return Err(eyre!(
353 "duplicate threshold-multiplier evaluator {} in cut group {:?}",
354 registration.collection_evaluator_id,
355 registration.cut_group_id,
356 ));
357 }
358 let next = next_local_evaluator
359 .entry(registration.cut_group_id)
360 .or_default();
361 if registration.collection_evaluator_id != *next {
362 return Err(eyre!(
363 "threshold-multiplier evaluator IDs in cut group {:?} are not contiguous: expected {}, got {}",
364 registration.cut_group_id,
365 *next,
366 registration.collection_evaluator_id,
367 ));
368 }
369 *next += 1;
370
371 registration.variant_ids.sort_unstable();
372 if registration
373 .variant_ids
374 .windows(2)
375 .any(|ids| ids[0] == ids[1])
376 {
377 return Err(eyre!(
378 "threshold-multiplier evaluator {} in cut group {:?} lists a variant more than once",
379 registration.collection_evaluator_id,
380 registration.cut_group_id,
381 ));
382 }
383 let evaluator_id = evaluators.len();
384 for &variant_id in ®istration.variant_ids {
385 let variant = variants.get(variant_id).ok_or_else(|| {
386 eyre!(
387 "threshold-multiplier evaluator {} refers to missing variant {}",
388 registration.collection_evaluator_id,
389 variant_id,
390 )
391 })?;
392 if variant.cut_group_id != registration.cut_group_id {
393 return Err(eyre!(
394 "threshold-multiplier evaluator {} cut group {:?} disagrees with variant {} cut group {:?}",
395 registration.collection_evaluator_id,
396 registration.cut_group_id,
397 variant_id,
398 variant.cut_group_id,
399 ));
400 }
401 if variant.multiplier.is_none() {
402 return Err(eyre!(
403 "identity threshold-counterterm variant {} unexpectedly references multiplier evaluator {}",
404 variant_id,
405 registration.collection_evaluator_id,
406 ));
407 }
408 if variant_evaluators
409 .insert((registration.cut_group_id, variant_id), evaluator_id)
410 .is_some()
411 {
412 return Err(eyre!(
413 "threshold-counterterm variant {} has more than one evaluator in cut group {:?}",
414 variant_id,
415 registration.cut_group_id,
416 ));
417 }
418 }
419 evaluators.push(ThresholdCountertermEvaluatorMetadata {
420 evaluator_id,
421 cut_group_id: registration.cut_group_id,
422 collection_evaluator_id: registration.collection_evaluator_id,
423 expression: registration.expression,
424 variant_ids: registration.variant_ids,
425 });
426 }
427
428 for variant in &variants {
429 let has_evaluator =
430 variant_evaluators.contains_key(&(variant.cut_group_id, variant.variant_id));
431 if variant.multiplier.is_some() != has_evaluator {
432 return Err(eyre!(
433 "threshold-counterterm variant {} multiplier/evaluator metadata disagree",
434 variant.variant_id,
435 ));
436 }
437 }
438
439 let evaluator_ids = |cut_group_id: Option<usize>, variant_ids: &[usize]| {
440 variant_ids
441 .iter()
442 .map(|variant_id| {
443 variant_evaluators
444 .get(&(cut_group_id, *variant_id))
445 .copied()
446 })
447 .collect()
448 };
449 let mut components = Vec::new();
450 let mut push_component = |cut_group_id, kind, variant_ids: Vec<usize>, sign| {
451 let component_id = components.len();
452 let component_evaluator_ids = evaluator_ids(cut_group_id, &variant_ids);
453 components.push(ThresholdCountertermComponentMetadata {
454 component_id,
455 cut_group_id,
456 kind,
457 variant_ids,
458 evaluator_ids: component_evaluator_ids,
459 sign,
460 });
461 };
462
463 let is_amplitude = variants
464 .iter()
465 .all(|variant| variant.side == ThresholdCountertermSide::Amplitude);
466 if is_amplitude {
467 for variant in &variants {
468 push_component(
469 None,
470 ThresholdCountertermComponentKind::Local,
471 vec![variant.variant_id],
472 -1,
473 );
474 push_component(
475 None,
476 ThresholdCountertermComponentKind::Integrated,
477 vec![variant.variant_id],
478 -1,
479 );
480 }
481 } else {
482 if variants
483 .iter()
484 .any(|variant| variant.side == ThresholdCountertermSide::Amplitude)
485 {
486 return Err(eyre!(
487 "threshold-counterterm metadata cannot mix amplitude and LU variants"
488 ));
489 }
490 for (cut_group_id, group) in resolved.cross_section_cut_groups.iter_enumerated() {
491 let cut_group = Some(cut_group_id.0);
492 for variant_id in group.left.iter().chain(&group.right) {
493 push_component(
494 cut_group,
495 ThresholdCountertermComponentKind::Local,
496 vec![variant_id.0],
497 -1,
498 );
499 push_component(
500 cut_group,
501 ThresholdCountertermComponentKind::Integrated,
502 vec![variant_id.0],
503 -1,
504 );
505 }
506 for (left, right) in group.iter_pairs() {
507 let pair = vec![left.0, right.0];
508 for kind in [
509 ThresholdCountertermComponentKind::LocalLocal,
510 ThresholdCountertermComponentKind::LocalIntegrated,
511 ThresholdCountertermComponentKind::IntegratedLocal,
512 ThresholdCountertermComponentKind::IntegratedIntegrated,
513 ] {
514 push_component(cut_group, kind, pair.clone(), 1);
515 }
516 }
517 }
518 }
519
520 let registry = Self {
521 graph_name: graph_name.into(),
522 variants,
523 evaluators,
524 components,
525 };
526 registry.validate()?;
527 Ok(registry)
528 }
529
530 pub(crate) fn component_id(
532 &self,
533 cut_group_id: Option<CutGroupId>,
534 kind: ThresholdCountertermComponentKind,
535 variant_ids: &[ThresholdCountertermVariantId],
536 ) -> Result<usize> {
537 self.components
538 .iter()
539 .find(|component| {
540 component.cut_group_id == cut_group_id.map(|id| id.0)
541 && component.kind == kind
542 && component.variant_ids.len() == variant_ids.len()
543 && component
544 .variant_ids
545 .iter()
546 .zip(variant_ids)
547 .all(|(stored, requested)| *stored == requested.0)
548 })
549 .map(|component| component.component_id)
550 .ok_or_else(|| {
551 eyre!(
552 "threshold-counterterm metadata has no {:?} component for cut group {:?} and variants {:?}",
553 kind,
554 cut_group_id.map(|id| id.0),
555 variant_ids.iter().map(|id| id.0).collect::<Vec<_>>(),
556 )
557 })
558 }
559
560 pub(crate) fn lu_variant_ids(
566 &self,
567 cut_group_id: CutGroupId,
568 side: ThresholdCountertermSide,
569 ) -> Result<Vec<ThresholdCountertermVariantId>> {
570 if side == ThresholdCountertermSide::Amplitude {
571 return Err(eyre!(
572 "amplitude threshold variants do not belong to LU cut group {}",
573 cut_group_id.0,
574 ));
575 }
576
577 Ok(self
578 .components
579 .iter()
580 .filter(|component| {
581 component.cut_group_id == Some(cut_group_id.0)
582 && component.kind == ThresholdCountertermComponentKind::Local
583 })
584 .filter_map(|component| {
585 let variant_id = *component.variant_ids.first()?;
586 (self.variants.get(variant_id)?.side == side)
587 .then_some(ThresholdCountertermVariantId(variant_id))
588 })
589 .collect())
590 }
591
592 pub fn validate(&self) -> Result<()> {
593 for (variant_id, variant) in self.variants.iter().enumerate() {
594 if variant.variant_id != variant_id {
595 return Err(eyre!(
596 "threshold-counterterm variant metadata ID {} is stored at index {}",
597 variant.variant_id,
598 variant_id,
599 ));
600 }
601 }
602 for (evaluator_id, evaluator) in self.evaluators.iter().enumerate() {
603 if evaluator.evaluator_id != evaluator_id {
604 return Err(eyre!(
605 "threshold-counterterm evaluator metadata ID {} is stored at index {}",
606 evaluator.evaluator_id,
607 evaluator_id,
608 ));
609 }
610 if evaluator
611 .variant_ids
612 .iter()
613 .any(|variant_id| *variant_id >= self.variants.len())
614 {
615 return Err(eyre!(
616 "threshold-counterterm evaluator {} refers to a missing variant",
617 evaluator_id,
618 ));
619 }
620 }
621 for (component_id, component) in self.components.iter().enumerate() {
622 if component.component_id != component_id {
623 return Err(eyre!(
624 "threshold-counterterm component metadata ID {} is stored at index {}",
625 component.component_id,
626 component_id,
627 ));
628 }
629 let expected_variants = component.kind.variant_count();
630 if component.variant_ids.len() != expected_variants
631 || component.evaluator_ids.len() != expected_variants
632 {
633 return Err(eyre!(
634 "threshold-counterterm component {} kind {:?} requires {} aligned variant/evaluator references",
635 component_id,
636 component.kind,
637 expected_variants,
638 ));
639 }
640 for (&variant_id, &evaluator_id) in
641 component.variant_ids.iter().zip(&component.evaluator_ids)
642 {
643 let variant = self.variants.get(variant_id).ok_or_else(|| {
644 eyre!(
645 "threshold-counterterm component {} refers to missing variant {}",
646 component_id,
647 variant_id,
648 )
649 })?;
650 if variant.cut_group_id != component.cut_group_id {
651 return Err(eyre!(
652 "threshold-counterterm component {} and variant {} have different cut groups",
653 component_id,
654 variant_id,
655 ));
656 }
657 match evaluator_id {
658 Some(evaluator_id) => {
659 let evaluator = self.evaluators.get(evaluator_id).ok_or_else(|| {
660 eyre!(
661 "threshold-counterterm component {} refers to missing evaluator {}",
662 component_id,
663 evaluator_id,
664 )
665 })?;
666 if evaluator.cut_group_id != component.cut_group_id
667 || !evaluator.variant_ids.contains(&variant_id)
668 {
669 return Err(eyre!(
670 "threshold-counterterm component {} evaluator {} is not registered for variant {}",
671 component_id,
672 evaluator_id,
673 variant_id,
674 ));
675 }
676 }
677 None if variant.multiplier.is_none() => {}
678 None => {
679 return Err(eyre!(
680 "threshold-counterterm component {} omits the evaluator for multiplier variant {}",
681 component_id,
682 variant_id,
683 ));
684 }
685 }
686 }
687 let expected_sign = if expected_variants == 1 { -1 } else { 1 };
688 if component.sign != expected_sign {
689 return Err(eyre!(
690 "threshold-counterterm component {} has sign {}, expected {}",
691 component_id,
692 component.sign,
693 expected_sign,
694 ));
695 }
696 }
697 Ok(())
698 }
699}
700
701impl ResolvedThresholdCounterterms {
702 pub fn materialized_spec(
708 &self,
709 raw: &ThresholdCountertermSpec,
710 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
711 ) -> ThresholdCountertermSpec {
712 let mut materialized = raw.clone();
713
714 for variant in &self.variants {
715 for association in variant
716 .associations
717 .iter()
718 .filter(|association| association.eligible)
719 {
720 let resolved_subspace = association
721 .subspace
722 .iter_basis_edges(all_lmbs)
723 .collect::<Vec<_>>();
724 let resolved_parent_lmb = association
725 .subspace
726 .get_lmb(all_lmbs)
727 .loop_edges
728 .iter()
729 .copied()
730 .collect();
731 let cut = if let Some(index) = materialized
732 .cuts
733 .iter()
734 .position(|cut| cut.edges == association.cut_edges)
735 {
736 &mut materialized.cuts[index]
737 } else {
738 materialized.cuts.push(ThresholdCountertermCut {
739 edges: association.cut_edges.clone(),
740 thresholds: Vec::new(),
741 });
742 materialized.cuts.last_mut().unwrap()
743 };
744
745 let threshold = if let Some(index) = cut
746 .thresholds
747 .iter()
748 .position(|threshold| threshold.edges == association.threshold_edges)
749 {
750 &mut cut.thresholds[index]
751 } else {
752 cut.thresholds.push(ThresholdCountertermThreshold {
753 edges: association.threshold_edges.clone(),
754 counterterms: Vec::new(),
755 });
756 cut.thresholds.last_mut().unwrap()
757 };
758
759 if let Some(declaration) = threshold.counterterms.iter_mut().find(|declaration| {
760 declaration.name.as_deref().unwrap_or("default") == variant.name
761 }) {
762 if !declaration.disable {
763 declaration.subspace.get_or_insert(resolved_subspace);
764 declaration.parent_lmb.get_or_insert(resolved_parent_lmb);
765 }
766 } else if threshold.counterterms.is_empty()
767 && association.origin == ThresholdCountertermOrigin::Autogenerated
768 {
769 threshold.counterterms.push(ThresholdCountertermVariant {
770 name: Some("default".to_string()),
771 group_id: None,
772 subspace: Some(resolved_subspace),
773 parent_lmb: Some(resolved_parent_lmb),
774 disable: false,
775 multiplier: None,
776 });
777 }
778 }
779 }
780
781 for cut in &mut materialized.cuts {
782 cut.thresholds
783 .sort_by(|left, right| left.edges.cmp(&right.edges));
784 }
785 materialized
786 .cuts
787 .sort_by(|left, right| left.edges.cmp(&right.edges));
788 materialized
789 }
790}
791
792#[cfg(test)]
793mod tests {
794 use linnet::half_edge::involution::EdgeIndex;
795 use typed_index_collections::{TiVec, ti_vec};
796
797 use crate::{
798 dot,
799 graph::{
800 Graph, LMBext, LmbIndex,
801 parse::from_dot::IntoGraph,
802 threshold_counterterms::{ThresholdCountertermMultiplier, ThresholdCountertermSpec},
803 },
804 initialisation::test_initialise,
805 momentum::sample::{LoopIndex, SubspaceData},
806 processes::CutGroupId,
807 };
808
809 use super::{
810 ResolvedCutGroupThresholdCounterterms, ResolvedThresholdCountertermAssociation,
811 ResolvedThresholdCountertermVariant, ResolvedThresholdCounterterms,
812 ThresholdCountertermComponentKind, ThresholdCountertermEvaluatorRegistration,
813 ThresholdCountertermMetadataRegistry, ThresholdCountertermOrigin, ThresholdCountertermSide,
814 ThresholdCountertermVariantId, ThresholdCountertermVariantStatus,
815 };
816
817 #[test]
818 fn cut_group_pairs_keep_duplicate_geometry_variants_distinct() {
819 let group = ResolvedCutGroupThresholdCounterterms {
820 left: vec![
821 ThresholdCountertermVariantId::from(0),
822 ThresholdCountertermVariantId::from(1),
823 ],
824 right: vec![
825 ThresholdCountertermVariantId::from(2),
826 ThresholdCountertermVariantId::from(3),
827 ],
828 };
829
830 assert_eq!(
831 group.iter_pairs().collect::<Vec<_>>(),
832 vec![
833 (
834 ThresholdCountertermVariantId::from(0),
835 ThresholdCountertermVariantId::from(2),
836 ),
837 (
838 ThresholdCountertermVariantId::from(0),
839 ThresholdCountertermVariantId::from(3),
840 ),
841 (
842 ThresholdCountertermVariantId::from(1),
843 ThresholdCountertermVariantId::from(2),
844 ),
845 (
846 ThresholdCountertermVariantId::from(1),
847 ThresholdCountertermVariantId::from(3),
848 ),
849 ]
850 );
851 }
852
853 #[test]
854 fn materialized_export_adds_only_eligible_implicit_defaults() {
855 test_initialise().unwrap();
856 let graph: Graph = dot!(digraph materialized_threshold_defaults {
857 ext [style=invis]
858 edge [num=1 mass=0]
859 node [num=1]
860 ext->v1:0 [id=0]
861 v1->v2 [id=1]
862 v2->v1 [id=2]
863 ext->v2:1 [id=3]
864 })
865 .unwrap();
866 let all_lmbs = ti_vec![graph.loop_momentum_basis.clone()];
867 let basis_edge = graph.loop_momentum_basis.loop_edges[LoopIndex(0)];
868 let subspace = SubspaceData::new_from_parent_basis_edges(
869 &[basis_edge],
870 &graph.full_filter(),
871 LmbIndex::from(0),
872 &graph,
873 &all_lmbs,
874 )
875 .unwrap();
876 let association = ResolvedThresholdCountertermAssociation {
877 cut_id: None,
878 cut_edges: Vec::new(),
879 threshold_edges: vec![EdgeIndex::from(1), EdgeIndex::from(2)],
880 esurface_id: crate::cff::esurface::EsurfaceID::from(0),
881 subspace: subspace.clone(),
882 requires_explicit_parent_lmb: false,
883 eligible: true,
884 origin: ThresholdCountertermOrigin::Autogenerated,
885 };
886 let variant = ResolvedThresholdCountertermVariant {
887 name: "default".to_string(),
888 group_id: None,
889 cut_group_id: None,
890 associations: vec![association],
891 side: ThresholdCountertermSide::Amplitude,
892 threshold_esurface_ids: vec![crate::cff::esurface::EsurfaceID::from(0)],
893 raised_esurface_group: crate::cff::esurface::RaisedEsurfaceGroup {
894 esurface_ids: vec![crate::cff::esurface::EsurfaceID::from(0)],
895 max_occurence: 1,
896 },
897 requested_subspace: None,
898 requested_parent_lmb: None,
899 resolved_parent_lmb: graph.loop_momentum_basis.loop_edges.clone().into(),
900 subspace,
901 subspace_loop_count: 1,
902 multiplier: None,
903 };
904 let resolved = ResolvedThresholdCounterterms {
905 legacy_equivalent: true,
906 variants: TiVec::from(vec![variant]),
907 cross_section_cut_groups: TiVec::new(),
908 };
909
910 let materialized =
911 resolved.materialized_spec(&ThresholdCountertermSpec::default(), &all_lmbs);
912 assert_eq!(materialized.cuts.len(), 1);
913 let threshold = &materialized.cuts[0].thresholds[0];
914 assert_eq!(threshold.counterterms.len(), 1);
915 let default = &threshold.counterterms[0];
916 assert_eq!(default.name.as_deref(), Some("default"));
917 assert_eq!(default.subspace.as_deref(), Some([basis_edge].as_slice()));
918 assert_eq!(
919 default.parent_lmb.as_deref(),
920 Some(graph.loop_momentum_basis.loop_edges.raw.as_slice())
921 );
922 assert!(default.multiplier.is_none());
923
924 let mut raw = materialized.clone();
925 raw.function_map.insert("shared(x)".into(), "x + 1".into());
926 let declaration = &mut raw.cuts[0].thresholds[0].counterterms[0];
927 declaration.subspace = None;
928 declaration.multiplier = Some(ThresholdCountertermMultiplier {
929 expression: "shared(1)".into(),
930 function_map: [("shared(x)".into(), "x + 2".into())].into(),
931 symmetrize: false,
932 opaque_derivatives: true,
933 });
934 let mut disabled = declaration.clone();
935 disabled.name = Some("disabled".into());
936 disabled.disable = true;
937 raw.cuts[0].thresholds[0].counterterms.push(disabled);
938 let mut dormant = raw.cuts[0].thresholds[0].counterterms[0].clone();
939 dormant.name = Some("dormant".into());
940 raw.cuts[0].thresholds[0].counterterms.insert(0, dormant);
941 let mut explicit = resolved.clone();
942 explicit.variants[ThresholdCountertermVariantId(0)].associations[0].origin =
943 ThresholdCountertermOrigin::Explicit;
944 explicit.variants[ThresholdCountertermVariantId(0)].multiplier =
946 Some(ThresholdCountertermMultiplier {
947 expression: "shared(1)".into(),
948 function_map: [
949 ("shared(x)".into(), "x + 2".into()),
950 ("extra()".into(), "3".into()),
951 ]
952 .into(),
953 symmetrize: false,
954 opaque_derivatives: true,
955 });
956 let mut expected = raw.clone();
957 expected.cuts[0].thresholds[0].counterterms[1].subspace = Some(vec![basis_edge]);
958 assert_eq!(explicit.materialized_spec(&raw, &all_lmbs), expected);
959 assert!(raw.cuts[0].thresholds[0].counterterms[1].subspace.is_none());
960
961 explicit.variants[ThresholdCountertermVariantId(0)].associations[0].eligible = false;
962 assert_eq!(explicit.materialized_spec(&raw, &all_lmbs), raw);
963 }
964
965 #[test]
966 fn materialized_raised_defaults_round_trip_association_local_embeddings() {
967 test_initialise().unwrap();
968 let graph: Graph = dot!(digraph raised_materialized_threshold_defaults {
969 ext [style=invis]
970 edge [num=1 mass=0]
971 node [num=1]
972 ext->v1:0 [id=0]
973 v1->v2 [id=1]
974 v2->v1 [id=2]
975 v2->v3 [id=3]
976 v3->v2 [id=4]
977 ext->v3:1 [id=5]
978 })
979 .unwrap();
980 let all_lmbs = graph.generate_loop_momentum_bases();
981 let containing_subgraph = graph.full_filter();
982 let subspaces = all_lmbs
983 .iter_enumerated()
984 .filter_map(|(lmb_index, _)| {
985 SubspaceData::new_from_parent_basis_edges(
986 &[EdgeIndex(1)],
987 &containing_subgraph,
988 lmb_index,
989 &graph,
990 &all_lmbs,
991 )
992 .ok()
993 })
994 .collect::<Vec<_>>();
995 let (implicit_subspace, rebased_subspace) = subspaces
996 .iter()
997 .enumerate()
998 .flat_map(|(left_index, left)| {
999 subspaces
1000 .iter()
1001 .skip(left_index + 1)
1002 .map(move |right| (left, right))
1003 })
1004 .find(|(left, right)| {
1005 left.parent_lmb_index() != right.parent_lmb_index()
1006 && left.has_equivalent_embedding(right, &all_lmbs)
1007 })
1008 .expect("the two-loop fixture must admit equivalent distinct parent LMBs");
1009 assert!(!implicit_subspace.has_same_embedding(rebased_subspace));
1010
1011 let association = |threshold_edges: Vec<EdgeIndex>,
1012 esurface_id: usize,
1013 subspace: &SubspaceData,
1014 requires_explicit_parent_lmb| {
1015 ResolvedThresholdCountertermAssociation {
1016 cut_id: None,
1017 cut_edges: Vec::new(),
1018 threshold_edges,
1019 esurface_id: crate::cff::esurface::EsurfaceID::from(esurface_id),
1020 subspace: subspace.clone(),
1021 requires_explicit_parent_lmb,
1022 eligible: true,
1023 origin: ThresholdCountertermOrigin::Autogenerated,
1024 }
1025 };
1026 let associations = vec![
1027 association(
1028 vec![EdgeIndex::from(1), EdgeIndex::from(2)],
1029 0,
1030 implicit_subspace,
1031 false,
1032 ),
1033 association(
1034 vec![EdgeIndex::from(2), EdgeIndex::from(3)],
1035 1,
1036 rebased_subspace,
1037 true,
1038 ),
1039 ];
1040 let resolved = ResolvedThresholdCounterterms {
1041 legacy_equivalent: true,
1042 variants: TiVec::from(vec![ResolvedThresholdCountertermVariant {
1043 name: "default".to_string(),
1044 group_id: None,
1045 cut_group_id: None,
1046 associations: associations.clone(),
1047 side: ThresholdCountertermSide::Amplitude,
1048 threshold_esurface_ids: vec![
1049 crate::cff::esurface::EsurfaceID::from(0),
1050 crate::cff::esurface::EsurfaceID::from(1),
1051 ],
1052 raised_esurface_group: crate::cff::esurface::RaisedEsurfaceGroup {
1053 esurface_ids: vec![
1054 crate::cff::esurface::EsurfaceID::from(0),
1055 crate::cff::esurface::EsurfaceID::from(1),
1056 ],
1057 max_occurence: 2,
1058 },
1059 requested_subspace: None,
1060 requested_parent_lmb: None,
1061 resolved_parent_lmb: implicit_subspace
1062 .get_lmb(&all_lmbs)
1063 .loop_edges
1064 .iter()
1065 .copied()
1066 .collect(),
1067 subspace: implicit_subspace.clone(),
1068 subspace_loop_count: implicit_subspace.loopcount(),
1069 multiplier: None,
1070 }]),
1071 cross_section_cut_groups: TiVec::new(),
1072 };
1073
1074 let materialized =
1075 resolved.materialized_spec(&ThresholdCountertermSpec::default(), &all_lmbs);
1076 let reparsed = ThresholdCountertermSpec::parse_toml(
1077 &materialized
1078 .to_toml()
1079 .expect("materialized defaults must serialize"),
1080 )
1081 .expect("materialized defaults must parse");
1082 assert_eq!(reparsed.cuts.len(), 1);
1083 assert_eq!(reparsed.cuts[0].thresholds.len(), 2);
1084
1085 for (threshold, association) in reparsed.cuts[0].thresholds.iter().zip(&associations) {
1086 let counterterm = &threshold.counterterms[0];
1087 let parent = counterterm
1088 .parent_lmb
1089 .as_ref()
1090 .expect("materialized variants must declare their full parent");
1091 let parent_lmb_index = all_lmbs
1092 .iter_enumerated()
1093 .find_map(|(lmb_index, lmb)| {
1094 lmb.loop_edges.iter().eq(parent.iter()).then_some(lmb_index)
1095 })
1096 .expect("serialized parent LMB must be generated");
1097 let round_tripped = SubspaceData::new_from_parent_basis_edges(
1098 counterterm.subspace.as_deref().unwrap(),
1099 &containing_subgraph,
1100 parent_lmb_index,
1101 &graph,
1102 &all_lmbs,
1103 )
1104 .expect("materialized association must reconstruct");
1105 assert!(round_tripped.has_same_embedding(&association.subspace));
1106 assert!(round_tripped.has_equivalent_embedding(implicit_subspace, &all_lmbs));
1107 }
1108
1109 assert_eq!(
1110 reparsed.cuts[0].thresholds[0].counterterms[0].parent_lmb,
1111 Some(implicit_subspace.get_lmb(&all_lmbs).loop_edges.raw.clone()),
1112 );
1113 assert_eq!(
1114 reparsed.cuts[0].thresholds[1].counterterms[0].parent_lmb,
1115 Some(
1116 rebased_subspace
1117 .get_lmb(&all_lmbs)
1118 .loop_edges
1119 .iter()
1120 .copied()
1121 .collect()
1122 ),
1123 );
1124
1125 let mut explicit = resolved.clone();
1126 for variant in &mut explicit.variants {
1127 for association in &mut variant.associations {
1128 association.origin = ThresholdCountertermOrigin::Explicit;
1129 }
1130 }
1131 let mut incomplete = reparsed.clone();
1132 for threshold in &mut incomplete.cuts[0].thresholds {
1133 threshold.counterterms[0].subspace = None;
1134 }
1135 assert_eq!(explicit.materialized_spec(&incomplete, &all_lmbs), reparsed);
1136 }
1137
1138 #[test]
1139 fn graph_registry_expands_components_in_stable_cartesian_order() {
1140 test_initialise().unwrap();
1141 let graph: Graph = dot!(digraph threshold_metadata_registry {
1142 ext [style=invis]
1143 edge [num=1 mass=0]
1144 node [num=1]
1145 ext->v1:0 [id=0]
1146 v1->v2 [id=1]
1147 v2->v1 [id=2]
1148 ext->v2:1 [id=3]
1149 })
1150 .unwrap();
1151 let all_lmbs = ti_vec![graph.loop_momentum_basis.clone()];
1152 let basis_edge = graph.loop_momentum_basis.loop_edges[LoopIndex(0)];
1153 let subspace = SubspaceData::new_from_parent_basis_edges(
1154 &[basis_edge],
1155 &graph.full_filter(),
1156 LmbIndex::from(0),
1157 &graph,
1158 &all_lmbs,
1159 )
1160 .unwrap();
1161 let association = ResolvedThresholdCountertermAssociation {
1162 cut_id: Some(crate::processes::CutId(0)),
1163 cut_edges: vec![EdgeIndex::from(0), EdgeIndex::from(3)],
1164 threshold_edges: vec![EdgeIndex::from(1), EdgeIndex::from(2)],
1165 esurface_id: crate::cff::esurface::EsurfaceID::from(0),
1166 subspace: subspace.clone(),
1167 requires_explicit_parent_lmb: false,
1168 eligible: true,
1169 origin: ThresholdCountertermOrigin::Explicit,
1170 };
1171 let multiplier = |expression: &str| ThresholdCountertermMultiplier {
1172 expression: expression.to_string(),
1173 function_map: Default::default(),
1174 symmetrize: false,
1175 opaque_derivatives: true,
1176 };
1177 let variant = |name: &str,
1178 side: ThresholdCountertermSide,
1179 multiplier: Option<ThresholdCountertermMultiplier>| {
1180 ResolvedThresholdCountertermVariant {
1181 name: name.to_string(),
1182 group_id: None,
1183 cut_group_id: Some(CutGroupId(0)),
1184 associations: vec![association.clone()],
1185 side,
1186 threshold_esurface_ids: vec![crate::cff::esurface::EsurfaceID::from(0)],
1187 raised_esurface_group: crate::cff::esurface::RaisedEsurfaceGroup {
1188 esurface_ids: vec![crate::cff::esurface::EsurfaceID::from(0)],
1189 max_occurence: 1,
1190 },
1191 requested_subspace: Some(vec![basis_edge]),
1192 requested_parent_lmb: None,
1193 resolved_parent_lmb: graph.loop_momentum_basis.loop_edges.clone().into(),
1194 subspace: subspace.clone(),
1195 subspace_loop_count: 1,
1196 multiplier,
1197 }
1198 };
1199 let resolved = ResolvedThresholdCounterterms {
1200 legacy_equivalent: false,
1201 variants: TiVec::from(vec![
1202 variant(
1203 "left_a",
1204 ThresholdCountertermSide::Left,
1205 Some(multiplier("f")),
1206 ),
1207 variant(
1208 "left_b",
1209 ThresholdCountertermSide::Left,
1210 Some(multiplier("f")),
1211 ),
1212 variant("right_identity", ThresholdCountertermSide::Right, None),
1213 variant(
1214 "right_g",
1215 ThresholdCountertermSide::Right,
1216 Some(multiplier("g")),
1217 ),
1218 ]),
1219 cross_section_cut_groups: ti_vec![ResolvedCutGroupThresholdCounterterms {
1220 left: vec![
1221 ThresholdCountertermVariantId(0),
1222 ThresholdCountertermVariantId(1),
1223 ],
1224 right: vec![
1225 ThresholdCountertermVariantId(2),
1226 ThresholdCountertermVariantId(3),
1227 ],
1228 }],
1229 };
1230 let statuses = vec![
1231 ThresholdCountertermVariantStatus {
1232 generated: true,
1233 active: true,
1234 };
1235 4
1236 ];
1237 let registry = ThresholdCountertermMetadataRegistry::build(
1238 "threshold_metadata_registry",
1239 &resolved,
1240 &all_lmbs,
1241 &statuses,
1242 vec![
1243 ThresholdCountertermEvaluatorRegistration {
1244 cut_group_id: Some(0),
1245 collection_evaluator_id: 0,
1246 expression: "canonical_f".to_string(),
1247 variant_ids: vec![1, 0],
1248 },
1249 ThresholdCountertermEvaluatorRegistration {
1250 cut_group_id: Some(0),
1251 collection_evaluator_id: 1,
1252 expression: "canonical_g".to_string(),
1253 variant_ids: vec![3],
1254 },
1255 ],
1256 )
1257 .unwrap();
1258
1259 assert_eq!(registry.evaluators[0].variant_ids, vec![0, 1]);
1260 assert_eq!(registry.components.len(), 24);
1261 assert_eq!(registry.components[0].variant_ids, vec![0]);
1262 assert_eq!(registry.components[7].variant_ids, vec![3]);
1263 assert_eq!(
1264 registry.components[8].kind,
1265 ThresholdCountertermComponentKind::LocalLocal
1266 );
1267 assert_eq!(registry.components[8].variant_ids, vec![0, 2]);
1268 assert_eq!(registry.components[8].evaluator_ids, vec![Some(0), None]);
1269 assert_eq!(registry.components[8].sign, 1);
1270 assert_eq!(
1271 registry.components[12].kind,
1272 ThresholdCountertermComponentKind::LocalLocal
1273 );
1274 assert_eq!(registry.components[12].variant_ids, vec![0, 3]);
1275 assert_eq!(
1276 registry.components[12].evaluator_ids,
1277 vec![Some(0), Some(1)]
1278 );
1279 assert_eq!(
1280 registry.components[23].kind,
1281 ThresholdCountertermComponentKind::IntegratedIntegrated
1282 );
1283 assert_eq!(registry.components[23].variant_ids, vec![1, 3]);
1284 assert_eq!(
1285 registry
1286 .lu_variant_ids(CutGroupId(0), ThresholdCountertermSide::Left)
1287 .unwrap(),
1288 vec![
1289 ThresholdCountertermVariantId(0),
1290 ThresholdCountertermVariantId(1),
1291 ],
1292 );
1293 assert_eq!(
1294 registry
1295 .lu_variant_ids(CutGroupId(0), ThresholdCountertermSide::Right)
1296 .unwrap(),
1297 vec![
1298 ThresholdCountertermVariantId(2),
1299 ThresholdCountertermVariantId(3),
1300 ],
1301 );
1302 for kind in [
1303 ThresholdCountertermComponentKind::Local,
1304 ThresholdCountertermComponentKind::Integrated,
1305 ThresholdCountertermComponentKind::LocalLocal,
1306 ThresholdCountertermComponentKind::LocalIntegrated,
1307 ThresholdCountertermComponentKind::IntegratedLocal,
1308 ThresholdCountertermComponentKind::IntegratedIntegrated,
1309 ] {
1310 assert!(
1311 registry
1312 .components
1313 .iter()
1314 .any(|component| component.kind == kind)
1315 );
1316 }
1317 assert_eq!(
1318 registry
1319 .component_id(
1320 Some(CutGroupId(0)),
1321 ThresholdCountertermComponentKind::LocalIntegrated,
1322 &[
1323 ThresholdCountertermVariantId(0),
1324 ThresholdCountertermVariantId(3),
1325 ],
1326 )
1327 .unwrap(),
1328 13,
1329 );
1330 assert_eq!(
1331 serde_json::from_str::<ThresholdCountertermMetadataRegistry>(
1332 &serde_json::to_string(®istry).unwrap(),
1333 )
1334 .unwrap(),
1335 registry,
1336 );
1337 }
1338}