Skip to main content

gammalooprs/settings/
mod.rs

1use bincode_trait_derive::{Decode, Encode};
2use global::{GenerationSettings, Parallelisation};
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    GammaLoopContext,
8    integrands::IntegrandSettings,
9    observables::{ObservablesSettings, QuantitiesSettings, SelectorsSettings},
10    settings::runtime::HFunctionSettings,
11    utils::{
12        F, FloatLike,
13        serde_utils::{IsDefault, show_defaults_helper},
14        tracing::LogStyle,
15    },
16};
17
18#[cfg_attr(
19    feature = "python_api",
20    pyo3::pyclass(from_py_object, get_all, set_all)
21)]
22#[derive(Debug, Clone, Deserialize, Serialize, Encode, Decode, JsonSchema, PartialEq)]
23#[trait_decode(trait= GammaLoopContext)]
24#[serde(default, deny_unknown_fields)]
25pub struct GlobalSettings {
26    /// Tracing filter applied to the optional log-file sink; `off` disables that sink.
27    #[serde(skip_serializing_if = "is_default_logfile_directive")]
28    #[serde(default = "default_logfile_directive")]
29    pub logfile_directive: String,
30    /// Tracing filter applied to terminal output shown by the interactive session.
31    #[serde(skip_serializing_if = "is_default_display_directive")]
32    #[serde(default = "default_display_directive")]
33    pub display_directive: String,
34    /// Formatting choices shared by terminal and file tracing subscribers.
35    #[serde(skip_serializing_if = "IsDefault::is_default")]
36    pub log_style: LogStyle,
37    /// Diagram, numerator, counterterm, evaluator, and compilation settings used during generation.
38    #[serde(skip_serializing_if = "IsDefault::is_default")]
39    pub generation: GenerationSettings,
40    /// Per-stage worker counts for diagram generation, evaluator construction, compilation, and integration.
41    #[serde(skip_serializing_if = "IsDefault::is_default")]
42    pub n_cores: Parallelisation,
43}
44
45#[cfg_attr(
46    feature = "python_api",
47    pyo3::pyclass(from_py_object, get_all, set_all)
48)]
49#[derive(Debug, Clone, Default, Deserialize, Serialize, Encode, Decode, JsonSchema, PartialEq)]
50#[trait_decode(trait= GammaLoopContext)]
51#[serde(default, deny_unknown_fields)]
52pub struct RuntimeSettings {
53    // Runtime settings
54    /// Evaluation scales, backend, caching, event generation, and output-unit controls.
55    #[serde(rename = "general", skip_serializing_if = "IsDefault::is_default")]
56    pub general: GeneralSettings,
57    /// Per-integrand overrides for external model parameters.
58    #[serde(rename = "model", skip_serializing_if = "IsDefault::is_default")]
59    pub model: RuntimeModelSettings,
60    /// Optional built-in test integrand used instead of a generated process integrand.
61    #[serde(rename = "integrand", skip_serializing_if = "IsDefault::is_default")]
62    pub hard_coded_integrand: Option<IntegrandSettings>,
63    /// Center-of-mass energy, external momenta, helicities, and phase-space improvement.
64    #[serde(rename = "kinematics", skip_serializing_if = "IsDefault::is_default")]
65    pub kinematics: KinematicsSettings,
66    /// Adaptive Monte Carlo iteration, accuracy, learning, and output controls.
67    #[serde(rename = "integrator", skip_serializing_if = "IsDefault::is_default")]
68    pub integrator: IntegratorSettings,
69    /// Named derived quantities available to selectors and observables.
70    #[serde(rename = "quantities", skip_serializing_if = "IsDefault::is_default")]
71    pub quantities: QuantitiesSettings,
72    /// Named histograms and other observable accumulators.
73    #[serde(rename = "observables", skip_serializing_if = "IsDefault::is_default")]
74    pub observables: ObservablesSettings,
75    /// Named event-selection predicates applied before observable accumulation.
76    #[serde(rename = "selectors", skip_serializing_if = "IsDefault::is_default")]
77    pub selectors: SelectorsSettings,
78    /// Precision escalation, rotated checks, and optional stability diagnostics.
79    #[serde(rename = "stability")]
80    #[serde(skip_serializing_if = "IsDefault::is_default")]
81    pub stability: StabilitySettings,
82    /// Continuous parameterization and discrete graph/orientation/channel sampling strategy.
83    #[serde(rename = "sampling", skip_serializing_if = "IsDefault::is_default")]
84    pub sampling: SamplingSettings,
85    /// Local and integrated threshold/ultraviolet subtraction controls.
86    #[serde(rename = "subtraction", skip_serializing_if = "IsDefault::is_default")]
87    pub subtraction: SubtractionSettings,
88    /// Damping profile shared by Local Unitarity threshold terms.
89    #[serde(rename = "h_function", skip_serializing_if = "IsDefault::is_default")]
90    pub lu_h_function: HFunctionSettings,
91}
92
93impl RuntimeSettings {
94    pub(crate) fn additional_params<T: FloatLike>(&self) -> Vec<F<T>> {
95        self.general
96            .additional_param_values
97            .iter()
98            .map(|a| F(T::from_f64(*a)))
99            .collect()
100    }
101
102    pub(crate) fn should_generate_events(&self) -> bool {
103        self.general.generate_events
104            || self.selectors.values().any(|selector| selector.active)
105            || !self.observables.is_empty()
106    }
107
108    pub(crate) fn should_buffer_generated_events(&self) -> bool {
109        self.general.generate_events || !self.observables.is_empty()
110    }
111
112    pub(crate) fn should_return_generated_events(&self) -> bool {
113        self.general.generate_events
114    }
115}
116
117fn default_logfile_directive() -> String {
118    "off".to_string()
119}
120
121fn default_display_directive() -> String {
122    "info".to_string()
123}
124
125fn is_default_logfile_directive(val: &String) -> bool {
126    show_defaults_helper(val == &default_logfile_directive())
127}
128
129fn is_default_display_directive(val: &String) -> bool {
130    show_defaults_helper(val == &default_display_directive())
131}
132
133impl Default for GlobalSettings {
134    fn default() -> Self {
135        Self {
136            logfile_directive: default_logfile_directive(),
137            display_directive: default_display_directive(),
138            log_style: LogStyle::default(),
139            generation: GenerationSettings::default(),
140            n_cores: Parallelisation::default(),
141        }
142    }
143}
144
145pub mod global;
146pub use runtime::{
147    GeneralSettings, IntegratorSettings, ObservablesOutputSettings, RuntimeModelSettings,
148    SamplingSettings, StabilitySettings, SubtractionSettings, kinematic::KinematicsSettings,
149};
150pub mod runtime;
151
152#[cfg(test)]
153mod tests {
154    use serde::{Deserialize, Serialize};
155
156    use crate::{
157        momentum::{Dep, ExternalMomenta, Helicity},
158        settings::{
159            GlobalSettings, RuntimeSettings, SamplingSettings, SubtractionSettings,
160            global::{GammaloopCompileOptions, GenerationSettings, ThresholdSubtractionSettings},
161            runtime::{
162                DiscreteGraphSamplingSettings, DiscreteGraphSamplingType,
163                GammaloopTropicalSamplingSettings,
164                kinematic::{Externals, improvement::PhaseSpaceImprovementSettings},
165            },
166        },
167        utils::{
168            DEFAULT_ESURFACE_EXISTENCE_THRESHOLD, F,
169            serde_utils::{SHOWDEFAULTS, ShowDefaultsGuard},
170        },
171    };
172    use std::fmt::Debug;
173
174    fn generic_test_settings<T>()
175    where
176        T: Serialize + for<'de> Deserialize<'de> + Default + PartialEq + Debug,
177    {
178        {
179            let default = T::default();
180            let serialized = serde_yaml::to_string(&default).unwrap();
181            assert_eq!(serialized, "{}\n");
182            let deserialized: T = serde_yaml::from_str(&serialized).unwrap();
183            assert_eq!(default, deserialized);
184
185            let deserialized_from_empty: T = serde_yaml::from_str("").unwrap();
186            assert_eq!(default, deserialized_from_empty);
187        }
188        {
189            let default = T::default();
190            let serialized = toml::to_string_pretty(&default).unwrap();
191            assert_eq!(serialized, "");
192            let deserialized: T = toml::from_str(&serialized).unwrap();
193            assert_eq!(default, deserialized);
194
195            let deserialized_from_empty: T = toml::from_str("").unwrap();
196            assert_eq!(default, deserialized_from_empty);
197        }
198        {
199            let default = T::default();
200            let serialized = serde_json::to_string(&default).unwrap();
201            assert_eq!(serialized, "{}");
202            let deserialized: T = serde_json::from_str(&serialized).unwrap();
203            assert_eq!(default, deserialized);
204
205            let deserialized_from_empty: T = serde_yaml::from_str("").unwrap();
206            assert_eq!(default, deserialized_from_empty);
207        }
208    }
209
210    #[test]
211    fn global_test_serialize_deserialize() {
212        generic_test_settings::<GlobalSettings>();
213    }
214
215    #[test]
216    fn generation_test_serialize_deserialize() {
217        generic_test_settings::<GenerationSettings>();
218    }
219
220    #[test]
221    fn esurface_existence_threshold_defaults_and_overrides() {
222        assert_eq!(
223            ThresholdSubtractionSettings::default().esurface_existence_threshold,
224            DEFAULT_ESURFACE_EXISTENCE_THRESHOLD,
225        );
226        assert_eq!(
227            SubtractionSettings::default().esurface_existence_threshold,
228            DEFAULT_ESURFACE_EXISTENCE_THRESHOLD,
229        );
230
231        let generation: GenerationSettings =
232            toml::from_str("[threshold_subtraction]\nesurface_existence_threshold = 2.5e-8\n")
233                .unwrap();
234        assert_eq!(
235            generation
236                .threshold_subtraction
237                .esurface_existence_threshold,
238            2.5e-8,
239        );
240
241        let runtime: RuntimeSettings =
242            toml::from_str("[subtraction]\nesurface_existence_threshold = 4.0e-9\n").unwrap();
243        assert_eq!(runtime.subtraction.esurface_existence_threshold, 4.0e-9,);
244
245        let zero_generation: GenerationSettings =
246            toml::from_str("[threshold_subtraction]\nesurface_existence_threshold = 0.0\n")
247                .unwrap();
248        assert_eq!(
249            zero_generation
250                .threshold_subtraction
251                .esurface_existence_threshold,
252            0.0,
253        );
254
255        for invalid in ["-1.0e-7", "nan", "inf", "-inf"] {
256            let generation = toml::from_str::<GenerationSettings>(&format!(
257                "[threshold_subtraction]\nesurface_existence_threshold = {invalid}\n"
258            ));
259            assert!(
260                generation.is_err(),
261                "generation-time E-surface tolerance {invalid} must be rejected"
262            );
263
264            let runtime = toml::from_str::<RuntimeSettings>(&format!(
265                "[subtraction]\nesurface_existence_threshold = {invalid}\n"
266            ));
267            assert!(
268                runtime.is_err(),
269                "runtime E-surface tolerance {invalid} must be rejected"
270            );
271        }
272    }
273
274    #[test]
275    fn radial_root_residual_tolerance_defaults_and_overrides() {
276        assert_eq!(
277            SubtractionSettings::default().radial_root_residual_tolerance,
278            64.0,
279        );
280        assert!(
281            !toml::to_string(&RuntimeSettings::default())
282                .unwrap()
283                .contains("radial_root_residual_tolerance")
284        );
285
286        let runtime: RuntimeSettings =
287            toml::from_str("[subtraction]\nradial_root_residual_tolerance = 128.0\n").unwrap();
288        assert_eq!(runtime.subtraction.radial_root_residual_tolerance, 128.0);
289
290        for invalid in ["-1.0", "nan", "inf", "-inf"] {
291            let runtime = toml::from_str::<RuntimeSettings>(&format!(
292                "[subtraction]\nradial_root_residual_tolerance = {invalid}\n"
293            ));
294            assert!(
295                runtime.is_err(),
296                "radial-root residual tolerance {invalid} must be rejected"
297            );
298        }
299    }
300
301    #[test]
302    fn evaluator_settings_partial_tables_use_evaluator_defaults() {
303        use crate::processes::EvaluatorSettings;
304
305        let parsed: EvaluatorSettings =
306            toml::from_str("horner_iterations = 0\ncpe_iterations = 0\n").unwrap();
307
308        assert_eq!(
309            parsed,
310            EvaluatorSettings {
311                horner_iterations: 0,
312                cpe_iterations: Some(0),
313                ..EvaluatorSettings::default()
314            }
315        );
316
317        let serialized = toml::to_string(&parsed).unwrap();
318        assert!(serialized.contains("horner_iterations = 0"));
319        assert!(serialized.contains("cpe_iterations = 0"));
320        assert!(!serialized.contains("iterative_orientation_optimization"));
321        assert!(!serialized.contains("n_cores"));
322        assert!(!serialized.contains("max_horner_scheme_variables"));
323        assert!(!serialized.contains("max_common_pair_cache_entries"));
324        assert!(!serialized.contains("max_common_pair_distance"));
325    }
326
327    #[test]
328    fn compile_test_serialize_deserialize() {
329        use crate::settings::global::GammaloopCompileOptions;
330        generic_test_settings::<GammaloopCompileOptions>();
331    }
332
333    #[test]
334    fn compile_settings_default_to_symjit_o2() {
335        use crate::{
336            processes::EvaluatorSettings,
337            settings::global::{
338                CompilationMode, CompilationOptimizationLevel, FrozenCompilationMode,
339                GammaloopCompileOptions,
340            },
341        };
342
343        let options = GammaloopCompileOptions::default();
344        assert_eq!(options.compilation_mode, CompilationMode::Symjit);
345        assert_eq!(options.optimization_level, CompilationOptimizationLevel::O2);
346        assert_eq!(
347            options.frozen_mode(&EvaluatorSettings {
348                compile: true,
349                ..Default::default()
350            }),
351            FrozenCompilationMode::Symjit(CompilationOptimizationLevel::O2)
352        );
353    }
354
355    #[test]
356    fn compile_settings_forward_symjit_optimization_level() {
357        use crate::{
358            processes::EvaluatorSettings,
359            settings::global::{
360                CompilationOptimizationLevel, FrozenCompilationMode, GammaloopCompileOptions,
361            },
362        };
363
364        let options = GammaloopCompileOptions {
365            optimization_level: CompilationOptimizationLevel::O1,
366            ..Default::default()
367        };
368
369        assert_eq!(
370            options.frozen_mode(&EvaluatorSettings {
371                compile: true,
372                ..Default::default()
373            }),
374            FrozenCompilationMode::Symjit(CompilationOptimizationLevel::O1)
375        );
376    }
377
378    #[test]
379    fn compile_settings_resolve_frozen_mode_from_compile_flag() {
380        use crate::{
381            processes::EvaluatorSettings,
382            settings::global::{CompilationMode, FrozenCompilationMode, GammaloopCompileOptions},
383        };
384
385        let options = GammaloopCompileOptions {
386            compilation_mode: CompilationMode::Assembly,
387            ..Default::default()
388        };
389
390        assert_eq!(
391            options.frozen_mode(&EvaluatorSettings {
392                compile: false,
393                ..Default::default()
394            }),
395            FrozenCompilationMode::Eager
396        );
397        assert!(matches!(
398            options.frozen_mode(&EvaluatorSettings {
399                compile: true,
400                ..Default::default()
401            }),
402            FrozenCompilationMode::Assembly(_)
403        ));
404    }
405
406    #[test]
407    fn compile_settings_map_custom_args_into_symbolica_compile_options() {
408        use crate::settings::global::{
409            CompilationMode, CompilationOptimizationLevel, GammaloopCompileOptions,
410        };
411
412        let options = GammaloopCompileOptions {
413            compilation_mode: CompilationMode::Cpp,
414            optimization_level: CompilationOptimizationLevel::O1,
415            fast_math: false,
416            unsafe_math: false,
417            compiler: "clang++".to_string(),
418            custom: vec!["-g".to_string(), "-Winvalid".to_string()],
419        };
420        let symbolica = options.to_symbolica_compile_options();
421
422        assert_eq!(
423            symbolica.to_string(),
424            "clang++ -shared -O1 -fPIC -march=native -g -Winvalid"
425        );
426    }
427
428    #[test]
429    fn tropical_subgraph_table_test_serialize_deserialize() {
430        use crate::settings::global::TropicalSubgraphTableSettings;
431        generic_test_settings::<TropicalSubgraphTableSettings>();
432    }
433
434    #[test]
435    fn runtime_test_serialize_deserialize() {
436        generic_test_settings::<RuntimeSettings>();
437    }
438
439    #[test]
440    fn runtime_model_settings_serialize_deserialize() {
441        use crate::settings::runtime::RuntimeModelSettings;
442        generic_test_settings::<RuntimeModelSettings>();
443    }
444
445    #[test]
446    fn runtime_settings_serializes_model_overrides_under_model_block() {
447        let mut settings = RuntimeSettings::default();
448        settings
449            .model
450            .external_parameters
451            .insert("mass_scalar_2".to_string(), (F(2.0), F(0.0)));
452
453        let serialized = toml::to_string_pretty(&settings).unwrap();
454        assert!(serialized.contains("[model]"));
455        assert!(serialized.contains("mass_scalar_2 = ["));
456
457        let deserialized: RuntimeSettings = toml::from_str(&serialized).unwrap();
458        assert_eq!(settings, deserialized);
459    }
460
461    #[test]
462    fn subtraction_settings_test_serialize_deserialize() {
463        use crate::settings::runtime::SubtractionSettings;
464        generic_test_settings::<SubtractionSettings>();
465    }
466
467    #[test]
468    fn test_general_settings_serialize_deserialize() {
469        use crate::settings::runtime::GeneralSettings;
470        generic_test_settings::<GeneralSettings>();
471    }
472
473    #[test]
474    fn runtime_event_generation_policy() {
475        let mut settings = RuntimeSettings::default();
476        assert!(!settings.should_generate_events());
477        assert!(!settings.should_buffer_generated_events());
478        assert!(!settings.should_return_generated_events());
479
480        settings.observables.insert(
481            "observable".to_string(),
482            crate::observables::ObservableSettings {
483                quantity: "pt".to_string(),
484                selections: Vec::new(),
485                entry_selection: crate::observables::EntrySelection::All,
486                entry_index: 0,
487                value_transform: crate::observables::ObservableValueTransform::Identity,
488                phase: crate::observables::ObservablePhase::Real,
489                misbinning_max_normalized_distance: None,
490                histogram: crate::observables::HistogramSettings::Continuous(
491                    crate::observables::ContinuousHistogramSettings {
492                        x_min: 0.0,
493                        x_max: 1.0,
494                        n_bins: 1,
495                        log_x_axis: false,
496                        log_y_axis: true,
497                        title: None,
498                        type_description: "AL".to_string(),
499                    },
500                ),
501            },
502        );
503        assert!(settings.should_generate_events());
504        assert!(settings.should_buffer_generated_events());
505        assert!(!settings.should_return_generated_events());
506
507        settings.observables.clear();
508
509        settings.selectors.insert(
510            "selector".to_string(),
511            crate::observables::SelectorSettings {
512                quantity: "pt".to_string(),
513                active: true,
514                entry_selection: crate::observables::EntrySelection::All,
515                entry_index: 0,
516                selector: crate::observables::SelectorDefinitionSettings::CountRange(
517                    crate::observables::CountRangeSelectorSettings {
518                        min_count: 1,
519                        max_count: None,
520                    },
521                ),
522            },
523        );
524        assert!(settings.should_generate_events());
525        assert!(!settings.should_buffer_generated_events());
526        assert!(!settings.should_return_generated_events());
527
528        settings.observables.insert(
529            "observable".to_string(),
530            crate::observables::ObservableSettings {
531                quantity: "pt".to_string(),
532                selections: Vec::new(),
533                entry_selection: crate::observables::EntrySelection::All,
534                entry_index: 0,
535                value_transform: crate::observables::ObservableValueTransform::Identity,
536                phase: crate::observables::ObservablePhase::Real,
537                misbinning_max_normalized_distance: None,
538                histogram: crate::observables::HistogramSettings::Continuous(
539                    crate::observables::ContinuousHistogramSettings {
540                        x_min: 0.0,
541                        x_max: 1.0,
542                        n_bins: 1,
543                        log_x_axis: false,
544                        log_y_axis: true,
545                        title: None,
546                        type_description: "AL".to_string(),
547                    },
548                ),
549            },
550        );
551        assert!(settings.should_generate_events());
552        assert!(settings.should_buffer_generated_events());
553        assert!(!settings.should_return_generated_events());
554
555        settings.general.generate_events = true;
556        assert!(settings.should_generate_events());
557        assert!(settings.should_buffer_generated_events());
558        assert!(settings.should_return_generated_events());
559    }
560
561    #[test]
562    fn test_integrator_settings_serialize_deserialize() {
563        use crate::settings::runtime::IntegratorSettings;
564        generic_test_settings::<IntegratorSettings>();
565    }
566
567    #[test]
568    fn test_observables_output_settings_serialize_deserialize() {
569        use crate::settings::runtime::ObservablesOutputSettings;
570        generic_test_settings::<ObservablesOutputSettings>();
571    }
572
573    #[test]
574    fn observables_output_settings_accept_single_entry_format_lists() {
575        use crate::{
576            observables::ObservableFileFormat, settings::runtime::ObservablesOutputSettings,
577        };
578
579        let parsed: ObservablesOutputSettings = toml::from_str("format = [\"hwu\"]").unwrap();
580        assert_eq!(parsed.format, vec![ObservableFileFormat::Hwu]);
581
582        let serialized = toml::to_string_pretty(&parsed).unwrap();
583        assert_eq!(serialized, "format = [\"hwu\"]\n");
584    }
585
586    #[test]
587    fn observables_output_settings_accept_multiple_formats() {
588        use crate::{
589            observables::ObservableFileFormat, settings::runtime::ObservablesOutputSettings,
590        };
591
592        let parsed: ObservablesOutputSettings =
593            toml::from_str("format = [\"hwu\", \"json\"]").unwrap();
594        assert_eq!(
595            parsed.format,
596            vec![ObservableFileFormat::Hwu, ObservableFileFormat::Json]
597        );
598
599        let serialized = toml::to_string_pretty(&parsed).unwrap();
600        let reparsed: ObservablesOutputSettings = toml::from_str(&serialized).unwrap();
601        assert_eq!(parsed, reparsed);
602    }
603
604    #[test]
605    fn test_parameterization_settings_serialize_deserialize() {
606        use crate::settings::runtime::ParameterizationSettings;
607        generic_test_settings::<ParameterizationSettings>();
608    }
609
610    #[test]
611    fn test_multi_channeling_settings_serialize_deserialize() {
612        use crate::settings::runtime::MultiChannelingSettings;
613        generic_test_settings::<MultiChannelingSettings>();
614    }
615
616    #[test]
617    fn test_gammaloop_tropical_sampling_settings_serialize_deserialize() {
618        use crate::settings::runtime::GammaloopTropicalSamplingSettings;
619        generic_test_settings::<GammaloopTropicalSamplingSettings>();
620    }
621
622    #[test]
623    fn test_discrete_graph_sampling_settings_serialize_deserialize() {
624        use crate::settings::runtime::DiscreteGraphSamplingSettings;
625        generic_test_settings::<DiscreteGraphSamplingSettings>();
626    }
627
628    #[test]
629    fn test_local_counter_term_settings_serialize_deserialize() {
630        use crate::settings::runtime::LocalCounterTermSettings;
631        generic_test_settings::<LocalCounterTermSettings>();
632    }
633
634    #[test]
635    fn test_uv_localisation_settings_serialize_deserialize() {
636        use crate::settings::runtime::UVLocalisationSettings;
637        generic_test_settings::<UVLocalisationSettings>();
638    }
639
640    #[test]
641    fn test_integrated_counterterm_settings_serialize_deserialize() {
642        use crate::settings::runtime::IntegratedCounterTermSettings;
643        generic_test_settings::<IntegratedCounterTermSettings>();
644    }
645
646    #[test]
647    fn test_uv_generation_settings_serialize_deserialize() {
648        use crate::uv::UVgenerationSettings;
649        generic_test_settings::<UVgenerationSettings>();
650    }
651
652    #[test]
653    fn test_overlap_settings_serialize_deserialize() {
654        use crate::settings::runtime::OverlapSettings;
655        generic_test_settings::<OverlapSettings>();
656    }
657    #[test]
658    fn test_h_function_settings_serialize_deserialize() {
659        use crate::settings::runtime::HFunctionSettings;
660        generic_test_settings::<HFunctionSettings>();
661    }
662
663    #[test]
664    fn test_kinematics_settings_serialize_deserialize() {
665        use crate::settings::KinematicsSettings;
666        generic_test_settings::<KinematicsSettings>();
667
668        let kinematics_settings = KinematicsSettings {
669            e_cm: 100.0,
670            externals: Externals::Constant {
671                momenta: vec![
672                    ExternalMomenta::Independent([F(1.), F(2.), F(3.), F(4.)]),
673                    ExternalMomenta::Dependent(Dep::Dep),
674                ],
675                helicities: vec![Helicity::PLUS, Helicity::MINUS],
676                improvement_settings: PhaseSpaceImprovementSettings::default(),
677                f_64_cache: None,
678                f_128_cache: None,
679            },
680        };
681
682        let toml = toml::to_string_pretty(&kinematics_settings).unwrap();
683        let deserialized: KinematicsSettings = toml::from_str(&toml).unwrap();
684        assert_eq!(kinematics_settings, deserialized);
685
686        let toml_without_ecm = format!(
687            "{}\n",
688            toml.lines()
689                .filter(|line| !line.trim_start().starts_with("e_cm = "))
690                .collect::<Vec<_>>()
691                .join("\n")
692        );
693        let deserialized_without_ecm: KinematicsSettings =
694            toml::from_str(&toml_without_ecm).unwrap();
695        assert_eq!(
696            deserialized_without_ecm.externals,
697            kinematics_settings.externals
698        );
699        assert_eq!(deserialized_without_ecm.e_cm, 2.5);
700    }
701
702    #[test]
703    fn sampling_settings_serializes_to_parser_shape() {
704        let sampling_settings = SamplingSettings::DiscreteGraphs(DiscreteGraphSamplingSettings {
705            graph_names: Vec::new(),
706            sample_orientations: true,
707            sampling_type: DiscreteGraphSamplingType::DiscreteMultiChanneling(
708                crate::settings::runtime::MultiChannelingSettings::default(),
709            ),
710        });
711
712        let _guard = ShowDefaultsGuard::new(true);
713        let toml = toml::to_string_pretty(&sampling_settings).unwrap();
714        assert!(toml.contains("graphs = \"monte_carlo\""));
715        assert!(toml.contains("orientations = \"monte_carlo\""));
716        assert!(toml.contains("lmb_multichanneling = true"));
717        assert!(toml.contains("lmb_channels = \"monte_carlo\""));
718        assert!(toml.contains("alpha = 3.0"));
719        assert!(toml.contains("lmb_channel_weight = \"ose\""));
720        assert!(toml.contains("coordinate_system = \"spherical\""));
721        assert!(toml.contains("power = 1.0"));
722        assert!(toml.contains("graph_names = []"));
723        assert!(!toml.contains("type = \"discrete_graph_sampling\""));
724        assert!(!toml.contains("subtype = \"discrete_multi_channeling\""));
725    }
726
727    #[test]
728    fn sampling_settings_graph_names_round_trip() {
729        let sampling_settings = SamplingSettings::DiscreteGraphs(DiscreteGraphSamplingSettings {
730            graph_names: vec!["GL22".to_string(), "GL30".to_string()],
731            sample_orientations: false,
732            sampling_type: DiscreteGraphSamplingType::Default(Default::default()),
733        });
734
735        let toml = toml::to_string_pretty(&sampling_settings).unwrap();
736        assert!(toml.contains("graphs = \"monte_carlo\""));
737        assert!(toml.contains("graph_names = ["));
738        assert!(toml.contains("\"GL22\""));
739        assert!(toml.contains("\"GL30\""));
740        let reparsed: SamplingSettings = toml::from_str(&toml).unwrap();
741        assert_eq!(reparsed, sampling_settings);
742        assert_eq!(reparsed.selected_graph_names(), ["GL22", "GL30"]);
743    }
744
745    #[test]
746    fn sampling_settings_default_has_no_graph_name_filter() {
747        assert!(
748            SamplingSettings::default()
749                .selected_graph_names()
750                .is_empty()
751        );
752
753        let parsed: SamplingSettings = toml::from_str("graphs = \"monte_carlo\"").unwrap();
754        assert!(parsed.selected_graph_names().is_empty());
755        assert!(
756            !toml::to_string_pretty(&parsed)
757                .unwrap()
758                .contains("graph_names")
759        );
760    }
761
762    #[test]
763    fn sampling_settings_rejects_graph_names_with_summed_graphs() {
764        let err =
765            toml::from_str::<SamplingSettings>("graphs = \"summed\"\ngraph_names = [\"GL22\"]")
766                .unwrap_err();
767        assert!(
768            err.to_string()
769                .contains("graph_names requires graphs = 'monte_carlo'")
770        );
771    }
772
773    #[test]
774    fn sampling_settings_rejects_duplicate_graph_names() {
775        let err = toml::from_str::<SamplingSettings>(
776            "graphs = \"monte_carlo\"\ngraph_names = [\"GL22\", \"GL22\"]",
777        )
778        .unwrap_err();
779        assert!(err.to_string().contains("duplicate graph name 'GL22'"));
780    }
781
782    #[test]
783    fn sampling_settings_rejects_incompatible_orientation_sampling() {
784        let invalid_toml = r#"
785graphs = "summed"
786orientations = "monte_carlo"
787lmb_multichanneling = false
788lmb_channels = "summed"
789coordinate_system = "spherical"
790mapping = "linear"
791b = 1.0
792"#;
793
794        let err = toml::from_str::<SamplingSettings>(invalid_toml).unwrap_err();
795        assert!(err.to_string().contains(
796            "orientations can only be set to 'monte_carlo' when graphs is 'monte_carlo'"
797        ));
798    }
799
800    #[test]
801    fn sampling_settings_serializes_lmb_basis_ids_for_default_sampling() {
802        let sampling_settings =
803            SamplingSettings::Default(crate::settings::runtime::ParameterizationSettings {
804                lmb_basis_ids: std::collections::BTreeMap::from([
805                    ("GL02".to_string(), vec![0, 3]),
806                    ("GL05".to_string(), vec![1]),
807                ]),
808                ..Default::default()
809            });
810
811        let toml = toml::to_string_pretty(&sampling_settings).unwrap();
812        assert!(toml.contains("lmb_basis_ids"));
813        let reparsed: SamplingSettings = toml::from_str(&toml).unwrap();
814        assert_eq!(reparsed, sampling_settings);
815    }
816
817    #[test]
818    fn sampling_settings_deserializes_lmb_basis_ids_for_default_sampling() {
819        let toml = r#"
820graphs = "summed"
821orientations = "summed"
822lmb_multichanneling = false
823lmb_channels = "summed"
824coordinate_system = "spherical"
825mapping = "linear"
826lmb_basis_ids = { GL02 = [0, 3], GL05 = [1] }
827"#;
828
829        let settings: SamplingSettings = toml::from_str(toml).unwrap();
830        assert_eq!(
831            settings,
832            SamplingSettings::Default(crate::settings::runtime::ParameterizationSettings {
833                lmb_basis_ids: std::collections::BTreeMap::from([
834                    ("GL02".to_string(), vec![0, 3]),
835                    ("GL05".to_string(), vec![1]),
836                ]),
837                ..Default::default()
838            })
839        );
840    }
841
842    #[test]
843    fn sampling_settings_allows_lmb_basis_ids_with_multichanneling() {
844        let toml = r#"
845graphs = "summed"
846orientations = "summed"
847lmb_multichanneling = true
848lmb_channels = "summed"
849coordinate_system = "spherical"
850lmb_basis_ids = { GL02 = [1] }
851"#;
852
853        let settings: SamplingSettings = toml::from_str(toml).unwrap();
854        assert!(
855            settings
856                .get_parameterization_settings()
857                .unwrap()
858                .lmb_basis_ids
859                .contains_key("GL02")
860        );
861    }
862
863    #[test]
864    fn sampling_settings_rejects_scalar_lmb_basis_id_as_unknown() {
865        let invalid_toml = r#"
866graphs = "summed"
867orientations = "summed"
868lmb_multichanneling = false
869lmb_channels = "summed"
870coordinate_system = "spherical"
871lmb_basis_id = 1
872"#;
873
874        let err = toml::from_str::<SamplingSettings>(invalid_toml).unwrap_err();
875        assert!(err.to_string().contains("unknown field `lmb_basis_id`"));
876    }
877
878    #[test]
879    fn sampling_settings_rejects_empty_lmb_basis_ids_list() {
880        let invalid_toml = r#"
881graphs = "summed"
882orientations = "summed"
883lmb_multichanneling = false
884lmb_channels = "summed"
885coordinate_system = "spherical"
886lmb_basis_ids = { GL02 = [] }
887"#;
888
889        let err = toml::from_str::<SamplingSettings>(invalid_toml).unwrap_err();
890        assert!(err.to_string().contains("cannot be empty"));
891    }
892
893    #[test]
894    fn sampling_settings_rejects_duplicate_lmb_basis_ids() {
895        let invalid_toml = r#"
896graphs = "summed"
897orientations = "summed"
898lmb_multichanneling = false
899lmb_channels = "summed"
900coordinate_system = "spherical"
901lmb_basis_ids = { GL02 = [1, 1] }
902"#;
903
904        let err = toml::from_str::<SamplingSettings>(invalid_toml).unwrap_err();
905        assert!(err.to_string().contains("duplicate basis id 1"));
906    }
907
908    #[test]
909    fn sampling_settings_rejects_lmb_basis_ids_with_tropical_sampling() {
910        let invalid_toml = r#"
911graphs = "monte_carlo"
912orientations = "summed"
913lmb_multichanneling = false
914lmb_channels = "summed"
915coordinate_system = "tropical"
916lmb_basis_ids = { GL02 = [1] }
917"#;
918
919        let err = toml::from_str::<SamplingSettings>(invalid_toml).unwrap_err();
920        assert!(
921            err.to_string()
922                .contains("coordinate_system = 'tropical' is incompatible with lmb_basis_ids")
923        );
924    }
925
926    #[test]
927    fn sampling_settings_deserializes_from_parser_shape() {
928        let toml = r#"
929graphs = "monte_carlo"
930orientations = "summed"
931lmb_multichanneling = true
932lmb_channels = "summed"
933alpha = 1.5
934lmb_channel_weight = "inverse_jacobian"
935coordinate_system = "momentum_space"
936mapping = "log"
937b = 5.0
938power = 2.0
939"#;
940
941        let settings: SamplingSettings = toml::from_str(toml).unwrap();
942        assert_eq!(
943            settings,
944            SamplingSettings::DiscreteGraphs(DiscreteGraphSamplingSettings {
945                graph_names: Vec::new(),
946                sample_orientations: false,
947                sampling_type: DiscreteGraphSamplingType::MultiChanneling(
948                    crate::settings::runtime::MultiChannelingSettings {
949                        alpha: 1.5,
950                        channel_weight: crate::settings::runtime::LmbChannelWeight::InverseJacobian,
951                        parameterization_settings:
952                            crate::settings::runtime::ParameterizationSettings {
953                                mode: crate::settings::runtime::ParameterizationMode::MomentumSpace,
954                                mapping: crate::settings::runtime::ParameterizationMapping::Log,
955                                b: 5.0,
956                                power: 2.0,
957                                lmb_basis_ids: Default::default(),
958                            },
959                    },
960                ),
961            })
962        );
963    }
964
965    #[test]
966    fn sampling_settings_deserializes_power_mapping() {
967        let toml = r#"
968graphs = "monte_carlo"
969orientations = "summed"
970lmb_multichanneling = true
971lmb_channels = "summed"
972lmb_channel_weight = "inverse_jacobian"
973coordinate_system = "spherical"
974mapping = "power"
975b = 1.5
976power = 4.0
977"#;
978
979        let settings: SamplingSettings = toml::from_str(toml).unwrap();
980        assert_eq!(
981            settings,
982            SamplingSettings::DiscreteGraphs(DiscreteGraphSamplingSettings {
983                graph_names: Vec::new(),
984                sample_orientations: false,
985                sampling_type: DiscreteGraphSamplingType::MultiChanneling(
986                    crate::settings::runtime::MultiChannelingSettings {
987                        alpha: 3.0,
988                        channel_weight: crate::settings::runtime::LmbChannelWeight::InverseJacobian,
989                        parameterization_settings:
990                            crate::settings::runtime::ParameterizationSettings {
991                                mode: crate::settings::runtime::ParameterizationMode::Spherical,
992                                mapping: crate::settings::runtime::ParameterizationMapping::Power,
993                                b: 1.5,
994                                power: 4.0,
995                                lmb_basis_ids: Default::default(),
996                            },
997                    },
998                ),
999            })
1000        );
1001    }
1002
1003    #[test]
1004    fn sampling_settings_rejects_invalid_power_mapping_exponent() {
1005        let invalid_toml = r#"
1006graphs = "monte_carlo"
1007orientations = "summed"
1008lmb_multichanneling = true
1009lmb_channels = "summed"
1010coordinate_system = "spherical"
1011mapping = "power"
1012power = 0.0
1013"#;
1014
1015        let err = toml::from_str::<SamplingSettings>(invalid_toml).unwrap_err();
1016        assert!(err.to_string().contains("requires power >= 1"));
1017    }
1018
1019    #[test]
1020    fn sampling_settings_deserializes_relative_spherical_coordinates() {
1021        let toml = r#"
1022graphs = "monte_carlo"
1023orientations = "summed"
1024lmb_multichanneling = true
1025lmb_channels = "summed"
1026lmb_channel_weight = "inverse_jacobian"
1027coordinate_system = "relative_spherical"
1028mapping = "linear"
1029b = 1.0
1030"#;
1031
1032        let settings: SamplingSettings = toml::from_str(toml).unwrap();
1033        assert_eq!(
1034            settings,
1035            SamplingSettings::DiscreteGraphs(DiscreteGraphSamplingSettings {
1036                graph_names: Vec::new(),
1037                sample_orientations: false,
1038                sampling_type: DiscreteGraphSamplingType::MultiChanneling(
1039                    crate::settings::runtime::MultiChannelingSettings {
1040                        alpha: 3.0,
1041                        channel_weight: crate::settings::runtime::LmbChannelWeight::InverseJacobian,
1042                        parameterization_settings:
1043                            crate::settings::runtime::ParameterizationSettings {
1044                                mode: crate::settings::runtime::ParameterizationMode::RelativeSpherical,
1045                                mapping: crate::settings::runtime::ParameterizationMapping::Linear,
1046                                b: 1.0,
1047                                power: 1.0,
1048                                lmb_basis_ids: Default::default(),
1049                            },
1050                    },
1051                ),
1052            })
1053        );
1054    }
1055
1056    #[test]
1057    fn sampling_settings_rejects_inverse_jacobian_for_flat_hyperspherical() {
1058        let invalid_toml = r#"
1059graphs = "monte_carlo"
1060orientations = "summed"
1061lmb_multichanneling = true
1062lmb_channels = "summed"
1063alpha = 1.5
1064lmb_channel_weight = "inverse_jacobian"
1065coordinate_system = "hyperspherical_flat"
1066mapping = "linear"
1067b = 1.0
1068"#;
1069
1070        let err = toml::from_str::<SamplingSettings>(invalid_toml).unwrap_err();
1071        assert!(err.to_string().contains("inverse map is not available"));
1072    }
1073
1074    #[test]
1075    fn how_does_tropical_look() {
1076        SHOWDEFAULTS.store(true, std::sync::atomic::Ordering::Relaxed);
1077        let sampling_settings = SamplingSettings::DiscreteGraphs(DiscreteGraphSamplingSettings {
1078            graph_names: Vec::new(),
1079            sample_orientations: false,
1080            sampling_type: DiscreteGraphSamplingType::TropicalSampling(
1081                GammaloopTropicalSamplingSettings {
1082                    upcast_on_failure: false,
1083                    matrix_stability_test: Some(1e-5),
1084                },
1085            ),
1086        });
1087        let toml = toml::to_string_pretty(&sampling_settings).unwrap();
1088        println!("{toml}");
1089        SHOWDEFAULTS.store(false, std::sync::atomic::Ordering::Relaxed);
1090    }
1091
1092    mod failing {
1093        use super::*;
1094
1095        #[test]
1096        fn test_stability_settings_serialize_deserialize() {
1097            use crate::settings::runtime::StabilitySettings;
1098            generic_test_settings::<StabilitySettings>();
1099        }
1100
1101        #[test]
1102        fn test_uv_generation_settings_serializes_renormalization_prescription() {
1103            use crate::uv::{
1104                ApproximationType, CTIdentifier, CTRenormalizationRule,
1105                RenormalizationPrescriptionSettings, UVgenerationSettings,
1106            };
1107            use std::collections::BTreeSet;
1108
1109            let settings = UVgenerationSettings {
1110                softct: true,
1111                renormalization_prescription: RenormalizationPrescriptionSettings {
1112                    log_divergent: ApproximationType::MUV,
1113                    massive_power_divergent: ApproximationType::OS,
1114                    massless_power_divergent: ApproximationType::IR,
1115                    overrides: vec![
1116                        CTRenormalizationRule::new(
1117                            CTIdentifier::new(BTreeSet::from([1]), Some(BTreeSet::from([1, 22]))),
1118                            ApproximationType::OS,
1119                        ),
1120                        CTRenormalizationRule::new(
1121                            CTIdentifier::new(BTreeSet::from([6]), None),
1122                            ApproximationType::Unsubtracted,
1123                        ),
1124                    ],
1125                },
1126                ..Default::default()
1127            };
1128
1129            let toml = toml::to_string_pretty(&GenerationSettings {
1130                compile: GammaloopCompileOptions {
1131                    compiler: "symjit".to_string(),
1132                    ..Default::default()
1133                },
1134                uv: settings.clone(),
1135                ..Default::default()
1136            })
1137            .unwrap();
1138            println!("{}", toml);
1139            assert!(toml.contains("[uv.renormalization_prescription]"));
1140            assert!(toml.contains("[[uv.renormalization_prescription.overrides]]"));
1141            assert!(toml.contains("prescription = \"Unsubtracted\""));
1142
1143            let deserialized_from_toml: GenerationSettings = toml::from_str(&toml).unwrap();
1144            assert_eq!(settings, deserialized_from_toml.uv);
1145
1146            let json = serde_json::to_string(&settings).unwrap();
1147            let deserialized_from_json: UVgenerationSettings = serde_json::from_str(&json).unwrap();
1148            assert_eq!(settings, deserialized_from_json);
1149        }
1150
1151        #[test]
1152        fn test_stability_level_settings_serialize_deserialize() {
1153            use crate::settings::runtime::StabilitySettings;
1154            generic_test_settings::<StabilitySettings>();
1155        }
1156    }
1157}