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