Skip to main content

gammalooprs/uv/
settings.rs

1use std::{
2    collections::BTreeSet,
3    fmt::{Display, Formatter},
4};
5
6use crate::utils::{
7    GS,
8    serde_utils::{
9        IsDefault, is_default_form_path, is_default_pysecdec_relative_precision,
10        is_default_python_path, is_default_vakint_evaluation_methods,
11        is_default_vakint_normalization, is_false, is_one_string, is_true, is_usize,
12    },
13};
14use bincode_trait_derive::{Decode, Encode};
15use schemars::JsonSchema;
16use serde::{Deserialize, Serialize};
17use vakint::{AlphaLoopOptions, EvaluationMethod, FMFTOptions, MATADOptions, PySecDecOptions};
18
19#[derive(
20    Debug,
21    Clone,
22    Copy,
23    Serialize,
24    Deserialize,
25    Encode,
26    Decode,
27    PartialEq,
28    Eq,
29    Hash,
30    JsonSchema,
31    Default,
32)]
33#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
34#[serde(deny_unknown_fields)]
35pub enum ApproximationType {
36    #[default]
37    #[serde(rename = "MUV", alias = "muv")]
38    MUV,
39    #[serde(rename = "PolePart", alias = "pole_part")]
40    PolePart,
41    #[serde(rename = "OS", alias = "os")]
42    OS,
43    #[serde(rename = "IR", alias = "ir")]
44    IR,
45    #[serde(rename = "Unsubtracted", alias = "unsubtracted")]
46    Unsubtracted,
47}
48
49impl Display for ApproximationType {
50    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
51        match self {
52            ApproximationType::MUV => write!(f, "MUV"),
53            ApproximationType::PolePart => write!(f, "PolePart"),
54            ApproximationType::OS => write!(f, "OS"),
55            ApproximationType::IR => write!(f, "IR"),
56            ApproximationType::Unsubtracted => write!(f, "Unsubtracted"),
57        }
58    }
59}
60
61#[derive(
62    Debug,
63    Clone,
64    Copy,
65    Serialize,
66    Deserialize,
67    Encode,
68    Decode,
69    PartialEq,
70    Eq,
71    Hash,
72    JsonSchema,
73    Default,
74)]
75#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
76#[serde(deny_unknown_fields)]
77pub enum UVOrchestrator {
78    #[serde(rename = "legacy_dag_forest", alias = "LegacyDagForest")]
79    LegacyDagForest,
80    #[default]
81    #[serde(rename = "hedge_poset", alias = "HedgePoset")]
82    HedgePoset,
83    #[serde(rename = "compare", alias = "Compare")]
84    Compare,
85}
86
87impl Display for UVOrchestrator {
88    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
89        match self {
90            UVOrchestrator::LegacyDagForest => write!(f, "legacy_dag_forest"),
91            UVOrchestrator::HedgePoset => write!(f, "hedge_poset"),
92            UVOrchestrator::Compare => write!(f, "compare"),
93        }
94    }
95}
96
97#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
98#[derive(
99    Debug,
100    Clone,
101    Serialize,
102    Deserialize,
103    Encode,
104    Decode,
105    Default,
106    PartialEq,
107    Eq,
108    PartialOrd,
109    Ord,
110    Hash,
111    JsonSchema,
112)]
113#[serde(default, deny_unknown_fields)]
114pub struct CTIdentifier {
115    /// Signed PDG identifiers on the counterterm's external boundary.
116    #[serde(skip_serializing_if = "BTreeSet::is_empty")]
117    pub external_pdg_set: BTreeSet<isize>,
118    /// Optional internal PDG identifiers; omitting them makes the rule match any internal set.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub internal_pdg_set: Option<BTreeSet<isize>>,
121}
122
123impl CTIdentifier {
124    pub fn new(
125        external_pdg_set: BTreeSet<isize>,
126        internal_pdg_set: Option<BTreeSet<isize>>,
127    ) -> Self {
128        Self {
129            external_pdg_set,
130            internal_pdg_set,
131        }
132    }
133
134    pub fn matches(&self, candidate: &Self) -> bool {
135        self.external_pdg_set == candidate.external_pdg_set
136            && self
137                .internal_pdg_set
138                .as_ref()
139                .is_none_or(|internal| candidate.internal_pdg_set.as_ref() == Some(internal))
140    }
141}
142
143#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
144#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, PartialEq, Eq, JsonSchema)]
145#[serde(deny_unknown_fields)]
146pub struct CTRenormalizationRule {
147    /// Particle-content pattern that selects the counterterms to which this rule applies.
148    pub ct_identifier: CTIdentifier,
149    /// Approximation that replaces the default prescription for matching counterterms.
150    pub prescription: ApproximationType,
151}
152
153impl CTRenormalizationRule {
154    pub fn new(ct_identifier: CTIdentifier, prescription: ApproximationType) -> Self {
155        Self {
156            ct_identifier,
157            prescription,
158        }
159    }
160}
161
162#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
163#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
164#[serde(default, deny_unknown_fields)]
165pub struct RenormalizationPrescriptionSettings {
166    /// Approximation applied to logarithmically divergent ultraviolet counterterms.
167    #[serde(skip_serializing_if = "IsDefault::is_default")]
168    pub log_divergent: ApproximationType,
169    /// Approximation applied to power-divergent counterterms with massive external structure.
170    #[serde(skip_serializing_if = "IsDefault::is_default")]
171    pub massive_power_divergent: ApproximationType,
172    /// Approximation applied to power-divergent counterterms with massless external structure.
173    #[serde(skip_serializing_if = "IsDefault::is_default")]
174    pub massless_power_divergent: ApproximationType,
175    /// Counterterm-specific prescription rules evaluated before the divergence-class defaults.
176    #[serde(
177        default,
178        skip_serializing_if = "IsDefault::is_default",
179        with = "ct_renormalization_overrides_serde"
180    )]
181    #[schemars(with = "Vec<CTRenormalizationRule>")]
182    pub overrides: Vec<CTRenormalizationRule>,
183}
184
185impl Default for RenormalizationPrescriptionSettings {
186    fn default() -> Self {
187        Self {
188            log_divergent: ApproximationType::MUV,
189            massive_power_divergent: ApproximationType::MUV,
190            massless_power_divergent: ApproximationType::MUV,
191            overrides: Vec::new(),
192        }
193    }
194}
195
196impl RenormalizationPrescriptionSettings {
197    pub fn approximation_scheme_for(
198        &self,
199        ct_identifier: &CTIdentifier,
200        dod: i32,
201        has_massive_externals: bool,
202    ) -> ApproximationType {
203        if let Some(rule) = self
204            .overrides
205            .iter()
206            .find(|rule| &rule.ct_identifier == ct_identifier)
207        {
208            return rule.prescription;
209        }
210
211        if let Some(rule) = self.overrides.iter().find(|rule| {
212            rule.ct_identifier.internal_pdg_set.is_none()
213                && rule.ct_identifier.matches(ct_identifier)
214        }) {
215            return rule.prescription;
216        }
217
218        if dod == 0 {
219            self.log_divergent
220        } else if has_massive_externals {
221            self.massive_power_divergent
222        } else {
223            self.massless_power_divergent
224        }
225    }
226}
227
228#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
229#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
230#[serde(default, deny_unknown_fields)]
231pub struct MATADSettings {
232    /// Expand MATAD master-integral abbreviations before returning the result.
233    #[serde(skip_serializing_if = "is_true")]
234    pub expand_masters: bool,
235    /// Substitute MATAD master integrals by their known analytic expressions.
236    #[serde(skip_serializing_if = "is_true")]
237    pub susbstitute_masters: bool,
238    /// Substitute harmonic polylogarithms produced by MATAD where formulas are available.
239    #[serde(skip_serializing_if = "is_true")]
240    pub substitute_hpls: bool,
241    /// Numerically substitute constants directly in the MATAD result.
242    #[serde(skip_serializing_if = "is_true")]
243    pub direct_numerical_substition: bool,
244}
245
246impl Default for MATADSettings {
247    fn default() -> Self {
248        Self {
249            expand_masters: true,
250            susbstitute_masters: true,
251            substitute_hpls: true,
252            direct_numerical_substition: true,
253        }
254    }
255}
256
257#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
258#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
259#[serde(default, deny_unknown_fields)]
260pub struct AlphaLoopSettings {
261    /// Substitute AlphaLoop master integrals by their registered expressions.
262    #[serde(skip_serializing_if = "is_true")]
263    pub susbstitute_masters: bool,
264}
265
266impl Default for AlphaLoopSettings {
267    fn default() -> Self {
268        Self {
269            susbstitute_masters: true,
270        }
271    }
272}
273
274#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
275#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
276#[serde(default, deny_unknown_fields)]
277pub struct FMFTSettings {
278    /// Expand FMFT master-integral abbreviations before returning the result.
279    #[serde(skip_serializing_if = "is_true")]
280    pub expand_masters: bool,
281    /// Substitute FMFT master integrals by their known analytic expressions.
282    #[serde(skip_serializing_if = "is_true")]
283    pub susbstitute_masters: bool,
284}
285
286impl Default for FMFTSettings {
287    fn default() -> Self {
288        Self {
289            expand_masters: true,
290            susbstitute_masters: true,
291        }
292    }
293}
294#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
295#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
296#[serde(default, deny_unknown_fields)]
297pub struct PySecDecSettings {
298    /// Suppress pySecDec progress output while Vakint evaluates an integral.
299    #[serde(skip_serializing_if = "is_true")]
300    pub quiet: bool,
301    /// Requested relative precision for the pySecDec numerical integration.
302    #[serde(skip_serializing_if = "is_default_pysecdec_relative_precision")]
303    pub relative_precision: f64,
304    /// Minimum pySecDec sample count before a precision-based stop is accepted.
305    #[serde(skip_serializing_if = "is_usize::<10_000>")]
306    pub min_n_evals: usize,
307    /// Maximum pySecDec sample count before the numerical integration stops.
308    #[serde(skip_serializing_if = "is_usize::<1_000_000_000_000>")]
309    pub max_n_evals: usize,
310    /// Existing pySecDec output directory to reuse instead of regenerating integration code.
311    #[serde(skip_serializing_if = "IsDefault::is_default")]
312    pub reuse_existing_output: Option<String>,
313}
314
315impl Default for PySecDecSettings {
316    fn default() -> Self {
317        Self {
318            quiet: true,
319            relative_precision: 1.0e-7,
320            min_n_evals: 10_000,
321            max_n_evals: 1_000_000_000_000,
322            reuse_existing_output: None,
323        }
324    }
325}
326
327#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
328#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
329#[serde(default, deny_unknown_fields)]
330pub struct VakintSettings {
331    /// FORM executable used by analytic Vakint evaluation backends.
332    #[serde(skip_serializing_if = "is_default_form_path")]
333    pub form_exe_path: String,
334    /// Python interpreter used to invoke pySecDec.
335    #[serde(skip_serializing_if = "is_default_python_path")]
336    pub python_exe_path: String,
337    /// Ordered Vakint evaluation backends tried for each matched vacuum integral.
338    #[serde(skip_serializing_if = "is_default_vakint_evaluation_methods")]
339    pub evaluation_methods: Vec<String>,
340    /// MATAD-specific master-integral and substitution controls.
341    #[serde(skip_serializing_if = "IsDefault::is_default")]
342    pub matad: MATADSettings,
343    /// AlphaLoop-specific master-integral substitution controls.
344    #[serde(skip_serializing_if = "IsDefault::is_default")]
345    pub alphaloop: AlphaLoopSettings,
346    /// FMFT-specific master-integral expansion and substitution controls.
347    #[serde(skip_serializing_if = "IsDefault::is_default")]
348    pub fmft: FMFTSettings,
349    /// pySecDec precision, sample, and output-reuse controls.
350    #[serde(skip_serializing_if = "IsDefault::is_default")]
351    pub pysecdec: PySecDecSettings,
352    /// Decimal precision used when substituting backend results at run time.
353    #[serde(skip_serializing_if = "is_usize::<16>")]
354    pub run_time_decimal_precision: usize,
355    /// Remove Vakint's temporary backend directory after a successful evaluation.
356    #[serde(skip_serializing_if = "is_true")]
357    pub clean_tmp_dir: bool,
358    /// Parent directory for temporary Vakint backend files; the system default is used when absent.
359    #[serde(skip_serializing_if = "IsDefault::is_default")]
360    pub temporary_directory: Option<String>,
361    /// Named normalization convention applied to matched vacuum integrals.
362    #[serde(skip_serializing_if = "is_default_vakint_normalization")]
363    pub normalization: String,
364    /// Extra symbolic normalization factor multiplied into every Vakint result.
365    #[serde(skip_serializing_if = "is_one_string")]
366    pub additional_normalization: String,
367}
368
369impl VakintSettings {
370    pub fn true_settings(&self) -> vakint::VakintSettings {
371        vakint::VakintSettings {
372            form_exe_path: self.form_exe_path.clone(),
373            python_exe_path: self.python_exe_path.clone(),
374            verify_numerator_identification: false,
375            run_time_decimal_precision: self.run_time_decimal_precision as u32,
376            allow_unknown_integrals: false,
377            clean_tmp_dir: self.clean_tmp_dir,
378            precision_for_input_float_rationalization:
379                vakint::InputFloatRationalizationPrecision::FullPrecision,
380            evaluation_order: vakint::EvaluationOrder(
381                self.evaluation_methods
382                    .iter()
383                    .map(|a| match a.as_str() {
384                        "alphaloop" => EvaluationMethod::AlphaLoop(AlphaLoopOptions {
385                            susbstitute_masters: self.alphaloop.susbstitute_masters,
386                        }),
387                        "matad" => EvaluationMethod::MATAD(MATADOptions {
388                            expand_masters: self.matad.expand_masters,
389                            susbstitute_masters: self.matad.susbstitute_masters,
390                            substitute_hpls: self.matad.substitute_hpls,
391                            direct_numerical_substition: self.matad.direct_numerical_substition,
392                        }),
393                        "fmft" => EvaluationMethod::FMFT(FMFTOptions {
394                            expand_masters: self.fmft.expand_masters,
395                            susbstitute_masters: self.fmft.susbstitute_masters,
396                        }),
397                        "pysecdec" => EvaluationMethod::PySecDec(PySecDecOptions {
398                            quiet: self.pysecdec.quiet,
399                            relative_precision: self.pysecdec.relative_precision,
400                            min_n_evals: self.pysecdec.min_n_evals as u64,
401                            max_n_evals: self.pysecdec.max_n_evals as u64,
402                            reuse_existing_output: self.pysecdec.reuse_existing_output.clone(),
403                            ..Default::default()
404                        }),
405                        _ => panic!("Unknown vakint evaluation method: {}", a),
406                    })
407                    .collect(),
408            ),
409            use_dot_product_notation: false,
410            temporary_directory: self.temporary_directory.clone(),
411            epsilon_symbol: GS.dim_epsilon.get_name().into(),
412            mu_r_sq_symbol: GS.mu_r_sq.get_name().into(),
413            integral_normalization_factor: match self.normalization.as_str() {
414                "MSbar" => vakint::LoopNormalizationFactor::MSbar,
415                "FMFTandMATAD" => vakint::LoopNormalizationFactor::FMFTandMATAD,
416                "pySecDec" => vakint::LoopNormalizationFactor::pySecDec,
417                _ => vakint::LoopNormalizationFactor::Custom(self.normalization.clone()),
418            },
419            //Custom("1".to_string()),
420            number_of_terms_in_epsilon_expansion: 5,
421            project_onto_tensor_integrals: true,
422            // ..Default::default()
423        }
424    }
425}
426
427impl Default for VakintSettings {
428    fn default() -> Self {
429        Self {
430            form_exe_path: "form".to_string(),
431            python_exe_path: "python3".to_string(),
432            // Supported evaluation methods: "alphaloop", "matad", "fmft" and "pysecdec"
433            evaluation_methods: vec![
434                "alphaloop".to_string(),
435                "matad".to_string(),
436                "fmft".to_string(),
437            ],
438            matad: MATADSettings::default(),
439            alphaloop: AlphaLoopSettings::default(),
440            fmft: FMFTSettings::default(),
441            pysecdec: PySecDecSettings::default(),
442            run_time_decimal_precision: 100,
443            clean_tmp_dir: true,
444            temporary_directory: None,
445            normalization: "MSbar".to_string(),
446            additional_normalization: "1".to_string(),
447        }
448    }
449}
450
451#[derive(Debug, Clone, Default, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
452#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
453pub enum FinalIntegrandDimension {
454    FourD,
455    #[default]
456    ThreeD,
457}
458
459impl Display for FinalIntegrandDimension {
460    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
461        match self {
462            FinalIntegrandDimension::FourD => write!(f, "4D"),
463            FinalIntegrandDimension::ThreeD => write!(f, "3D"),
464        }
465    }
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, PartialEq, JsonSchema)]
469#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
470#[serde(default, deny_unknown_fields)]
471pub struct UVgenerationSettings {
472    /// Generate soft ultraviolet counterterms where required by the selected prescription.
473    #[serde(skip_serializing_if = "is_true")]
474    pub softct: bool,
475    /// Generate analytically integrated ultraviolet counterterms.
476    #[serde(skip_serializing_if = "is_true")]
477    pub generate_integrated: bool,
478    /// Reduce universal tensor kernels; false sends each complete numerator to Vakint.
479    #[serde(skip_serializing_if = "is_true")]
480    pub project_integrated_uv_cts_onto_tensor_integrals: bool,
481    /// Subtract generated ultraviolet counterterms from the final integrand.
482    #[serde(skip_serializing_if = "is_true")]
483    pub subtract_uv: bool,
484    /// Dimensional representation used for the final locally subtracted integrand.
485    #[serde(skip_serializing_if = "IsDefault::is_default")]
486    pub final_integrand: FinalIntegrandDimension,
487    /// Construct local counterterms by Taylor-expanding 4D integrands before CFF projection.
488    /// Requires an explicit sum over all generated orientations.
489    #[serde(skip_serializing_if = "is_false")]
490    pub local_uv_cts_from_expanded_4d_integrands: bool,
491    /// Insert explicit marker functions around generated ultraviolet counterterms.
492    #[serde(skip_serializing_if = "is_false")]
493    pub add_marker: bool,
494    /// Preserve ultraviolet marker functions after counterterm orchestration.
495    #[serde(skip_serializing_if = "is_true")]
496    pub keep_marker: bool,
497    /// Rewrite Lorentz contractions into scalar inner products during ultraviolet processing.
498    #[serde(skip_serializing_if = "is_true")]
499    pub inner_products: bool,
500    /// Algorithm used to enumerate and combine nested ultraviolet counterterms.
501    #[serde(skip_serializing_if = "IsDefault::is_default")]
502    pub orchestrator: UVOrchestrator,
503    /// Default and counterterm-specific renormalization approximation schemes.
504    #[serde(skip_serializing_if = "IsDefault::is_default")]
505    pub renormalization_prescription: RenormalizationPrescriptionSettings,
506    /// Vakint backend configuration for integrated ultraviolet counterterms.
507    #[serde(skip_serializing_if = "IsDefault::is_default")]
508    pub vakint: VakintSettings,
509}
510
511impl Default for UVgenerationSettings {
512    fn default() -> Self {
513        UVgenerationSettings {
514            softct: true,
515            generate_integrated: true,
516            project_integrated_uv_cts_onto_tensor_integrals: true,
517            subtract_uv: true,
518            final_integrand: FinalIntegrandDimension::default(),
519            local_uv_cts_from_expanded_4d_integrands: false,
520            inner_products: true,
521            orchestrator: UVOrchestrator::default(),
522            add_marker: false,
523            keep_marker: true,
524            renormalization_prescription: RenormalizationPrescriptionSettings::default(),
525            vakint: VakintSettings::default(),
526        }
527    }
528}
529
530impl UVgenerationSettings {
531    pub fn approximation_scheme_for(
532        &self,
533        ct_identifier: &CTIdentifier,
534        dod: i32,
535        has_massive_externals: bool,
536    ) -> ApproximationType {
537        self.renormalization_prescription.approximation_scheme_for(
538            ct_identifier,
539            dod,
540            has_massive_externals,
541        )
542    }
543}
544
545mod ct_renormalization_overrides_serde {
546    use super::CTRenormalizationRule;
547    use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
548    use std::collections::BTreeSet;
549
550    pub fn serialize<S>(
551        overrides: &Vec<CTRenormalizationRule>,
552        serializer: S,
553    ) -> Result<S::Ok, S::Error>
554    where
555        S: Serializer,
556    {
557        overrides.serialize(serializer)
558    }
559
560    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<CTRenormalizationRule>, D::Error>
561    where
562        D: Deserializer<'de>,
563    {
564        let rules = Vec::<CTRenormalizationRule>::deserialize(deserializer)?;
565        let mut seen = BTreeSet::new();
566
567        for rule in &rules {
568            if !seen.insert(rule.ct_identifier.clone()) {
569                return Err(D::Error::custom(format!(
570                    "duplicate CTIdentifier in renormalization_prescription.overrides: {:?}",
571                    rule.ct_identifier
572                )));
573            }
574        }
575
576        Ok(rules)
577    }
578}
579
580#[cfg(test)]
581mod tests {
582    use super::{
583        ApproximationType, CTIdentifier, CTRenormalizationRule,
584        RenormalizationPrescriptionSettings, UVOrchestrator, UVgenerationSettings,
585    };
586    use std::collections::BTreeSet;
587
588    fn pdg_set(values: impl IntoIterator<Item = isize>) -> BTreeSet<isize> {
589        values.into_iter().collect()
590    }
591
592    #[test]
593    fn tensor_integral_projection_mode_roundtrips_with_visible_defaults() {
594        use crate::utils::serde_utils::ShowDefaultsGuard;
595
596        let defaults: UVgenerationSettings = toml::from_str("").unwrap();
597        assert!(defaults.project_integrated_uv_cts_onto_tensor_integrals);
598        let guard = ShowDefaultsGuard::new(false);
599        assert!(
600            !toml::to_string(&defaults)
601                .unwrap()
602                .contains("project_integrated_uv_cts_onto_tensor_integrals")
603        );
604        let monolithic: UVgenerationSettings =
605            toml::from_str("project_integrated_uv_cts_onto_tensor_integrals = false").unwrap();
606        let serialized = toml::to_string(&monolithic).unwrap();
607        assert!(serialized.contains("project_integrated_uv_cts_onto_tensor_integrals = false"));
608        assert!(
609            !toml::from_str::<UVgenerationSettings>(&serialized)
610                .unwrap()
611                .project_integrated_uv_cts_onto_tensor_integrals
612        );
613        drop(guard);
614        let _guard = ShowDefaultsGuard::new(true);
615        assert!(
616            toml::to_string(&defaults)
617                .unwrap()
618                .contains("project_integrated_uv_cts_onto_tensor_integrals = true")
619        );
620    }
621
622    #[test]
623    fn vakint_physical_loop_normalization_roundtrips_with_visible_defaults() {
624        use crate::utils::serde_utils::ShowDefaultsGuard;
625
626        let defaults: super::VakintSettings = toml::from_str("").unwrap();
627        assert_eq!(defaults.additional_normalization, "1");
628        let guard = ShowDefaultsGuard::new(false);
629        let omitted: toml::Table = toml::from_str(&toml::to_string(&defaults).unwrap()).unwrap();
630        assert!(!omitted.contains_key("additional_normalization"));
631        let explicit: super::VakintSettings =
632            toml::from_str("additional_normalization = '-1'").unwrap();
633        let serialized: toml::Table = toml::from_str(&toml::to_string(&explicit).unwrap()).unwrap();
634        assert_eq!(
635            serialized
636                .get("additional_normalization")
637                .and_then(toml::Value::as_str),
638            Some("-1")
639        );
640        drop(guard);
641        let _guard = ShowDefaultsGuard::new(true);
642        let serialized = toml::to_string(&defaults).unwrap();
643        let visible: toml::Table = toml::from_str(&serialized).unwrap();
644        assert_eq!(
645            visible
646                .get("additional_normalization")
647                .and_then(toml::Value::as_str),
648            Some("1")
649        );
650        assert_eq!(
651            toml::from_str::<super::VakintSettings>(&serialized)
652                .unwrap()
653                .additional_normalization,
654            "1",
655        );
656    }
657
658    #[test]
659    fn renormalization_prescription_prefers_exact_internal_match_over_wildcard() {
660        let exact_identifier = CTIdentifier::new(pdg_set([1]), Some(pdg_set([1, 22])));
661        let wildcard_identifier = CTIdentifier::new(pdg_set([1]), None);
662        let settings = UVgenerationSettings {
663            renormalization_prescription: RenormalizationPrescriptionSettings {
664                overrides: vec![
665                    CTRenormalizationRule::new(
666                        wildcard_identifier,
667                        ApproximationType::Unsubtracted,
668                    ),
669                    CTRenormalizationRule::new(exact_identifier.clone(), ApproximationType::OS),
670                ],
671                ..Default::default()
672            },
673            ..Default::default()
674        };
675
676        assert_eq!(
677            settings.approximation_scheme_for(&exact_identifier, 0, false),
678            ApproximationType::OS
679        );
680    }
681
682    #[test]
683    fn renormalization_prescription_matches_wildcard_internal_rule() {
684        let candidate_identifier = CTIdentifier::new(pdg_set([1]), Some(pdg_set([1, 22])));
685        let settings = UVgenerationSettings {
686            renormalization_prescription: RenormalizationPrescriptionSettings {
687                overrides: vec![CTRenormalizationRule::new(
688                    CTIdentifier::new(pdg_set([1]), None),
689                    ApproximationType::OS,
690                )],
691                ..Default::default()
692            },
693            ..Default::default()
694        };
695
696        assert_eq!(
697            settings.approximation_scheme_for(&candidate_identifier, 0, false),
698            ApproximationType::OS
699        );
700    }
701
702    #[test]
703    fn renormalization_prescription_can_select_unsubtracted() {
704        let candidate_identifier = CTIdentifier::new(pdg_set([1]), Some(pdg_set([1, 22])));
705        let settings = UVgenerationSettings {
706            renormalization_prescription: RenormalizationPrescriptionSettings {
707                overrides: vec![CTRenormalizationRule::new(
708                    CTIdentifier::new(pdg_set([1]), None),
709                    ApproximationType::Unsubtracted,
710                )],
711                ..Default::default()
712            },
713            ..Default::default()
714        };
715
716        assert_eq!(
717            settings.approximation_scheme_for(&candidate_identifier, 0, false),
718            ApproximationType::Unsubtracted
719        );
720    }
721
722    #[test]
723    fn renormalization_prescription_defaults_to_msbar() {
724        let settings = UVgenerationSettings::default();
725
726        assert_eq!(
727            settings.approximation_scheme_for(
728                &CTIdentifier::new(pdg_set([1]), Some(pdg_set([1, 22]))),
729                1,
730                false
731            ),
732            ApproximationType::MUV
733        );
734    }
735
736    #[test]
737    fn renormalization_prescription_accepts_blanket_pole_part() {
738        let settings: RenormalizationPrescriptionSettings = toml::from_str(
739            r#"
740log_divergent = "PolePart"
741massive_power_divergent = "PolePart"
742massless_power_divergent = "PolePart"
743"#,
744        )
745        .unwrap();
746
747        assert_eq!(settings.log_divergent, ApproximationType::PolePart);
748        assert_eq!(
749            settings.massive_power_divergent,
750            ApproximationType::PolePart
751        );
752        assert_eq!(
753            settings.massless_power_divergent,
754            ApproximationType::PolePart
755        );
756    }
757
758    #[test]
759    fn renormalization_prescription_uses_log_divergent_bucket() {
760        let settings = UVgenerationSettings {
761            renormalization_prescription: RenormalizationPrescriptionSettings {
762                log_divergent: ApproximationType::OS,
763                massive_power_divergent: ApproximationType::IR,
764                massless_power_divergent: ApproximationType::Unsubtracted,
765                ..Default::default()
766            },
767            ..Default::default()
768        };
769
770        assert_eq!(
771            settings.approximation_scheme_for(
772                &CTIdentifier::new(pdg_set([1]), Some(pdg_set([1, 22]))),
773                0,
774                false
775            ),
776            ApproximationType::OS
777        );
778    }
779
780    #[test]
781    fn renormalization_prescription_uses_massive_power_divergent_bucket() {
782        let settings = UVgenerationSettings {
783            renormalization_prescription: RenormalizationPrescriptionSettings {
784                massive_power_divergent: ApproximationType::OS,
785                massless_power_divergent: ApproximationType::IR,
786                ..Default::default()
787            },
788            ..Default::default()
789        };
790
791        assert_eq!(
792            settings.approximation_scheme_for(
793                &CTIdentifier::new(pdg_set([1]), Some(pdg_set([1, 22]))),
794                1,
795                true
796            ),
797            ApproximationType::OS
798        );
799    }
800
801    #[test]
802    fn renormalization_prescription_uses_massless_power_divergent_bucket() {
803        let settings = UVgenerationSettings {
804            renormalization_prescription: RenormalizationPrescriptionSettings {
805                massive_power_divergent: ApproximationType::OS,
806                massless_power_divergent: ApproximationType::IR,
807                ..Default::default()
808            },
809            ..Default::default()
810        };
811
812        assert_eq!(
813            settings.approximation_scheme_for(
814                &CTIdentifier::new(pdg_set([1]), Some(pdg_set([1, 22]))),
815                1,
816                false
817            ),
818            ApproximationType::IR
819        );
820    }
821
822    #[test]
823    fn renormalization_prescription_rejects_duplicate_overrides() {
824        let toml = r#"
825[[overrides]]
826prescription = "OS"
827
828[overrides.ct_identifier]
829external_pdg_set = [1]
830
831[[overrides]]
832prescription = "IR"
833
834[overrides.ct_identifier]
835external_pdg_set = [1]
836"#;
837
838        let err = toml::from_str::<RenormalizationPrescriptionSettings>(toml).unwrap_err();
839        assert!(
840            err.to_string()
841                .contains("duplicate CTIdentifier in renormalization_prescription.overrides"),
842            "{err}"
843        );
844    }
845
846    #[test]
847    fn orchestrator_defaults_to_hedge_poset_and_accepts_explicit_modes() {
848        assert_eq!(UVOrchestrator::default(), UVOrchestrator::HedgePoset);
849        assert_eq!(
850            UVgenerationSettings::default().orchestrator,
851            UVOrchestrator::HedgePoset
852        );
853        assert_eq!(
854            toml::from_str::<UVgenerationSettings>("")
855                .unwrap()
856                .orchestrator,
857            UVOrchestrator::HedgePoset
858        );
859
860        let legacy = UVgenerationSettings {
861            orchestrator: UVOrchestrator::LegacyDagForest,
862            ..Default::default()
863        };
864        assert_eq!(legacy.orchestrator, UVOrchestrator::LegacyDagForest);
865        let legacy_toml = toml::to_string(&legacy).unwrap();
866        assert!(legacy_toml.contains("orchestrator = \"legacy_dag_forest\""));
867        assert_eq!(
868            toml::from_str::<UVgenerationSettings>(&legacy_toml).unwrap(),
869            legacy
870        );
871
872        let compare = UVgenerationSettings {
873            orchestrator: UVOrchestrator::Compare,
874            ..Default::default()
875        };
876        assert_eq!(compare.orchestrator, UVOrchestrator::Compare);
877    }
878}