1use std::{
2 collections::{BTreeMap, BTreeSet},
3 fmt::Display,
4};
5
6use bincode_trait_derive::{Decode, Encode};
7use eyre::Result;
8use linnet::half_edge::involution::EdgeVec;
9use schemars::JsonSchema;
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11use spenso::algebra::{algebraic_traits::IsZero, complex::Complex};
12use tracing::warn;
13use typed_index_collections::TiVec;
14
15use crate::{
16 DependentMomentaConstructor, GammaLoopContext,
17 cff::esurface::Esurface,
18 graph::LoopMomentumBasis,
19 integrands::process::evaluators::EvaluatorMethod,
20 momentum::{Helicity, RotationMethod, sample::ExternalIndex, signature::SignatureLike},
21 observables::ObservableFileFormat,
22 settings::runtime::kinematic::{Externals, improvement::generate_default_momenta},
23 utils::{
24 ApproxEq, DEFAULT_ESURFACE_EXISTENCE_THRESHOLD, F, FloatLike, format_uncertainty,
25 serde_utils::{
26 _default_rotation_axis, _default_stability_levels, IsDefault,
27 deserialize_nonnegative_finite_f64, is_default_esurface_existence_threshold,
28 is_default_rotation_axis, is_default_stability_levels, is_false, is_float, is_true,
29 is_u64, is_usize, show_defaults_helper,
30 },
31 },
32};
33use symbolica::domains::float::Real;
34
35use super::{RuntimeSettings, global::OrientationPattern};
36
37#[cfg_attr(
38 feature = "python_api",
39 pyo3::pyclass(from_py_object, get_all, set_all)
40)]
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, Encode, Decode, JsonSchema)]
42#[serde(default)]
43pub struct RuntimeModelSettings {
44 #[serde(flatten, skip_serializing_if = "IsDefault::is_default")]
45 pub external_parameters: BTreeMap<String, (F<f64>, F<f64>)>,
46}
47
48#[cfg_attr(
49 feature = "python_api",
50 pyo3::pyclass(from_py_object, get_all, set_all)
51)]
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
53#[serde(default, deny_unknown_fields)]
54pub struct SubtractionSettings {
55 #[serde(skip_serializing_if = "IsDefault::is_default")]
57 pub local_ct_settings: LocalCounterTermSettings,
58 #[serde(skip_serializing_if = "IsDefault::is_default")]
60 pub integrated_ct_settings: IntegratedCounterTermSettings,
61 #[serde(skip_serializing_if = "IsDefault::is_default")]
63 pub overlap_settings: OverlapSettings,
64 #[serde(
67 deserialize_with = "deserialize_nonnegative_finite_f64",
68 skip_serializing_if = "is_default_esurface_existence_threshold"
69 )]
70 #[schemars(range(min = 0.0))]
71 pub esurface_existence_threshold: f64,
72 #[serde(
77 deserialize_with = "deserialize_nonnegative_finite_f64",
78 skip_serializing_if = "is_float::<64>"
79 )]
80 #[schemars(range(min = 0.0))]
81 pub radial_root_residual_tolerance: f64,
82 #[serde(skip_serializing_if = "is_false")]
84 pub disable_threshold_subtraction: bool,
85}
86
87impl Default for SubtractionSettings {
88 fn default() -> Self {
89 Self {
90 local_ct_settings: LocalCounterTermSettings::default(),
91 integrated_ct_settings: IntegratedCounterTermSettings::default(),
92 overlap_settings: OverlapSettings::default(),
93 esurface_existence_threshold: DEFAULT_ESURFACE_EXISTENCE_THRESHOLD,
94 radial_root_residual_tolerance: 64.0,
95 disable_threshold_subtraction: false,
96 }
97 }
98}
99
100#[derive(Copy, Clone)]
101pub struct LockedRuntimeSettings<'a>(&'a RuntimeSettings);
102impl<'a> From<&'a RuntimeSettings> for LockedRuntimeSettings<'a> {
103 fn from(value: &'a RuntimeSettings) -> Self {
104 LockedRuntimeSettings(value)
105 }
106}
107
108impl<'a> From<LockedRuntimeSettings<'a>> for RuntimeSettings {
109 fn from(value: LockedRuntimeSettings) -> Self {
110 value.0.clone()
111 }
112}
113
114impl<'a> LockedRuntimeSettings<'a> {
115 pub(crate) fn helicities(&self) -> &[Helicity] {
116 self.0.kinematics.externals.get_helicities()
117 }
118
119 pub(crate) fn into_with_modified_kinematics(
121 self,
122 external_signature: &SignatureLike<ExternalIndex>,
123 external_masses: &TiVec<ExternalIndex, F<f64>>,
124 ) -> Result<RuntimeSettings> {
125 if external_signature.is_empty() {
126 Ok(self.into())
127 } else {
128 match &self.0.kinematics.externals {
129 Externals::Constant {
130 momenta,
131 helicities,
132 ..
133 } => {
134 if momenta.is_empty() && helicities.is_empty() {
135 warn!(
136 "No external kinematics were provided; using default-generated external momenta and helicities."
137 );
138 let new_externals = generate_default_momenta(
139 external_masses,
140 external_signature,
141 &F(self.0.kinematics.e_cm),
142 )?;
143
144 let mut new_settigns = self.0.clone();
145 new_settigns.kinematics.externals = new_externals;
146 Ok(new_settigns)
147 } else {
148 Ok(self.into())
149 }
150 }
151 }
152 }
153 }
154
155 pub(crate) fn existence_check(
156 &self,
157 esurface: &Esurface,
158 masses: &EdgeVec<F<f64>>,
159 external_signature: &SignatureLike<ExternalIndex>,
160 lmb: &LoopMomentumBasis,
161 esurface_existence_threshold: f64,
162 ) -> bool {
163 let dependent_momenta_constructor =
164 DependentMomentaConstructor::Amplitude(external_signature);
165
166 esurface
167 .classify_existence(
168 &self
169 .0
170 .kinematics
171 .externals
172 .get_dependent_externals(dependent_momenta_constructor)
173 .unwrap(),
174 lmb,
175 masses,
176 &F(self.0.kinematics.e_cm),
177 &F(esurface_existence_threshold),
178 )
179 .is_existing()
180 }
181}
182
183#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
184#[derive(
185 Debug, Copy, Clone, Deserialize, Serialize, Encode, Decode, PartialEq, Eq, JsonSchema, Default,
186)]
187#[serde(rename_all = "snake_case")]
188pub enum IntegralUnit {
189 #[default]
190 Auto,
191 Picobarn,
192 Femtobarn,
193 Attobarn,
194 Millibarn,
195 None,
196}
197
198impl IntegralUnit {
199 pub(crate) fn resolve_for_cross_section(self, n_initial_state_particles: usize) -> Self {
200 match self {
201 Self::Auto if n_initial_state_particles > 1 => Self::Picobarn,
202 Self::Auto => Self::None,
203 explicit => explicit,
204 }
205 }
206
207 pub(crate) fn relative_to_picobarn_factor<T: FloatLike>(self, one: &F<T>) -> Option<F<T>> {
208 match self {
209 Self::Picobarn => Some(one.one()),
210 Self::Femtobarn => Some(one.from_i64(1_000)),
211 Self::Attobarn => Some(one.from_i64(1_000_000)),
212 Self::Millibarn => Some(one.one() / one.from_i64(1_000_000_000)),
213 Self::None => None,
214 Self::Auto => unreachable!("integral unit must be resolved before conversion"),
215 }
216 }
217}
218
219#[cfg_attr(
220 feature = "python_api",
221 pyo3::pyclass(from_py_object, get_all, set_all)
222)]
223#[derive(Debug, Clone, Deserialize, Serialize, Encode, Decode, PartialEq, JsonSchema)]
224#[trait_decode(trait= GammaLoopContext)]
225#[serde(default, deny_unknown_fields)]
226pub struct GeneralSettings {
227 #[serde(skip_serializing_if = "is_false")]
229 pub use_ltd: bool,
230 #[serde(skip_serializing_if = "IsDefault::is_default")]
232 pub evaluator_method: EvaluatorMethod,
233 #[serde(skip_serializing_if = "IsDefault::is_default")]
235 pub orientation_pat: OrientationPattern,
236 #[serde(skip_serializing_if = "is_false")]
238 pub load_compiled_cff: bool,
239 #[serde(skip_serializing_if = "is_false")]
241 pub enable_cache: bool,
242 #[serde(skip_serializing_if = "is_false")]
244 pub debug_cache: bool,
245 #[serde(skip_serializing_if = "is_float::<1000>")]
247 pub m_uv: f64,
248 #[serde(skip_serializing_if = "is_float::<1000>")]
250 pub renormalization_localization_scale: f64,
251 #[serde(skip_serializing_if = "is_float::<1000>")]
253 pub mu_r: f64,
254 #[serde(skip_serializing_if = "IsDefault::is_default")]
256 pub additional_param_values: Vec<f64>,
257 #[serde(skip_serializing_if = "IsDefault::is_default")]
259 pub integral_unit: IntegralUnit,
260 #[serde(skip_serializing_if = "is_false")]
262 pub disable_flux_factor: bool,
263 #[serde(skip_serializing_if = "is_false")]
265 pub generate_events: bool,
266 #[serde(skip_serializing_if = "is_false")]
268 pub store_additional_weights_in_event: bool,
269}
270
271impl Default for GeneralSettings {
272 fn default() -> Self {
273 Self {
274 evaluator_method: EvaluatorMethod::default(),
275 use_ltd: false,
276 load_compiled_cff: false,
277 enable_cache: false,
278 debug_cache: false,
279 orientation_pat: OrientationPattern::default(),
280 m_uv: 1000.0,
281 renormalization_localization_scale: 1000.0,
282 mu_r: 1000.0,
283
284 additional_param_values: vec![],
285 integral_unit: IntegralUnit::Auto,
286 disable_flux_factor: false,
287 generate_events: false,
288 store_additional_weights_in_event: false,
289 }
290 }
291}
292
293impl GeneralSettings {
294 pub(crate) fn mu_r_sq(&self) -> f64 {
295 self.mu_r * self.mu_r
296 }
297}
298
299#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
300#[derive(
301 Debug, Copy, Clone, PartialEq, Deserialize, Default, Serialize, Encode, Decode, JsonSchema,
302)]
303#[serde(deny_unknown_fields)]
305pub enum IntegratedPhase {
306 #[serde(rename = "real")]
307 #[default]
308 Real,
309 #[serde(rename = "imag")]
310 Imag,
311 #[serde(rename = "both")]
312 Both,
313}
314
315pub mod kinematic;
316
317#[cfg_attr(
318 feature = "python_api",
319 pyo3::pyclass(from_py_object, get_all, set_all)
320)]
321#[derive(Debug, Clone, Deserialize, Serialize, Encode, Decode, PartialEq, JsonSchema)]
322#[serde(default, deny_unknown_fields)]
323pub struct ObservablesOutputSettings {
324 #[serde(
326 default = "default_observable_output_formats",
327 skip_serializing_if = "is_default_observable_output_formats"
328 )]
329 pub format: Vec<ObservableFileFormat>,
330}
331
332fn default_observable_output_formats() -> Vec<ObservableFileFormat> {
333 vec![ObservableFileFormat::Json]
334}
335
336fn is_default_observable_output_formats(formats: &Vec<ObservableFileFormat>) -> bool {
337 show_defaults_helper(formats.as_slice() == default_observable_output_formats().as_slice())
338}
339
340impl Default for ObservablesOutputSettings {
341 fn default() -> Self {
342 Self {
343 format: default_observable_output_formats(),
344 }
345 }
346}
347
348impl ObservablesOutputSettings {
349 pub(crate) fn resolved_formats(&self) -> Vec<ObservableFileFormat> {
350 let mut resolved_formats = Vec::new();
351 for format in &self.format {
352 if *format == ObservableFileFormat::None || resolved_formats.contains(format) {
353 continue;
354 }
355 resolved_formats.push(*format);
356 }
357 resolved_formats
358 }
359}
360
361#[cfg_attr(
362 feature = "python_api",
363 pyo3::pyclass(from_py_object, get_all, set_all)
364)]
365#[derive(Debug, Clone, Deserialize, Serialize, Encode, Decode, PartialEq, JsonSchema)]
366#[serde(default, deny_unknown_fields)]
367pub struct IntegratorSettings {
368 #[serde(skip_serializing_if = "is_usize::<64>")]
370 pub n_bins: usize,
371 #[serde(skip_serializing_if = "IsDefault::is_default")]
373 pub bin_number_evolution: Option<Vec<usize>>,
374 #[serde(skip_serializing_if = "is_usize::<1000>")]
376 pub min_samples_for_update: usize,
377 #[serde(skip_serializing_if = "is_usize::<100000>")]
379 pub n_start: usize,
380 #[serde(skip_serializing_if = "is_usize::<10000>")]
382 pub n_increase: usize,
383 #[serde(skip_serializing_if = "is_usize::<10000000000>")]
385 pub n_max: usize,
386 #[serde(skip_serializing_if = "IsDefault::is_default")]
388 pub target_relative_accuracy: Option<f64>,
389 #[serde(skip_serializing_if = "IsDefault::is_default")]
391 pub target_absolute_accuracy: Option<f64>,
392 #[serde(skip_serializing_if = "IsDefault::is_default")]
394 pub integrated_phase: IntegratedPhase,
395 #[serde(skip_serializing_if = "is_float::<1>")]
397 pub discrete_dim_learning_rate: f64,
398 #[serde(skip_serializing_if = "is_float::<1>")]
400 pub continuous_dim_learning_rate: f64,
401 #[serde(skip_serializing_if = "is_false")]
403 pub train_on_avg: bool,
404 #[serde(skip_serializing_if = "is_true")]
406 pub show_max_wgt_info: bool,
407 #[serde(skip_serializing_if = "is_float::<30>")]
409 pub max_prob_ratio: f64,
410 #[serde(skip_serializing_if = "is_u64::<69>")]
412 pub seed: u64,
413 #[serde(skip_serializing_if = "IsDefault::is_default")]
415 pub observables_output: ObservablesOutputSettings,
416}
417
418impl Default for IntegratorSettings {
419 fn default() -> Self {
420 Self {
421 n_bins: 64,
422 bin_number_evolution: None,
423 min_samples_for_update: 1000,
424 n_start: 100000,
425 n_increase: 10000,
426 n_max: 10000000000,
427 target_relative_accuracy: None,
428 target_absolute_accuracy: None,
429 integrated_phase: IntegratedPhase::default(),
430 discrete_dim_learning_rate: 1.0,
431 continuous_dim_learning_rate: 1.0,
432 train_on_avg: false,
433 show_max_wgt_info: true,
434 max_prob_ratio: 30.0,
435 seed: 69,
436 observables_output: ObservablesOutputSettings::default(),
437 }
438 }
439}
440
441#[cfg_attr(
442 feature = "python_api",
443 pyo3::pyclass(from_py_object, get_all, set_all)
444)]
445#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Encode, Decode, JsonSchema)]
446#[serde(default, deny_unknown_fields)]
447pub struct ParameterizationSettings {
448 #[serde(skip_serializing_if = "IsDefault::is_default")]
449 pub mode: ParameterizationMode,
450 #[serde(skip_serializing_if = "IsDefault::is_default")]
451 pub mapping: ParameterizationMapping,
452 #[serde(skip_serializing_if = "is_float::<1>")]
453 pub b: f64,
454 #[serde(skip_serializing_if = "is_float::<1>")]
455 pub power: f64,
456 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
457 pub lmb_basis_ids: BTreeMap<String, Vec<usize>>,
458}
459
460impl Default for ParameterizationSettings {
461 fn default() -> Self {
462 Self {
463 b: 1.0,
464 power: 1.0,
465 mode: ParameterizationMode::default(),
466 mapping: ParameterizationMapping::default(),
467 lmb_basis_ids: BTreeMap::new(),
468 }
469 }
470}
471
472#[derive(Serialize, Deserialize, Default, Debug, Clone)]
473#[serde(default, deny_unknown_fields)]
474pub struct IntegralEstimate {
475 pub neval: usize,
476 pub real_zero: usize,
477 pub im_zero: usize,
478 pub result: Complex<F<f64>>,
479 pub error: Complex<F<f64>>,
480 pub real_chisq: F<f64>,
481 pub im_chisq: F<f64>,
482}
483
484impl Display for IntegralEstimate {
485 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
486 let real_formatted = if self.error.re.0.abs() > 0.0 && f.alternate() {
488 format_uncertainty(self.result.re, self.error.re)
489 } else {
490 match f.precision() {
492 Some(p) => format!("{:.prec$e}", self.result.re.0, prec = p),
493 None => format!("{:e}", self.result.re.0),
494 }
495 };
496
497 let im_formatted = if self.error.im.0.abs() > 0.0 && f.alternate() {
498 format_uncertainty(self.result.im, self.error.im)
499 } else {
500 match f.precision() {
502 Some(p) => format!("{:.prec$e}", self.result.im.0, prec = p),
503 None => format!("{:e}", self.result.im.0),
504 }
505 };
506
507 if self.result.im.0.abs() > 0.0 {
509 if f.alternate() {
510 write!(
511 f,
512 "{} + {}i (neval: {}, real_chisq: {:.3}, im_chisq: {:.3})",
513 real_formatted, im_formatted, self.neval, self.real_chisq.0, self.im_chisq.0
514 )
515 } else {
516 write!(f, "{} + {}i", real_formatted, im_formatted,)
517 }
518 } else if f.alternate() {
519 write!(
520 f,
521 "{} (neval: {}, real_chisq: {:.3})",
522 real_formatted, self.neval, self.real_chisq.0
523 )
524 } else {
525 write!(f, "{}", real_formatted)
526 }
527 }
528}
529
530impl IntegralEstimate {
531 pub fn is_compatible_with_target(&self, target: Complex<F<f64>>, sigma: i8) -> bool {
532 let res_norm = self.result.norm().re;
533 let tolerance = if res_norm.is_zero() {
534 self.error.norm().re
535 } else {
536 (self.error).norm().re * F(sigma as f64) / res_norm
537 };
538
539 if tolerance.0 > 0.1 {
540 warn!("Tolerance larger than 10%! {}", tolerance)
541 }
542 self.result.approx_eq(&target, &tolerance)
544 }
545
546 pub fn is_compatible_with_result(&self, other: &Self, sigma: i8) -> bool {
547 let combined_error = Complex::new(
548 (self.error.re * self.error.re + other.error.re * other.error.re).sqrt(),
549 (self.error.im * self.error.im + other.error.im * other.error.im).sqrt(),
550 );
551 let delta = self.result - other.result;
552 let delta_norm = delta.norm().re;
553 let tolerance = if delta_norm.is_zero() {
554 combined_error.norm().re
555 } else {
556 combined_error.norm().re * F(sigma as f64) / delta_norm
557 };
558
559 if tolerance.0 > 0.1 {
560 warn!("Tolerance larger than 10%! {}", tolerance)
561 }
562
563 self.result.approx_eq(&other.result, &tolerance)
564 }
565}
566
567#[derive(Serialize, Deserialize, Default, Debug, Clone)]
568#[serde(default, deny_unknown_fields)]
569pub struct IntegrationTableComponentResult {
570 pub component: String,
571 pub value: F<f64>,
572 pub error: F<f64>,
573 pub relative_error_percent: Option<f64>,
574 pub chi_sq_per_dof: f64,
575 pub target_delta_sigma: Option<f64>,
576 pub target_delta_percent: Option<f64>,
577 pub max_weight_impact: f64,
578}
579
580#[derive(Serialize, Deserialize, Default, Debug, Clone)]
581#[serde(default, deny_unknown_fields)]
582pub struct IntegrationStatisticsSnapshot {
583 pub num_evals: usize,
584 pub average_total_time_seconds: f64,
585 pub average_parameterization_time_seconds: f64,
586 pub average_integrand_time_seconds: f64,
587 pub average_evaluator_time_seconds: f64,
588 pub average_observable_time_seconds: f64,
589 pub average_integrator_time_seconds: f64,
590 pub f64_percentage: f64,
591 pub f128_percentage: f64,
592 pub arb_percentage: f64,
593 pub nan_percentage: f64,
594 pub nan_or_unstable_percentage: f64,
595 pub generated_event_count: usize,
596 pub accepted_event_count: usize,
597 pub selection_efficiency_percentage: Option<f64>,
598}
599
600#[derive(Serialize, Deserialize, Default, Debug, Clone)]
601#[serde(default, deny_unknown_fields)]
602pub struct MaxWeightInfoEntry {
603 pub component: String,
604 pub sign: String,
605 pub max_eval: F<f64>,
606 pub coordinates: Option<String>,
607}
608
609#[derive(Serialize, Deserialize, Default, Debug, Clone)]
610#[serde(default, deny_unknown_fields)]
611pub struct DiscreteCoordinate {
612 pub axis_label: String,
613 pub bin_index: usize,
614 pub bin_label: Option<String>,
615}
616
617#[derive(Serialize, Deserialize, Default, Debug, Clone)]
618#[serde(default, deny_unknown_fields)]
619pub struct DiscreteBreakdownEntry {
620 pub bin_index: usize,
621 pub bin_label: Option<String>,
622 pub pdf: F<f64>,
623 pub value: F<f64>,
624 pub error: F<f64>,
625 pub chi_sq: F<f64>,
626 pub processed_samples: usize,
627}
628
629#[derive(Serialize, Deserialize, Default, Debug, Clone)]
630#[serde(default, deny_unknown_fields)]
631pub struct DiscreteBreakdown {
632 pub axis_label: String,
633 pub fixed_coordinates: Vec<DiscreteCoordinate>,
634 pub entries: Vec<DiscreteBreakdownEntry>,
635}
636
637#[derive(Serialize, Deserialize, Default, Debug, Clone)]
638#[serde(default, deny_unknown_fields)]
639pub struct ComponentDiscreteBreakdown {
640 pub re: Option<DiscreteBreakdown>,
641 pub im: Option<DiscreteBreakdown>,
642}
643
644#[derive(Serialize, Deserialize, Default, Debug, Clone)]
645#[serde(default, deny_unknown_fields)]
646pub struct SlotIntegrationResult {
647 pub key: String,
648 pub process: String,
649 pub integrand: String,
650 pub target: Option<Complex<F<f64>>>,
651 pub integral: IntegralEstimate,
652 pub table_results: Vec<IntegrationTableComponentResult>,
653 pub integration_statistics: IntegrationStatisticsSnapshot,
654 pub max_weight_info: Vec<MaxWeightInfoEntry>,
655 pub grid_breakdown: ComponentDiscreteBreakdown,
656}
657
658#[derive(Serialize, Deserialize, Default, Debug, Clone)]
659#[serde(default, deny_unknown_fields)]
660pub struct IntegrationResult {
661 pub slots: Vec<SlotIntegrationResult>,
662}
663
664impl IntegrationResult {
665 pub fn slot(&self, key: &str) -> Option<&SlotIntegrationResult> {
666 self.slots.iter().find(|slot| slot.key == key)
667 }
668
669 pub fn single_slot(&self) -> Option<&SlotIntegrationResult> {
670 (self.slots.len() == 1).then(|| &self.slots[0])
671 }
672
673 pub fn single_slot_integral(&self) -> Option<&IntegralEstimate> {
674 self.single_slot().map(|slot| &slot.integral)
675 }
676
677 pub fn is_compatible_with_target(&self, target: Complex<F<f64>>, n_sigma: i8) -> bool {
678 self.single_slot_integral()
679 .is_some_and(|integral| integral.is_compatible_with_target(target, n_sigma))
680 }
681}
682
683impl Display for IntegrationResult {
684 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
685 if let Some(slot) = self.single_slot() {
686 return slot.integral.fmt(f);
687 }
688
689 for (index, slot) in self.slots.iter().enumerate() {
690 if index > 0 {
691 writeln!(f)?;
692 }
693 write!(f, "{}: {}", slot.key, slot.integral)?;
694 }
695 Ok(())
696 }
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702 use crate::utils::serde_utils::ShowDefaultsGuard;
703 use spenso::algebra::complex::Complex;
704
705 #[test]
706 fn test_integration_result_display() {
707 let result = IntegralEstimate {
708 neval: 1000000,
709 real_zero: 0,
710 im_zero: 0,
711 result: Complex::new(F(1.23456789e-3), F(4.56789e-5)),
712 error: Complex::new(F(1.2e-5), F(3.4e-7)),
713 real_chisq: F(1.05),
714 im_chisq: F(0.98),
715 };
716
717 let display_str = format!("{}", result);
718 println!("Integration result display: {}", display_str);
719
720 let real_only_result = IntegralEstimate {
722 neval: 500000,
723 real_zero: 0,
724 im_zero: 0,
725 result: Complex::new(F(2.345e-2), F(0.0)),
726 error: Complex::new(F(1.5e-4), F(0.0)),
727 real_chisq: F(1.12),
728 im_chisq: F(0.0),
729 };
730
731 let real_display_str = format!("{}", real_only_result);
732 println!("Real-only result display: {}", real_display_str);
733
734 let precision_str = format!("{:.2}", result);
736 println!("With precision 2: {}", precision_str);
737 }
738
739 #[test]
740 fn integrator_settings_target_accuracy_defaults_stay_optional() {
741 let settings = IntegratorSettings::default();
742
743 let hidden_defaults = toml::to_string(&settings).expect("serialize integrator defaults");
744 assert!(!hidden_defaults.contains("target_relative_accuracy"));
745 assert!(!hidden_defaults.contains("target_absolute_accuracy"));
746
747 let _guard = ShowDefaultsGuard::new(true);
748 let shown_defaults = toml::to_string(&settings).expect("serialize integrator defaults");
749 assert!(shown_defaults.contains("n_bins"));
750 assert!(!shown_defaults.contains("target_relative_accuracy"));
751 assert!(!shown_defaults.contains("target_absolute_accuracy"));
752 }
753
754 #[test]
755 fn integrator_settings_target_accuracy_serializes_when_configured() {
756 let settings = IntegratorSettings {
757 target_relative_accuracy: Some(0.05),
758 target_absolute_accuracy: Some(1.0e-6),
759 ..IntegratorSettings::default()
760 };
761
762 let serialized = toml::to_string(&settings).expect("serialize configured integrator");
763 assert!(serialized.contains("target_relative_accuracy = 0.05"));
764 assert!(serialized.contains("target_absolute_accuracy = "));
765
766 let deserialized: IntegratorSettings =
767 toml::from_str(&serialized).expect("deserialize configured integrator");
768 assert_eq!(settings, deserialized);
769 }
770
771 #[test]
772 fn integral_unit_auto_resolves_by_initial_state_count() {
773 assert_eq!(
774 IntegralUnit::Auto.resolve_for_cross_section(1),
775 IntegralUnit::None
776 );
777 assert_eq!(
778 IntegralUnit::Auto.resolve_for_cross_section(2),
779 IntegralUnit::Picobarn
780 );
781 }
782
783 #[test]
784 fn integral_unit_relative_scalings_match_requested_barn_unit() {
785 let one = F(1.0f64);
786 assert_eq!(
787 IntegralUnit::Picobarn.relative_to_picobarn_factor(&one),
788 Some(F(1.0))
789 );
790 assert_eq!(
791 IntegralUnit::Femtobarn.relative_to_picobarn_factor(&one),
792 Some(F(1_000.0))
793 );
794 assert_eq!(
795 IntegralUnit::Attobarn.relative_to_picobarn_factor(&one),
796 Some(F(1_000_000.0))
797 );
798 assert_eq!(
799 IntegralUnit::Millibarn.relative_to_picobarn_factor(&one),
800 Some(F(1.0e-9))
801 );
802 assert_eq!(IntegralUnit::None.relative_to_picobarn_factor(&one), None);
803 }
804
805 #[test]
806 fn general_settings_store_mu_r_and_compute_mu_r_sq_internally() {
807 let settings = GeneralSettings {
808 mu_r: 91.188,
809 ..GeneralSettings::default()
810 };
811
812 assert_eq!(settings.mu_r, 91.188);
813 assert!((settings.mu_r_sq() - 8315.251344).abs() < 1.0e-12);
814
815 let serialized = toml::to_string(&settings).expect("serialize general settings");
816 assert!(serialized.contains("mu_r = 91.188"));
817 assert!(!serialized.contains("mu_r_sq"));
818 }
819}
820
821#[cfg_attr(
822 feature = "python_api",
823 pyo3::pyclass(from_py_object, get_all, set_all)
824)]
825#[derive(Serialize, Deserialize, Debug, Clone, Encode, Decode, PartialEq, JsonSchema)]
826#[serde(default, deny_unknown_fields)]
827pub struct StabilitySettings {
828 #[serde(skip_serializing_if = "is_default_rotation_axis")]
830 pub rotation_axis: Vec<RotationSetting>,
831 #[serde(skip_serializing_if = "is_default_stability_levels")]
833 pub levels: Vec<StabilityLevelSetting>,
834 #[serde(skip_serializing_if = "is_true")]
836 pub check_on_norm: bool,
837 #[serde(skip_serializing_if = "is_true")]
839 pub escalate_if_exact_zero: bool,
840 #[serde(skip_serializing_if = "is_float::<-1>")]
842 pub loop_momenta_norm_escalation_factor: f64,
843 #[serde(skip_serializing_if = "IsDefault::is_default")]
845 pub recording: Option<StabilityRecordingSettings>,
846}
847
848impl Default for StabilitySettings {
849 fn default() -> Self {
850 Self {
851 rotation_axis: _default_rotation_axis(),
852 levels: _default_stability_levels(),
853 check_on_norm: true,
854 escalate_if_exact_zero: false,
855 loop_momenta_norm_escalation_factor: -1.0,
856 recording: None,
857 }
858 }
859}
860
861#[cfg_attr(
862 feature = "python_api",
863 pyo3::pyclass(from_py_object, get_all, set_all)
864)]
865#[derive(Serialize, Deserialize, Debug, Clone, Copy, Encode, Decode, PartialEq, JsonSchema)]
866#[serde(default, deny_unknown_fields)]
867pub struct StabilityRecordingSettings {
868 #[serde(skip_serializing_if = "is_false")]
870 pub record_rotated_results: bool,
871 #[serde(skip_serializing_if = "is_false")]
873 pub record_all_stability_levels: bool,
874 #[serde(skip_serializing_if = "is_false")]
876 pub record_loop_momenta_escalation: bool,
877}
878
879#[allow(clippy::derivable_impls)]
880impl Default for StabilityRecordingSettings {
881 fn default() -> Self {
882 Self {
883 record_rotated_results: false,
884 record_all_stability_levels: false,
885 record_loop_momenta_escalation: false,
886 }
887 }
888}
889
890#[cfg_attr(
891 feature = "python_api",
892 pyo3::pyclass(from_py_object, get_all, set_all)
893)]
894#[derive(Serialize, Deserialize, Debug, Clone, Copy, Encode, Decode, PartialEq, JsonSchema)]
895#[serde(deny_unknown_fields)]
896pub struct StabilityLevelSetting {
897 pub precision: Precision,
899 pub required_precision_for_re: f64,
901 pub required_precision_for_im: f64,
903 pub escalate_for_large_weight_threshold: f64,
905}
906
907impl StabilityLevelSetting {
908 pub fn default_double() -> Self {
909 Self {
910 precision: Precision::Double,
911 required_precision_for_re: 1e-5,
912 required_precision_for_im: 1e-5,
913 escalate_for_large_weight_threshold: 0.9,
914 }
915 }
916
917 pub fn default_quad() -> Self {
918 Self {
919 precision: Precision::Quad,
920 required_precision_for_re: 1e-5,
921 required_precision_for_im: 1e-5,
922 escalate_for_large_weight_threshold: -1.0,
923 }
924 }
925
926 pub fn default_arb() -> Self {
927 Self {
928 precision: Precision::Arb,
929 required_precision_for_re: 1e-5,
930 required_precision_for_im: 1e-5,
931 escalate_for_large_weight_threshold: -1.0,
932 }
933 }
934}
935
936#[cfg_attr(
937 feature = "python_api",
938 pyo3::pyclass(from_py_object, get_all, set_all)
939)]
940#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Encode, Decode, JsonSchema)]
941#[serde(tag = "type")]
942#[serde(deny_unknown_fields)]
943pub enum RotationSetting {
944 #[serde(rename = "x")]
945 Pi2X {},
946 #[serde(rename = "y")]
947 Pi2Y {},
948 #[serde(rename = "z")]
949 Pi2Z {},
950 #[serde(rename = "none")]
951 None {},
952 #[serde(rename = "euler_angles")]
953 EulerAngles {
954 alpha: f64,
956 beta: f64,
958 gamma: f64,
960 },
961}
962
963impl Default for RotationSetting {
964 fn default() -> Self {
965 Self::Pi2Z {}
966 }
967}
968
969impl RotationSetting {
970 pub(crate) fn rotation_method(&self) -> RotationMethod {
971 match self {
972 Self::Pi2X {} => RotationMethod::Pi2X,
973 Self::Pi2Y {} => RotationMethod::Pi2Y,
974 Self::Pi2Z {} => RotationMethod::Pi2Z,
975 Self::None {} => RotationMethod::Identity,
976 Self::EulerAngles { alpha, beta, gamma } => {
977 RotationMethod::EulerAngles(*alpha, *beta, *gamma)
978 }
979 }
980 }
981
982 pub(crate) fn _as_str(&self) -> String {
983 match self {
984 Self::Pi2X {} => "x".to_owned(),
985 Self::Pi2Y {} => "y".to_owned(),
986 Self::Pi2Z {} => "z".to_owned(),
987 Self::None {} => "none".to_owned(),
988 Self::EulerAngles { alpha, beta, gamma } => {
989 format!("euler {} {} {}", alpha, beta, gamma)
990 }
991 }
992 }
993}
994
995#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
996#[derive(
997 Serialize, Deserialize, Debug, Clone, PartialEq, Copy, Hash, Eq, Encode, Decode, JsonSchema,
998)]
999#[serde(deny_unknown_fields)]
1000pub enum Precision {
1001 Double,
1002 Quad,
1003 Arb,
1004}
1005
1006impl Display for Precision {
1007 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1008 match self {
1009 Self::Double => write!(f, "f64"),
1010 Self::Quad => write!(f, "f128"),
1011 Self::Arb => write!(f, "arb"),
1012 }
1013 }
1014}
1015
1016#[cfg_attr(
1017 feature = "python_api",
1018 pyo3::pyclass(from_py_object, get_all, set_all)
1019)]
1020#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, Encode, Decode, JsonSchema)]
1021#[serde(deny_unknown_fields)]
1022pub enum ParameterizationMode {
1023 #[serde(rename = "cartesian")]
1024 Cartesian,
1025 #[serde(rename = "spherical")]
1026 #[default]
1027 Spherical,
1028 #[serde(rename = "hyperspherical")]
1029 HyperSpherical,
1030 #[serde(rename = "hyperspherical_flat")]
1031 HyperSphericalFlat,
1032 #[serde(rename = "momentum_space")]
1033 MomentumSpace,
1034 #[serde(rename = "relative_spherical")]
1035 RelativeSpherical,
1036 #[serde(rename = "spherical_common_radial")]
1037 SphericalCommonRadial,
1038 #[serde(rename = "spherical_product_common_radial")]
1039 SphericalProductCommonRadial,
1040}
1041
1042#[cfg_attr(
1043 feature = "python_api",
1044 pyo3::pyclass(from_py_object, get_all, set_all)
1045)]
1046#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1047#[serde(default, deny_unknown_fields)]
1048pub struct OutputMetadata {
1049 #[serde(skip_serializing_if = "IsDefault::is_default")]
1050 pub model_name: String,
1051 #[serde(skip_serializing_if = "IsDefault::is_default")]
1052 pub output_type: String,
1053 #[serde(skip_serializing_if = "IsDefault::is_default")]
1054 pub contents: Vec<String>,
1055}
1056
1057impl Display for ParameterizationMode {
1058 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1059 match self {
1060 ParameterizationMode::Cartesian => write!(f, "cartesian"),
1061 ParameterizationMode::Spherical => write!(f, "spherical"),
1062 ParameterizationMode::RelativeSpherical => write!(f, "relative spherical"),
1063 ParameterizationMode::SphericalCommonRadial => {
1064 write!(f, "common-radial spherical")
1065 }
1066 ParameterizationMode::SphericalProductCommonRadial => {
1067 write!(f, "product/common-radial spherical")
1068 }
1069 ParameterizationMode::HyperSpherical => write!(f, "hyperspherical"),
1070 ParameterizationMode::HyperSphericalFlat => write!(f, "flat hyperspherical"),
1071 ParameterizationMode::MomentumSpace => write!(f, "momentum space"),
1072 }
1073 }
1074}
1075
1076#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
1077#[derive(Debug, Clone, Deserialize, PartialEq, Default, Serialize, Encode, Decode, JsonSchema)]
1078#[serde(deny_unknown_fields)]
1079pub enum ParameterizationMapping {
1080 #[serde(rename = "log")]
1081 Log,
1082 #[serde(rename = "power")]
1083 Power,
1084 #[serde(rename = "linear")]
1085 #[default]
1086 Linear,
1087}
1088
1089#[derive(Debug, Clone, PartialEq, Encode, Decode)]
1090#[cfg_attr(
1091 feature = "python_api",
1092 pyo3::pyclass(from_py_object, get_all, set_all)
1093)]
1094pub enum SamplingSettings {
1095 Default(ParameterizationSettings),
1096 MultiChanneling(MultiChannelingSettings),
1097 DiscreteGraphs(DiscreteGraphSamplingSettings),
1098}
1099
1100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
1101#[cfg_attr(
1102 feature = "python_api",
1103 pyo3::pyclass(from_py_object, get_all, set_all)
1104)]
1105#[serde(default, deny_unknown_fields)]
1106pub struct SamplingSettingsParser {
1107 #[serde(skip_serializing_if = "IsDefault::is_default")]
1109 pub graphs: SumMode,
1110 #[serde(skip_serializing_if = "IsDefault::is_default")]
1112 pub graph_names: Vec<String>,
1113 #[serde(skip_serializing_if = "IsDefault::is_default")]
1115 pub orientations: SumMode,
1116 #[serde(skip_serializing_if = "is_false")]
1118 pub lmb_multichanneling: bool,
1119 #[serde(skip_serializing_if = "IsDefault::is_default")]
1121 pub lmb_channels: SumMode,
1122 #[serde(skip_serializing_if = "is_float::<3>")]
1124 pub alpha: f64,
1125 #[serde(skip_serializing_if = "IsDefault::is_default")]
1127 pub lmb_channel_weight: LmbChannelWeight,
1128 #[serde(skip_serializing_if = "IsDefault::is_default")]
1130 pub coordinate_system: CoordinateSystem,
1131 #[serde(skip_serializing_if = "IsDefault::is_default")]
1133 pub mapping: ParameterizationMapping,
1134 #[serde(skip_serializing_if = "is_float::<1>")]
1136 pub b: f64,
1137 #[serde(skip_serializing_if = "is_float::<1>")]
1139 pub power: f64,
1140 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
1142 pub lmb_basis_ids: BTreeMap<String, Vec<usize>>,
1143}
1144
1145impl Default for SamplingSettingsParser {
1146 fn default() -> Self {
1147 Self {
1148 graphs: SumMode::Summed,
1149 graph_names: Vec::new(),
1150 orientations: SumMode::Summed,
1151 lmb_multichanneling: false,
1152 lmb_channels: SumMode::Summed,
1153 alpha: 3.0,
1154 lmb_channel_weight: LmbChannelWeight::default(),
1155 coordinate_system: CoordinateSystem::Spherical,
1156 mapping: ParameterizationMapping::Linear,
1157 b: 1.0,
1158 power: 1.0,
1159 lmb_basis_ids: BTreeMap::new(),
1160 }
1161 }
1162}
1163
1164#[derive(
1165 Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Encode, Decode, JsonSchema,
1166)]
1167#[cfg_attr(
1168 feature = "python_api",
1169 pyo3::pyclass(from_py_object, get_all, set_all)
1170)]
1171pub enum LmbChannelWeight {
1172 #[serde(rename = "ose")]
1173 #[default]
1174 Ose,
1175 #[serde(rename = "inverse_jacobian")]
1176 InverseJacobian,
1177}
1178
1179#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
1180#[cfg_attr(
1181 feature = "python_api",
1182 pyo3::pyclass(from_py_object, get_all, set_all)
1183)]
1184pub enum SumMode {
1185 #[serde(rename = "summed")]
1186 #[default]
1187 Summed,
1188 #[serde(rename = "monte_carlo")]
1189 MonteCarlo,
1190}
1191
1192#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema, Default)]
1193#[cfg_attr(
1194 feature = "python_api",
1195 pyo3::pyclass(from_py_object, get_all, set_all)
1196)]
1197pub enum CoordinateSystem {
1198 #[serde(rename = "cartesian")]
1199 Cartesian,
1200 #[serde(rename = "spherical")]
1201 #[default]
1202 Spherical,
1203 #[serde(rename = "hyperspherical")]
1204 HyperSpherical,
1205 #[serde(rename = "hyperspherical_flat")]
1206 HyperSphericalFlat,
1207 #[serde(rename = "momentum_space")]
1208 MomentumSpace,
1209 #[serde(rename = "tropical")]
1210 MomTrop,
1211 #[serde(rename = "relative_spherical")]
1212 RelativeSpherical,
1213 #[serde(rename = "spherical_common_radial")]
1214 SphericalCommonRadial,
1215 #[serde(rename = "spherical_product_common_radial")]
1216 SphericalProductCommonRadial,
1217}
1218
1219impl Default for SamplingSettings {
1220 fn default() -> Self {
1221 Self::Default(ParameterizationSettings::default())
1222 }
1223}
1224
1225fn validate_lmb_basis_ids(lmb_basis_ids: &BTreeMap<String, Vec<usize>>) -> Result<(), String> {
1226 for (graph_name, basis_ids) in lmb_basis_ids {
1227 if basis_ids.is_empty() {
1228 return Err(format!(
1229 "Invalid sampling settings: lmb_basis_ids entry for graph '{graph_name}' cannot be empty."
1230 ));
1231 }
1232
1233 let mut seen = BTreeSet::new();
1234 for basis_id in basis_ids {
1235 if !seen.insert(*basis_id) {
1236 return Err(format!(
1237 "Invalid sampling settings: lmb_basis_ids entry for graph '{graph_name}' contains duplicate basis id {basis_id}."
1238 ));
1239 }
1240 }
1241 }
1242
1243 Ok(())
1244}
1245
1246impl SamplingSettings {
1247 fn as_parser(&self) -> SamplingSettingsParser {
1248 match self {
1249 SamplingSettings::Default(settings) => SamplingSettingsParser {
1250 graphs: SumMode::Summed,
1251 graph_names: Vec::new(),
1252 orientations: SumMode::Summed,
1253 lmb_multichanneling: false,
1254 lmb_channels: SumMode::Summed,
1255 alpha: 3.0,
1256 lmb_channel_weight: LmbChannelWeight::default(),
1257 coordinate_system: CoordinateSystem::from_mode(settings.mode.clone()),
1258 mapping: settings.mapping.clone(),
1259 b: settings.b,
1260 power: settings.power,
1261 lmb_basis_ids: settings.lmb_basis_ids.clone(),
1262 },
1263 SamplingSettings::MultiChanneling(settings) => SamplingSettingsParser {
1264 graphs: SumMode::Summed,
1265 graph_names: Vec::new(),
1266 orientations: SumMode::Summed,
1267 lmb_multichanneling: true,
1268 lmb_channels: SumMode::Summed,
1269 alpha: settings.alpha,
1270 lmb_channel_weight: settings.channel_weight,
1271 coordinate_system: CoordinateSystem::from_mode(
1272 settings.parameterization_settings.mode.clone(),
1273 ),
1274 mapping: settings.parameterization_settings.mapping.clone(),
1275 b: settings.parameterization_settings.b,
1276 power: settings.parameterization_settings.power,
1277 lmb_basis_ids: settings.parameterization_settings.lmb_basis_ids.clone(),
1278 },
1279 SamplingSettings::DiscreteGraphs(settings) => {
1280 let orientations = if settings.sample_orientations {
1281 SumMode::MonteCarlo
1282 } else {
1283 SumMode::Summed
1284 };
1285
1286 match &settings.sampling_type {
1287 DiscreteGraphSamplingType::Default(parameterization_settings) => {
1288 SamplingSettingsParser {
1289 graphs: SumMode::MonteCarlo,
1290 graph_names: settings.graph_names.clone(),
1291 orientations,
1292 lmb_multichanneling: false,
1293 lmb_channels: SumMode::Summed,
1294 alpha: 3.0,
1295 lmb_channel_weight: LmbChannelWeight::default(),
1296 coordinate_system: CoordinateSystem::from_mode(
1297 parameterization_settings.mode.clone(),
1298 ),
1299 mapping: parameterization_settings.mapping.clone(),
1300 b: parameterization_settings.b,
1301 power: parameterization_settings.power,
1302 lmb_basis_ids: parameterization_settings.lmb_basis_ids.clone(),
1303 }
1304 }
1305 DiscreteGraphSamplingType::MultiChanneling(multichanneling_settings) => {
1306 SamplingSettingsParser {
1307 graphs: SumMode::MonteCarlo,
1308 graph_names: settings.graph_names.clone(),
1309 orientations,
1310 lmb_multichanneling: true,
1311 lmb_channels: SumMode::Summed,
1312 alpha: multichanneling_settings.alpha,
1313 lmb_channel_weight: multichanneling_settings.channel_weight,
1314 coordinate_system: CoordinateSystem::from_mode(
1315 multichanneling_settings
1316 .parameterization_settings
1317 .mode
1318 .clone(),
1319 ),
1320 mapping: multichanneling_settings
1321 .parameterization_settings
1322 .mapping
1323 .clone(),
1324 b: multichanneling_settings.parameterization_settings.b,
1325 power: multichanneling_settings.parameterization_settings.power,
1326 lmb_basis_ids: multichanneling_settings
1327 .parameterization_settings
1328 .lmb_basis_ids
1329 .clone(),
1330 }
1331 }
1332 DiscreteGraphSamplingType::DiscreteMultiChanneling(
1333 multichanneling_settings,
1334 ) => SamplingSettingsParser {
1335 graphs: SumMode::MonteCarlo,
1336 graph_names: settings.graph_names.clone(),
1337 orientations,
1338 lmb_multichanneling: true,
1339 lmb_channels: SumMode::MonteCarlo,
1340 alpha: multichanneling_settings.alpha,
1341 lmb_channel_weight: multichanneling_settings.channel_weight,
1342 coordinate_system: CoordinateSystem::from_mode(
1343 multichanneling_settings
1344 .parameterization_settings
1345 .mode
1346 .clone(),
1347 ),
1348 mapping: multichanneling_settings
1349 .parameterization_settings
1350 .mapping
1351 .clone(),
1352 b: multichanneling_settings.parameterization_settings.b,
1353 power: multichanneling_settings.parameterization_settings.power,
1354 lmb_basis_ids: multichanneling_settings
1355 .parameterization_settings
1356 .lmb_basis_ids
1357 .clone(),
1358 },
1359 DiscreteGraphSamplingType::TropicalSampling(_) => SamplingSettingsParser {
1360 graphs: SumMode::MonteCarlo,
1361 graph_names: settings.graph_names.clone(),
1362 orientations,
1363 lmb_multichanneling: false,
1364 lmb_channels: SumMode::Summed,
1365 alpha: 3.0,
1366 lmb_channel_weight: LmbChannelWeight::default(),
1367 coordinate_system: CoordinateSystem::MomTrop,
1368 mapping: ParameterizationMapping::default(),
1369 b: 1.0,
1370 power: 1.0,
1371 lmb_basis_ids: BTreeMap::new(),
1372 },
1373 }
1374 }
1375 }
1376 }
1377
1378 fn from_parser(parser: SamplingSettingsParser) -> Result<Self, String> {
1379 let SamplingSettingsParser {
1380 graphs,
1381 graph_names,
1382 orientations,
1383 lmb_multichanneling,
1384 lmb_channels,
1385 alpha,
1386 lmb_channel_weight,
1387 coordinate_system,
1388 mapping,
1389 b,
1390 power,
1391 lmb_basis_ids,
1392 } = parser;
1393
1394 validate_lmb_basis_ids(&lmb_basis_ids)?;
1395
1396 let mut seen_graph_names = BTreeSet::new();
1397 for graph_name in &graph_names {
1398 if !seen_graph_names.insert(graph_name) {
1399 return Err(format!(
1400 "Invalid sampling settings: graph_names contains duplicate graph name '{graph_name}'."
1401 ));
1402 }
1403 }
1404 if !graph_names.is_empty() && matches!(graphs, SumMode::Summed) {
1405 return Err(
1406 "Invalid sampling settings: graph_names requires graphs = 'monte_carlo'."
1407 .to_string(),
1408 );
1409 }
1410
1411 let sample_orientations = match (graphs.clone(), orientations) {
1412 (SumMode::Summed, SumMode::Summed) => false,
1413 (SumMode::Summed, SumMode::MonteCarlo) => {
1414 return Err(
1415 "Invalid sampling settings: orientations can only be set to 'monte_carlo' when graphs is 'monte_carlo'.".to_string(),
1416 )
1417 }
1418 (SumMode::MonteCarlo, SumMode::Summed) => false,
1419 (SumMode::MonteCarlo, SumMode::MonteCarlo) => true,
1420 };
1421
1422 if matches!(coordinate_system, CoordinateSystem::MomTrop) {
1423 if !lmb_basis_ids.is_empty() {
1424 return Err(
1425 "Invalid sampling settings: coordinate_system = 'tropical' is incompatible with lmb_basis_ids."
1426 .to_string(),
1427 );
1428 }
1429 if !matches!(graphs, SumMode::MonteCarlo) {
1430 return Err(
1431 "Invalid sampling settings: coordinate_system = 'tropical' requires graphs = 'monte_carlo'."
1432 .to_string(),
1433 );
1434 }
1435
1436 if lmb_multichanneling {
1437 return Err(
1438 "Invalid sampling settings: coordinate_system = 'tropical' is incompatible with lmb_multichanneling = true."
1439 .to_string(),
1440 );
1441 }
1442
1443 return Ok(SamplingSettings::DiscreteGraphs(
1444 DiscreteGraphSamplingSettings {
1445 graph_names,
1446 sample_orientations,
1447 sampling_type: DiscreteGraphSamplingType::TropicalSampling(
1448 GammaloopTropicalSamplingSettings::default(),
1449 ),
1450 },
1451 ));
1452 }
1453
1454 let mode = coordinate_system.into_mode();
1455 if matches!(mode, ParameterizationMode::SphericalProductCommonRadial)
1456 && (!lmb_multichanneling || lmb_channel_weight != LmbChannelWeight::InverseJacobian)
1457 {
1458 return Err(
1459 "Invalid sampling settings: coordinate_system = 'spherical_product_common_radial' requires lmb_multichanneling = true and lmb_channel_weight = 'inverse_jacobian'."
1460 .to_string(),
1461 );
1462 }
1463 if lmb_channel_weight == LmbChannelWeight::InverseJacobian
1464 && matches!(mode, ParameterizationMode::HyperSphericalFlat)
1465 {
1466 return Err(
1467 "Invalid sampling settings: lmb_channel_weight = 'inverse_jacobian' is incompatible with coordinate_system = 'hyperspherical_flat' because the inverse map is not available."
1468 .to_string(),
1469 );
1470 }
1471 if matches!(mapping, ParameterizationMapping::Power) {
1472 if !matches!(
1473 mode,
1474 ParameterizationMode::Spherical
1475 | ParameterizationMode::RelativeSpherical
1476 | ParameterizationMode::SphericalCommonRadial
1477 | ParameterizationMode::SphericalProductCommonRadial
1478 | ParameterizationMode::HyperSpherical
1479 | ParameterizationMode::HyperSphericalFlat
1480 ) {
1481 return Err(
1482 "Invalid sampling settings: mapping = 'power' requires a spherical coordinate system."
1483 .to_string(),
1484 );
1485 }
1486 if !power.is_finite() || power < 1.0 {
1487 return Err(
1488 "Invalid sampling settings: mapping = 'power' requires power >= 1.".to_string(),
1489 );
1490 }
1491 }
1492 let parameterization_settings = ParameterizationSettings {
1493 mode,
1494 mapping,
1495 b,
1496 power,
1497 lmb_basis_ids,
1498 };
1499
1500 match graphs {
1501 SumMode::Summed => {
1502 if !matches!(lmb_channels, SumMode::Summed) {
1503 return Err(
1504 "Invalid sampling settings: lmb_channels = 'monte_carlo' requires graphs = 'monte_carlo'."
1505 .to_string(),
1506 );
1507 }
1508
1509 if sample_orientations {
1510 return Err(
1511 "Invalid sampling settings: orientations can only be Monte Carlo sampled when graphs are Monte Carlo sampled."
1512 .to_string(),
1513 );
1514 }
1515
1516 if lmb_multichanneling {
1517 Ok(SamplingSettings::MultiChanneling(MultiChannelingSettings {
1518 alpha,
1519 channel_weight: lmb_channel_weight,
1520 parameterization_settings,
1521 }))
1522 } else {
1523 Ok(SamplingSettings::Default(parameterization_settings))
1524 }
1525 }
1526 SumMode::MonteCarlo => {
1527 let sampling_type = if lmb_multichanneling {
1528 let settings = MultiChannelingSettings {
1529 alpha,
1530 channel_weight: lmb_channel_weight,
1531 parameterization_settings,
1532 };
1533
1534 match lmb_channels {
1535 SumMode::Summed => DiscreteGraphSamplingType::MultiChanneling(settings),
1536 SumMode::MonteCarlo => {
1537 DiscreteGraphSamplingType::DiscreteMultiChanneling(settings)
1538 }
1539 }
1540 } else {
1541 DiscreteGraphSamplingType::Default(parameterization_settings)
1542 };
1543
1544 Ok(SamplingSettings::DiscreteGraphs(
1545 DiscreteGraphSamplingSettings {
1546 graph_names,
1547 sample_orientations,
1548 sampling_type,
1549 },
1550 ))
1551 }
1552 }
1553 }
1554}
1555
1556impl Serialize for SamplingSettings {
1557 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1558 where
1559 S: Serializer,
1560 {
1561 self.as_parser().serialize(serializer)
1562 }
1563}
1564
1565impl<'de> Deserialize<'de> for SamplingSettings {
1566 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1567 where
1568 D: Deserializer<'de>,
1569 {
1570 let parser = SamplingSettingsParser::deserialize(deserializer)?;
1571 SamplingSettings::from_parser(parser).map_err(serde::de::Error::custom)
1572 }
1573}
1574
1575impl JsonSchema for SamplingSettings {
1576 fn schema_name() -> std::borrow::Cow<'static, str> {
1577 "SamplingSettings".into()
1578 }
1579
1580 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1581 SamplingSettingsParser::json_schema(generator)
1582 }
1583}
1584
1585impl CoordinateSystem {
1586 fn from_mode(mode: ParameterizationMode) -> Self {
1587 match mode {
1588 ParameterizationMode::Cartesian => Self::Cartesian,
1589 ParameterizationMode::Spherical => Self::Spherical,
1590 ParameterizationMode::RelativeSpherical => Self::RelativeSpherical,
1591 ParameterizationMode::SphericalCommonRadial => Self::SphericalCommonRadial,
1592 ParameterizationMode::SphericalProductCommonRadial => {
1593 Self::SphericalProductCommonRadial
1594 }
1595 ParameterizationMode::HyperSpherical => Self::HyperSpherical,
1596 ParameterizationMode::HyperSphericalFlat => Self::HyperSphericalFlat,
1597 ParameterizationMode::MomentumSpace => Self::MomentumSpace,
1598 }
1599 }
1600
1601 fn into_mode(self) -> ParameterizationMode {
1602 match self {
1603 CoordinateSystem::Cartesian => ParameterizationMode::Cartesian,
1604 CoordinateSystem::Spherical => ParameterizationMode::Spherical,
1605 CoordinateSystem::RelativeSpherical => ParameterizationMode::RelativeSpherical,
1606 CoordinateSystem::SphericalCommonRadial => ParameterizationMode::SphericalCommonRadial,
1607 CoordinateSystem::SphericalProductCommonRadial => {
1608 ParameterizationMode::SphericalProductCommonRadial
1609 }
1610 CoordinateSystem::HyperSpherical => ParameterizationMode::HyperSpherical,
1611 CoordinateSystem::HyperSphericalFlat => ParameterizationMode::HyperSphericalFlat,
1612 CoordinateSystem::MomentumSpace => ParameterizationMode::MomentumSpace,
1613 CoordinateSystem::MomTrop => {
1614 unreachable!("tropical coordinate system has no ParameterizationMode equivalent")
1615 }
1616 }
1617 }
1618}
1619
1620impl SamplingSettings {
1621 pub fn selected_graph_names(&self) -> &[String] {
1622 match self {
1623 SamplingSettings::DiscreteGraphs(settings) => &settings.graph_names,
1624 SamplingSettings::Default(_) | SamplingSettings::MultiChanneling(_) => &[],
1625 }
1626 }
1627
1628 pub fn get_parameterization_settings(&self) -> Option<ParameterizationSettings> {
1629 match self {
1630 SamplingSettings::Default(settings) => Some(settings.clone()),
1631 SamplingSettings::MultiChanneling(settings) => {
1632 Some(settings.parameterization_settings.clone())
1633 }
1634 SamplingSettings::DiscreteGraphs(settings) => match &settings.sampling_type {
1635 DiscreteGraphSamplingType::Default(settings) => Some(settings.clone()),
1636 DiscreteGraphSamplingType::MultiChanneling(settings) => {
1637 Some(settings.parameterization_settings.clone())
1638 }
1639 DiscreteGraphSamplingType::DiscreteMultiChanneling(settings) => {
1640 Some(settings.parameterization_settings.clone())
1641 }
1642 DiscreteGraphSamplingType::TropicalSampling(_) => None,
1643 },
1644 }
1645 }
1646
1647 pub(crate) fn discrete_depth(&self) -> usize {
1648 match self {
1649 SamplingSettings::Default(_) => 0,
1650 SamplingSettings::MultiChanneling(_) => 0,
1651 SamplingSettings::DiscreteGraphs(settings) => {
1652 let depth_from_orientations = settings.sample_orientations as usize;
1653
1654 match &settings.sampling_type {
1655 DiscreteGraphSamplingType::Default(_) => 1 + depth_from_orientations,
1656 DiscreteGraphSamplingType::MultiChanneling(_) => 1 + depth_from_orientations,
1657 DiscreteGraphSamplingType::DiscreteMultiChanneling(_) => {
1658 2 + depth_from_orientations
1659 }
1660 DiscreteGraphSamplingType::TropicalSampling(_) => 1 + depth_from_orientations,
1661 }
1662 }
1663 }
1664 }
1665
1666 pub(crate) fn describe_settings(&self) -> String {
1667 match self {
1668 SamplingSettings::Default(settings) => {
1669 format!("{} coordinates", settings.mode)
1670 }
1671 SamplingSettings::MultiChanneling(settings) => {
1672 format!(
1673 "lmb multichanneling in {} coordinates",
1674 settings.parameterization_settings.mode
1675 )
1676 }
1677 SamplingSettings::DiscreteGraphs(settings) => {
1678 let discrete_graph_string = if settings.graph_names.is_empty() {
1679 "Monte Carlo over graphs".to_string()
1680 } else {
1681 format!(
1682 "Monte Carlo over selected graph groups [{}]",
1683 settings.graph_names.join(", ")
1684 )
1685 };
1686 let orientation_sampling_string = if settings.sample_orientations {
1687 "and Monte Carlo over orientations"
1688 } else {
1689 ""
1690 };
1691
1692 match &settings.sampling_type {
1693 DiscreteGraphSamplingType::Default(settings) => {
1694 format!(
1695 "{} {} in {} coordinates",
1696 discrete_graph_string, orientation_sampling_string, settings.mode
1697 )
1698 }
1699 DiscreteGraphSamplingType::MultiChanneling(settings) => {
1700 format!(
1701 "{}, lmb multichanneling in {} coordinates {}",
1702 discrete_graph_string,
1703 settings.parameterization_settings.mode,
1704 orientation_sampling_string,
1705 )
1706 }
1707 DiscreteGraphSamplingType::DiscreteMultiChanneling(settings) => {
1708 format!(
1709 "{}, {} and monte carlo over lmbs in {} coordinates",
1710 discrete_graph_string,
1711 orientation_sampling_string,
1712 settings.parameterization_settings.mode
1713 )
1714 }
1715 DiscreteGraphSamplingType::TropicalSampling(_) => {
1716 format!(
1717 "{} {} using 🌴🥥 tropical sampling 🥥🌴",
1718 discrete_graph_string, orientation_sampling_string,
1719 )
1720 }
1721 }
1722 }
1723 }
1724 }
1725}
1726
1727#[cfg_attr(
1728 feature = "python_api",
1729 pyo3::pyclass(from_py_object, get_all, set_all)
1730)]
1731#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
1732#[serde(deny_unknown_fields, default)]
1733pub struct MultiChannelingSettings {
1734 #[serde(skip_serializing_if = "is_float::<3>")]
1736 pub alpha: f64,
1737 #[serde(skip_serializing_if = "IsDefault::is_default")]
1739 pub channel_weight: LmbChannelWeight,
1740 #[serde(skip_serializing_if = "IsDefault::is_default")]
1742 pub parameterization_settings: ParameterizationSettings,
1743}
1744
1745impl Default for MultiChannelingSettings {
1746 fn default() -> Self {
1747 Self {
1748 alpha: 3.0,
1749 channel_weight: LmbChannelWeight::default(),
1750 parameterization_settings: ParameterizationSettings::default(),
1751 }
1752 }
1753}
1754
1755#[cfg_attr(
1756 feature = "python_api",
1757 pyo3::pyclass(from_py_object, get_all, set_all)
1758)]
1759#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
1760#[serde(deny_unknown_fields, default)]
1761pub struct GammaloopTropicalSamplingSettings {
1762 #[serde(skip_serializing_if = "is_true")]
1764 pub upcast_on_failure: bool,
1765 #[serde(skip_serializing_if = "IsDefault::is_default")]
1767 pub matrix_stability_test: Option<f64>,
1768}
1769
1770impl Default for GammaloopTropicalSamplingSettings {
1771 fn default() -> Self {
1772 Self {
1773 upcast_on_failure: true,
1774 matrix_stability_test: None,
1775 }
1776 }
1777}
1778
1779impl GammaloopTropicalSamplingSettings {
1780 pub fn into_tropical_sampling_settings(&self) -> momtrop::TropicalSamplingSettings {
1781 momtrop::TropicalSamplingSettings {
1782 matrix_stability_test: self.matrix_stability_test,
1783 print_debug_info: false,
1784 return_metadata: false,
1785 }
1786 }
1787}
1788
1789#[cfg_attr(
1790 feature = "python_api",
1791 pyo3::pyclass(from_py_object, get_all, set_all)
1792)]
1793#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
1794#[serde(tag = "subtype", deny_unknown_fields)]
1795pub enum DiscreteGraphSamplingType {
1796 #[serde(rename = "default")]
1797 Default(ParameterizationSettings),
1798 #[serde(rename = "multi_channeling")]
1799 MultiChanneling(MultiChannelingSettings),
1800 #[serde(rename = "discrete_multi_channeling")]
1801 DiscreteMultiChanneling(MultiChannelingSettings),
1802 #[serde(rename = "tropical")]
1803 TropicalSampling(GammaloopTropicalSamplingSettings),
1804}
1805
1806impl Default for DiscreteGraphSamplingType {
1807 fn default() -> Self {
1808 DiscreteGraphSamplingType::Default(ParameterizationSettings::default())
1809 }
1810}
1811
1812#[cfg_attr(
1813 feature = "python_api",
1814 pyo3::pyclass(from_py_object, get_all, set_all)
1815)]
1816#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema, Default)]
1817#[serde(deny_unknown_fields, default)]
1818pub struct DiscreteGraphSamplingSettings {
1819 #[serde(skip_serializing_if = "IsDefault::is_default")]
1821 pub graph_names: Vec<String>,
1822 #[serde(skip_serializing_if = "is_false")]
1824 pub sample_orientations: bool,
1825 #[serde(skip_serializing_if = "IsDefault::is_default")]
1827 pub sampling_type: DiscreteGraphSamplingType,
1828}
1829
1830#[cfg_attr(
1831 feature = "python_api",
1832 pyo3::pyclass(from_py_object, get_all, set_all)
1833)]
1834#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
1835#[serde(deny_unknown_fields, default)]
1836pub struct LocalCounterTermSettings {
1837 #[serde(skip_serializing_if = "IsDefault::is_default")]
1839 pub uv_localisation: UVLocalisationSettings,
1840}
1841
1842#[cfg_attr(
1843 feature = "python_api",
1844 pyo3::pyclass(from_py_object, get_all, set_all)
1845)]
1846#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
1847#[serde(default, deny_unknown_fields)]
1848pub struct UVLocalisationSettings {
1849 #[serde(skip_serializing_if = "is_float::<10>")]
1851 pub sliver_width: f64,
1852 #[serde(skip_serializing_if = "is_false")]
1854 pub dynamic_width: bool,
1855 #[serde(skip_serializing_if = "is_float::<1>")]
1857 pub gaussian_width: f64,
1858 #[serde(skip_serializing_if = "is_false")]
1860 pub force_uv_dampers_to_one: bool,
1861}
1862
1863impl Default for UVLocalisationSettings {
1864 fn default() -> Self {
1865 Self {
1866 sliver_width: 10.0,
1867 dynamic_width: false,
1868 gaussian_width: 1.0,
1869 force_uv_dampers_to_one: false,
1870 }
1871 }
1872}
1873
1874#[cfg_attr(
1875 feature = "python_api",
1876 pyo3::pyclass(from_py_object, get_all, set_all)
1877)]
1878#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
1879#[serde(deny_unknown_fields, default)]
1880pub struct IntegratedCounterTermSettings {
1881 #[serde(skip_serializing_if = "IsDefault::is_default")]
1883 pub range: IntegratedCounterTermRange,
1884}
1885
1886#[cfg_attr(
1887 feature = "python_api",
1888 pyo3::pyclass(from_py_object, get_all, set_all)
1889)]
1890#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
1891#[serde(tag = "type")]
1892#[serde(deny_unknown_fields)]
1893pub enum IntegratedCounterTermRange {
1894 #[serde(rename = "infinite")]
1896 Infinite {
1897 h_function_settings: HFunctionSettings,
1899 },
1900 #[serde(rename = "compact")]
1902 Compact {},
1903}
1904
1905impl Default for IntegratedCounterTermRange {
1906 fn default() -> Self {
1907 Self::Infinite {
1908 h_function_settings: HFunctionSettings::default(),
1909 }
1910 }
1911}
1912
1913#[cfg_attr(
1914 feature = "python_api",
1915 pyo3::pyclass(from_py_object, get_all, set_all)
1916)]
1917#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
1918#[serde(deny_unknown_fields)]
1919#[serde(default)]
1920pub struct OverlapSettings {
1921 #[serde(skip_serializing_if = "IsDefault::is_default")]
1924 pub force_global_center: Option<Vec<[f64; 3]>>,
1925 #[serde(skip_serializing_if = "is_true")]
1927 pub check_global_center: bool,
1928 #[serde(skip_serializing_if = "is_true")]
1930 pub try_origin: bool,
1931 #[serde(skip_serializing_if = "is_false")]
1933 pub try_origin_all_lmbs: bool,
1934}
1935
1936impl Default for OverlapSettings {
1937 fn default() -> Self {
1938 Self {
1939 force_global_center: None,
1940 check_global_center: true,
1941 try_origin: true,
1942 try_origin_all_lmbs: false,
1943 }
1944 }
1945}
1946
1947#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
1948#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
1949#[serde(deny_unknown_fields)]
1950pub enum HFunction {
1952 #[default]
1953 #[serde(rename = "poly_exponential")]
1954 PolyExponential,
1955 #[serde(rename = "exponential")]
1956 Exponential,
1957 #[serde(rename = "poly_left_right_exponential")]
1958 PolyLeftRightExponential,
1959 #[serde(rename = "exponential_ct")]
1960 ExponentialCT,
1961}
1962
1963#[cfg_attr(feature = "python_api", pyo3::pyclass(from_py_object))]
1964#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
1965#[serde(deny_unknown_fields)]
1966#[serde(default)]
1967pub struct HFunctionSettings {
1968 #[serde(skip_serializing_if = "IsDefault::is_default")]
1970 pub function: HFunction,
1971 #[serde(skip_serializing_if = "is_float::<1>")]
1973 pub sigma: f64,
1974 #[serde(skip_serializing_if = "is_true")]
1976 pub enabled_dampening: bool,
1977 #[serde(skip_serializing_if = "IsDefault::is_default")]
1979 pub power: Option<usize>,
1980}
1981
1982impl Default for HFunctionSettings {
1983 fn default() -> Self {
1984 Self {
1985 sigma: 1.0,
1986 function: HFunction::default(),
1987 enabled_dampening: true,
1988 power: None,
1989 }
1990 }
1991}