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