Skip to main content

gammalooprs/processes/
threshold_counterterms.rs

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/// Physical association provenance retained without exposing internal Cartesian products.
57#[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    /// The association-local representation is retained because one raised semantic variant may
64    /// aggregate equivalent subspaces expressed in different parent bases.
65    pub subspace: SubspaceData,
66    /// Whether omitting `parent_lmb` from a materialized default would select a different parent
67    /// from the one used after graph-global cut-group rebasing.
68    pub requires_explicit_parent_lmb: bool,
69    pub eligible: bool,
70    pub origin: ThresholdCountertermOrigin,
71}
72
73/// One independently generated threshold-counterterm variant after graph-local resolution.
74#[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/// Complete process-derived view. Pair products remain implicit in the left/right lists.
112#[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/// Whether one resolved variant has symbolic content and can be reached by the generated
121/// orientation subset. The two flags deliberately remain independent: an existing semantic slot
122/// may be generated but inactive for a selected-orientation integrand.
123#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
124pub struct ThresholdCountertermVariantStatus {
125    pub generated: bool,
126    pub active: bool,
127}
128
129/// A cut-local evaluator and the variants that reference it before graph-local IDs are assigned.
130///
131/// Multiplier collections deduplicate canonical scalar expressions within one input layout. This
132/// neutral registration type lets the process metadata layer retain that exact deduplication
133/// without depending on evaluator implementation details.
134#[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/// Stable graph-local metadata for one independently resolved counterterm variant.
162#[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/// One canonical multiplier evaluator. IDs are contiguous within the graph, while the original
182/// cut-local ID is retained to make standalone-archive validation direct.
183#[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/// Algebraic L/I component generated internally from the compact per-variant graph directives.
193#[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/// Stable graph-local metadata for one expanded component.
230///
231/// `sign` is `-1` for a single-sided/amplitude counterterm and `+1` for an iterated LU term. The
232/// event's bare and weighted values already include this sign.
233#[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/// Static counterterm metadata owned by one graph term in an integrand.
244///
245/// Events identify this registry through their existing graph ID and a component ID, so selecting
246/// a subset of graphs never renumbers metadata inside a graph.
247#[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 &registration.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    /// Resolve the stable component slot generated for one expanded threshold-counterterm term.
531    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    /// Return runtime threshold IDs in the same order as the generated LU side collection.
561    ///
562    /// This lookup is needed when an explicit normalized legacy document owns metadata but keeps
563    /// the homogeneous overlap path (`variant_subspaces = None`). Local components appear once per
564    /// variant and are emitted left-before-right in exactly the runtime collection order.
565    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    /// Complete eligible generated declarations in the compact graph document.
703    ///
704    /// Omitted subspaces are filled from their physical association, while dormant and disabled
705    /// declarations retain their supplied semantics. Function definitions retain their graph/local
706    /// scopes. The completion contains no inferred side, pair, or L/I component records.
707    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        // Runtime multipliers have merged scopes; materialization must preserve the raw split.
945        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(&registry).unwrap(),
1333            )
1334            .unwrap(),
1335            registry,
1336        );
1337    }
1338}