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::SamplingMapDefinition,
20 integrands::process::evaluators::EvaluatorMethod,
21 momentum::{Helicity, RotationMethod, sample::ExternalIndex, signature::SignatureLike},
22 observables::ObservableFileFormat,
23 settings::runtime::kinematic::{Externals, improvement::generate_default_momenta},
24 utils::{
25 ApproxEq, DEFAULT_ESURFACE_EXISTENCE_THRESHOLD, F, FloatLike, format_uncertainty,
26 serde_utils::{
27 _default_rotation_axis, _default_stability_levels, IsDefault,
28 deserialize_nonnegative_finite_f64, is_default_esurface_existence_threshold,
29 is_default_rotation_axis, is_default_stability_levels, is_false, is_float, is_true,
30 is_u64, is_usize, show_defaults_helper,
31 },
32 },
33};
34use symbolica::domains::{float::Real, integer::Integer, rational::Rational};
35
36use super::{RuntimeSettings, global::OrientationPattern};
37
38#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, Encode, Decode, JsonSchema)]
40#[serde(default)]
41pub struct RuntimeModelSettings {
42 #[serde(flatten, skip_serializing_if = "IsDefault::is_default")]
43 pub external_parameters: BTreeMap<String, (F<f64>, F<f64>)>,
44}
45
46#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
48#[serde(default, deny_unknown_fields)]
49pub struct SubtractionSettings {
50 #[serde(skip_serializing_if = "IsDefault::is_default")]
52 pub local_ct_settings: LocalCounterTermSettings,
53 #[serde(skip_serializing_if = "IsDefault::is_default")]
55 pub integrated_ct_settings: IntegratedCounterTermSettings,
56 #[serde(skip_serializing_if = "IsDefault::is_default")]
58 pub overlap_settings: OverlapSettings,
59 #[serde(
62 deserialize_with = "deserialize_nonnegative_finite_f64",
63 skip_serializing_if = "is_default_esurface_existence_threshold"
64 )]
65 #[schemars(range(min = 0.0))]
66 pub esurface_existence_threshold: f64,
67 #[serde(
72 deserialize_with = "deserialize_nonnegative_finite_f64",
73 skip_serializing_if = "is_float::<64>"
74 )]
75 #[schemars(range(min = 0.0))]
76 pub radial_root_residual_tolerance: f64,
77 #[serde(skip_serializing_if = "is_false")]
79 pub disable_threshold_subtraction: bool,
80}
81
82impl Default for SubtractionSettings {
83 fn default() -> Self {
84 Self {
85 local_ct_settings: LocalCounterTermSettings::default(),
86 integrated_ct_settings: IntegratedCounterTermSettings::default(),
87 overlap_settings: OverlapSettings::default(),
88 esurface_existence_threshold: DEFAULT_ESURFACE_EXISTENCE_THRESHOLD,
89 radial_root_residual_tolerance: 64.0,
90 disable_threshold_subtraction: false,
91 }
92 }
93}
94
95#[derive(Copy, Clone)]
96pub struct LockedRuntimeSettings<'a>(&'a RuntimeSettings);
97impl<'a> From<&'a RuntimeSettings> for LockedRuntimeSettings<'a> {
98 fn from(value: &'a RuntimeSettings) -> Self {
99 LockedRuntimeSettings(value)
100 }
101}
102
103impl<'a> From<LockedRuntimeSettings<'a>> for RuntimeSettings {
104 fn from(value: LockedRuntimeSettings) -> Self {
105 value.0.clone()
106 }
107}
108
109impl<'a> LockedRuntimeSettings<'a> {
110 pub(crate) fn helicities(&self) -> &[Helicity] {
111 self.0.kinematics.externals.get_helicities()
112 }
113
114 pub(crate) fn into_with_modified_kinematics(
116 self,
117 external_signature: &SignatureLike<ExternalIndex>,
118 external_masses: &TiVec<ExternalIndex, F<f64>>,
119 ) -> Result<RuntimeSettings> {
120 if external_signature.is_empty() {
121 Ok(self.into())
122 } else {
123 match &self.0.kinematics.externals {
124 Externals::Constant {
125 momenta,
126 helicities,
127 ..
128 } => {
129 if momenta.is_empty() && helicities.is_empty() {
130 warn!(
131 "No external kinematics were provided; using default-generated external momenta and helicities."
132 );
133 let new_externals = generate_default_momenta(
134 external_masses,
135 external_signature,
136 &F(self.0.kinematics.e_cm),
137 )?;
138
139 let mut new_settigns = self.0.clone();
140 new_settigns.kinematics.externals = new_externals;
141 Ok(new_settigns)
142 } else {
143 Ok(self.into())
144 }
145 }
146 }
147 }
148 }
149
150 pub(crate) fn existence_check(
151 &self,
152 esurface: &Esurface,
153 masses: &EdgeVec<F<f64>>,
154 external_signature: &SignatureLike<ExternalIndex>,
155 lmb: &LoopMomentumBasis,
156 esurface_existence_threshold: f64,
157 ) -> bool {
158 let dependent_momenta_constructor =
159 DependentMomentaConstructor::Amplitude(external_signature);
160
161 esurface
162 .classify_existence(
163 &self
164 .0
165 .kinematics
166 .externals
167 .get_dependent_externals(dependent_momenta_constructor)
168 .unwrap(),
169 lmb,
170 masses,
171 &F(self.0.kinematics.e_cm),
172 &F(esurface_existence_threshold),
173 )
174 .is_existing()
175 }
176}
177
178#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
179#[derive(
180 Debug, Copy, Clone, Deserialize, Serialize, Encode, Decode, PartialEq, Eq, JsonSchema, Default,
181)]
182#[serde(rename_all = "snake_case")]
183pub enum IntegralUnit {
184 #[default]
185 Auto,
186 Picobarn,
187 Femtobarn,
188 Attobarn,
189 Millibarn,
190 None,
191}
192
193impl IntegralUnit {
194 pub(crate) fn resolve_for_cross_section(self, n_initial_state_particles: usize) -> Self {
195 match self {
196 Self::Auto if n_initial_state_particles > 1 => Self::Picobarn,
197 Self::Auto => Self::None,
198 explicit => explicit,
199 }
200 }
201
202 pub(crate) fn relative_to_picobarn_factor<T: FloatLike>(self, one: &F<T>) -> Option<F<T>> {
203 match self {
204 Self::Picobarn => Some(one.one()),
205 Self::Femtobarn => Some(one.from_i64(1_000)),
206 Self::Attobarn => Some(one.from_i64(1_000_000)),
207 Self::Millibarn => Some(one.one() / one.from_i64(1_000_000_000)),
208 Self::None => None,
209 Self::Auto => unreachable!("integral unit must be resolved before conversion"),
210 }
211 }
212}
213
214#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
215#[derive(Debug, Clone, Deserialize, Serialize, Encode, Decode, PartialEq, JsonSchema)]
216#[trait_decode(trait= GammaLoopContext)]
217#[serde(default, deny_unknown_fields)]
218pub struct GeneralSettings {
219 #[serde(skip_serializing_if = "is_false")]
221 pub use_ltd: bool,
222 #[serde(skip_serializing_if = "IsDefault::is_default")]
224 pub evaluator_method: EvaluatorMethod,
225 #[serde(skip_serializing_if = "IsDefault::is_default")]
227 pub orientation_pat: OrientationPattern,
228 #[serde(skip_serializing_if = "is_false")]
230 pub load_compiled_cff: bool,
231 #[serde(skip_serializing_if = "is_false")]
233 pub enable_cache: bool,
234 #[serde(skip_serializing_if = "is_false")]
236 pub debug_cache: bool,
237 #[serde(skip_serializing_if = "is_float::<1000>")]
239 pub m_uv: f64,
240 #[serde(skip_serializing_if = "is_float::<1000>")]
242 pub renormalization_localization_scale: f64,
243 #[serde(skip_serializing_if = "is_float::<1000>")]
245 pub mu_r: f64,
246 #[serde(skip_serializing_if = "is_float::<1>")]
248 pub numerator_sampling_scale: f64,
249 #[serde(skip_serializing_if = "is_float::<1>")]
251 pub inverse_temperature: f64,
252 #[serde(skip_serializing_if = "IsDefault::is_default")]
254 pub additional_param_values: Vec<f64>,
255 #[serde(skip_serializing_if = "IsDefault::is_default")]
257 pub integral_unit: IntegralUnit,
258 #[serde(skip_serializing_if = "is_false")]
260 pub disable_flux_factor: bool,
261 #[serde(skip_serializing_if = "is_false")]
263 pub generate_events: bool,
264 #[serde(skip_serializing_if = "is_false")]
266 pub store_additional_weights_in_event: bool,
267}
268
269impl Default for GeneralSettings {
270 fn default() -> Self {
271 Self {
272 evaluator_method: EvaluatorMethod::default(),
273 use_ltd: false,
274 load_compiled_cff: false,
275 enable_cache: false,
276 debug_cache: false,
277 orientation_pat: OrientationPattern::default(),
278 m_uv: 1000.0,
279 renormalization_localization_scale: 1000.0,
280 mu_r: 1000.0,
281 numerator_sampling_scale: 1.0,
282 inverse_temperature: 1.0,
283 additional_param_values: vec![],
284 integral_unit: IntegralUnit::Auto,
285 disable_flux_factor: false,
286 generate_events: false,
287 store_additional_weights_in_event: false,
288 }
289 }
290}
291
292impl GeneralSettings {
293 pub(crate) fn mu_r_sq(&self) -> f64 {
294 self.mu_r * self.mu_r
295 }
296}
297
298#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
299#[derive(
300 Debug, Copy, Clone, PartialEq, Deserialize, Default, Serialize, Encode, Decode, JsonSchema,
301)]
302#[serde(deny_unknown_fields)]
304pub enum IntegratedPhase {
305 #[serde(rename = "real")]
306 #[default]
307 Real,
308 #[serde(rename = "imag")]
309 Imag,
310 #[serde(rename = "both")]
311 Both,
312}
313
314pub mod kinematic;
315
316#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
317#[derive(Debug, Clone, Deserialize, Serialize, Encode, Decode, PartialEq, JsonSchema)]
318#[serde(default, deny_unknown_fields)]
319pub struct ObservablesOutputSettings {
320 #[serde(
322 default = "default_observable_output_formats",
323 skip_serializing_if = "is_default_observable_output_formats"
324 )]
325 pub format: Vec<ObservableFileFormat>,
326}
327
328fn default_observable_output_formats() -> Vec<ObservableFileFormat> {
329 vec![ObservableFileFormat::Json]
330}
331
332fn is_default_observable_output_formats(formats: &Vec<ObservableFileFormat>) -> bool {
333 show_defaults_helper(formats.as_slice() == default_observable_output_formats().as_slice())
334}
335
336impl Default for ObservablesOutputSettings {
337 fn default() -> Self {
338 Self {
339 format: default_observable_output_formats(),
340 }
341 }
342}
343
344impl ObservablesOutputSettings {
345 pub(crate) fn resolved_formats(&self) -> Vec<ObservableFileFormat> {
346 let mut resolved_formats = Vec::new();
347 for format in &self.format {
348 if *format == ObservableFileFormat::None || resolved_formats.contains(format) {
349 continue;
350 }
351 resolved_formats.push(*format);
352 }
353 resolved_formats
354 }
355}
356
357#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
358#[derive(Debug, Clone, Deserialize, Serialize, Encode, Decode, PartialEq, JsonSchema)]
359#[serde(default, deny_unknown_fields)]
360pub struct IntegratorSettings {
361 #[serde(skip_serializing_if = "is_usize::<64>")]
363 pub n_bins: usize,
364 #[serde(skip_serializing_if = "IsDefault::is_default")]
366 pub bin_number_evolution: Option<Vec<usize>>,
367 #[serde(skip_serializing_if = "is_usize::<1000>")]
369 pub min_samples_for_update: usize,
370 #[serde(skip_serializing_if = "is_usize::<100000>")]
372 pub n_start: usize,
373 #[serde(skip_serializing_if = "is_usize::<10000>")]
375 pub n_increase: usize,
376 #[serde(skip_serializing_if = "is_usize::<10000000000>")]
378 pub n_max: usize,
379 #[serde(skip_serializing_if = "IsDefault::is_default")]
381 pub target_relative_accuracy: Option<f64>,
382 #[serde(skip_serializing_if = "IsDefault::is_default")]
384 pub target_absolute_accuracy: Option<f64>,
385 #[serde(skip_serializing_if = "IsDefault::is_default")]
387 pub integrated_phase: IntegratedPhase,
388 #[serde(skip_serializing_if = "is_float::<1>")]
390 pub discrete_dim_learning_rate: f64,
391 #[serde(skip_serializing_if = "is_float::<1>")]
393 pub continuous_dim_learning_rate: f64,
394 #[serde(skip_serializing_if = "is_false")]
396 pub train_on_avg: bool,
397 #[serde(skip_serializing_if = "is_true")]
399 pub show_max_wgt_info: bool,
400 #[serde(skip_serializing_if = "is_float::<30>")]
402 pub max_prob_ratio: f64,
403 #[serde(skip_serializing_if = "is_u64::<69>")]
405 pub seed: u64,
406 #[serde(skip_serializing_if = "IsDefault::is_default")]
408 pub observables_output: ObservablesOutputSettings,
409}
410
411impl Default for IntegratorSettings {
412 fn default() -> Self {
413 Self {
414 n_bins: 64,
415 bin_number_evolution: None,
416 min_samples_for_update: 1000,
417 n_start: 100000,
418 n_increase: 10000,
419 n_max: 10000000000,
420 target_relative_accuracy: None,
421 target_absolute_accuracy: None,
422 integrated_phase: IntegratedPhase::default(),
423 discrete_dim_learning_rate: 1.0,
424 continuous_dim_learning_rate: 1.0,
425 train_on_avg: false,
426 show_max_wgt_info: true,
427 max_prob_ratio: 30.0,
428 seed: 69,
429 observables_output: ObservablesOutputSettings::default(),
430 }
431 }
432}
433
434#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
435#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Encode, Decode, JsonSchema)]
436#[serde(default, deny_unknown_fields)]
437pub struct ParameterizationSettings {
438 #[serde(skip_serializing_if = "IsDefault::is_default")]
439 pub mode: ParameterizationMode,
440 #[serde(skip_serializing_if = "IsDefault::is_default")]
441 pub mapping: ParameterizationMapping,
442 #[serde(skip_serializing_if = "is_float::<1>")]
443 pub b: f64,
444 #[serde(skip_serializing_if = "is_float::<1>")]
445 pub power: f64,
446 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
447 pub lmb_basis_ids: BTreeMap<String, Vec<usize>>,
448 #[serde(skip_serializing_if = "IsDefault::is_default")]
452 pub sampling_channels: SamplingChannelSelection,
453}
454
455impl Default for ParameterizationSettings {
456 fn default() -> Self {
457 Self {
458 b: 1.0,
459 power: 1.0,
460 mode: ParameterizationMode::default(),
461 mapping: ParameterizationMapping::default(),
462 lmb_basis_ids: BTreeMap::new(),
463 sampling_channels: SamplingChannelSelection::default(),
464 }
465 }
466}
467
468#[derive(Serialize, Deserialize, Default, Debug, Clone)]
469#[serde(default, deny_unknown_fields)]
470pub struct IntegralEstimate {
471 pub neval: usize,
472 pub real_zero: usize,
473 pub im_zero: usize,
474 pub result: Complex<F<f64>>,
475 pub error: Complex<F<f64>>,
476 pub real_chisq: F<f64>,
477 pub im_chisq: F<f64>,
478}
479
480impl Display for IntegralEstimate {
481 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482 let real_formatted = if self.error.re.0.abs() > 0.0 && f.alternate() {
484 format_uncertainty(self.result.re, self.error.re)
485 } else {
486 match f.precision() {
488 Some(p) => format!("{:.prec$e}", self.result.re.0, prec = p),
489 None => format!("{:e}", self.result.re.0),
490 }
491 };
492
493 let im_formatted = if self.error.im.0.abs() > 0.0 && f.alternate() {
494 format_uncertainty(self.result.im, self.error.im)
495 } else {
496 match f.precision() {
498 Some(p) => format!("{:.prec$e}", self.result.im.0, prec = p),
499 None => format!("{:e}", self.result.im.0),
500 }
501 };
502
503 if self.result.im.0.abs() > 0.0 {
505 if f.alternate() {
506 write!(
507 f,
508 "{} + {}i (neval: {}, real_chisq: {:.3}, im_chisq: {:.3})",
509 real_formatted, im_formatted, self.neval, self.real_chisq.0, self.im_chisq.0
510 )
511 } else {
512 write!(f, "{} + {}i", real_formatted, im_formatted,)
513 }
514 } else if f.alternate() {
515 write!(
516 f,
517 "{} (neval: {}, real_chisq: {:.3})",
518 real_formatted, self.neval, self.real_chisq.0
519 )
520 } else {
521 write!(f, "{}", real_formatted)
522 }
523 }
524}
525
526impl IntegralEstimate {
527 pub fn is_compatible_with_target(&self, target: Complex<F<f64>>, sigma: i8) -> bool {
528 let res_norm = self.result.norm().re;
529 let tolerance = if res_norm.is_zero() {
530 self.error.norm().re
531 } else {
532 (self.error).norm().re * F(sigma as f64) / res_norm
533 };
534
535 if tolerance.0 > 0.1 {
536 warn!("Tolerance larger than 10%! {}", tolerance)
537 }
538 self.result.approx_eq(&target, &tolerance)
540 }
541
542 pub fn is_compatible_with_result(&self, other: &Self, sigma: i8) -> bool {
543 let combined_error = Complex::new(
544 (self.error.re * self.error.re + other.error.re * other.error.re).sqrt(),
545 (self.error.im * self.error.im + other.error.im * other.error.im).sqrt(),
546 );
547 let delta = self.result - other.result;
548 let delta_norm = delta.norm().re;
549 let tolerance = if delta_norm.is_zero() {
550 combined_error.norm().re
551 } else {
552 combined_error.norm().re * F(sigma as f64) / delta_norm
553 };
554
555 if tolerance.0 > 0.1 {
556 warn!("Tolerance larger than 10%! {}", tolerance)
557 }
558
559 self.result.approx_eq(&other.result, &tolerance)
560 }
561}
562
563#[derive(Serialize, Deserialize, Default, Debug, Clone)]
564#[serde(default, deny_unknown_fields)]
565pub struct IntegrationTableComponentResult {
566 pub component: String,
567 pub value: F<f64>,
568 pub error: F<f64>,
569 pub relative_error_percent: Option<f64>,
570 pub chi_sq_per_dof: f64,
571 pub target_delta_sigma: Option<f64>,
572 pub target_delta_percent: Option<f64>,
573 pub max_weight_impact: f64,
574}
575
576#[derive(Serialize, Deserialize, Default, Debug, Clone)]
577#[serde(default, deny_unknown_fields)]
578pub struct IntegrationStatisticsSnapshot {
579 pub num_evals: usize,
580 pub average_total_time_seconds: f64,
581 pub average_parameterization_time_seconds: f64,
582 pub average_integrand_time_seconds: f64,
583 pub average_evaluator_time_seconds: f64,
584 pub average_observable_time_seconds: f64,
585 pub average_integrator_time_seconds: f64,
586 pub f64_percentage: f64,
587 pub f128_percentage: f64,
588 pub arb_percentage: f64,
589 pub nan_percentage: f64,
590 pub nan_or_unstable_percentage: f64,
591 pub generated_event_count: usize,
592 pub accepted_event_count: usize,
593 pub selection_efficiency_percentage: Option<f64>,
594}
595
596#[derive(Serialize, Deserialize, Default, Debug, Clone)]
597#[serde(default, deny_unknown_fields)]
598pub struct MaxWeightInfoEntry {
599 pub component: String,
600 pub sign: String,
601 pub max_eval: F<f64>,
602 pub coordinates: Option<String>,
603}
604
605#[derive(Serialize, Deserialize, Default, Debug, Clone)]
606#[serde(default, deny_unknown_fields)]
607pub struct DiscreteCoordinate {
608 pub axis_label: String,
609 pub bin_index: usize,
610 pub bin_label: Option<String>,
611}
612
613#[derive(Serialize, Deserialize, Default, Debug, Clone)]
614#[serde(default, deny_unknown_fields)]
615pub struct DiscreteBreakdownEntry {
616 pub bin_index: usize,
617 pub bin_label: Option<String>,
618 pub pdf: F<f64>,
619 pub value: F<f64>,
620 pub error: F<f64>,
621 pub chi_sq: F<f64>,
622 pub processed_samples: usize,
623}
624
625#[derive(Serialize, Deserialize, Default, Debug, Clone)]
626#[serde(default, deny_unknown_fields)]
627pub struct DiscreteBreakdown {
628 pub axis_label: String,
629 pub fixed_coordinates: Vec<DiscreteCoordinate>,
630 pub entries: Vec<DiscreteBreakdownEntry>,
631}
632
633#[derive(Serialize, Deserialize, Default, Debug, Clone)]
634#[serde(default, deny_unknown_fields)]
635pub struct ComponentDiscreteBreakdown {
636 pub re: Option<DiscreteBreakdown>,
637 pub im: Option<DiscreteBreakdown>,
638}
639
640#[derive(Serialize, Deserialize, Default, Debug, Clone)]
641#[serde(default, deny_unknown_fields)]
642pub struct AbsoluteIntegrationResult {
643 pub integral: IntegralEstimate,
644 pub table_results: Vec<IntegrationTableComponentResult>,
645 pub max_weight_info: Vec<MaxWeightInfoEntry>,
646 pub grid_breakdown: ComponentDiscreteBreakdown,
647}
648
649#[derive(Serialize, Deserialize, Default, Debug, Clone)]
650#[serde(default, deny_unknown_fields)]
651pub struct SlotIntegrationResult {
652 pub key: String,
653 pub process: String,
654 pub integrand: String,
655 pub target: Option<Complex<F<f64>>>,
656 pub integral: IntegralEstimate,
657 pub table_results: Vec<IntegrationTableComponentResult>,
658 pub integration_statistics: IntegrationStatisticsSnapshot,
659 pub max_weight_info: Vec<MaxWeightInfoEntry>,
660 pub grid_breakdown: ComponentDiscreteBreakdown,
661 pub absolute: AbsoluteIntegrationResult,
662}
663
664#[derive(Serialize, Deserialize, Default, Debug, Clone)]
665#[serde(default, deny_unknown_fields)]
666pub struct IntegrationResult {
667 pub slots: Vec<SlotIntegrationResult>,
668}
669
670impl IntegrationResult {
671 pub fn slot(&self, key: &str) -> Option<&SlotIntegrationResult> {
672 self.slots.iter().find(|slot| slot.key == key)
673 }
674
675 pub fn single_slot(&self) -> Option<&SlotIntegrationResult> {
676 (self.slots.len() == 1).then(|| &self.slots[0])
677 }
678
679 pub fn single_slot_integral(&self) -> Option<&IntegralEstimate> {
680 self.single_slot().map(|slot| &slot.integral)
681 }
682
683 pub fn is_compatible_with_target(&self, target: Complex<F<f64>>, n_sigma: i8) -> bool {
684 self.single_slot_integral()
685 .is_some_and(|integral| integral.is_compatible_with_target(target, n_sigma))
686 }
687}
688
689impl Display for IntegrationResult {
690 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
691 if let Some(slot) = self.single_slot() {
692 return slot.integral.fmt(f);
693 }
694
695 for (index, slot) in self.slots.iter().enumerate() {
696 if index > 0 {
697 writeln!(f)?;
698 }
699 write!(f, "{}: {}", slot.key, slot.integral)?;
700 }
701 Ok(())
702 }
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708 use crate::utils::serde_utils::ShowDefaultsGuard;
709 use spenso::algebra::complex::Complex;
710
711 #[test]
712 fn sampling_lu_h_profile_shorthand_and_table_share_one_definition() {
713 let shorthand: SamplingChannelDefinition =
714 toml::from_str("around='phase_space(cut(2,3))'\nradial_profile='lu_h'").unwrap();
715 let table: SamplingChannelDefinition = toml::from_str("around='phase_space(cut(2,3))'\nradial_profile={kind='lu_h',approximation='log_logistic',broad_fraction=0.02}").unwrap();
716 assert_eq!(shorthand, table);
717 assert_eq!(
718 shorthand.radial_profile,
719 Some(SamplingRadialProfile::default())
720 );
721 let exported = toml::to_string(&shorthand).unwrap();
722 assert_eq!(
723 toml::from_str::<SamplingChannelDefinition>(&exported).unwrap(),
724 shorthand
725 );
726 for invalid in [
727 "{kind='lu_h',broad_faction=0.1}",
728 "'unknown'",
729 "{kind='lu_h',approximation='exact'}",
730 ] {
731 assert!(
732 toml::from_str::<SamplingChannelDefinition>(&format!("radial_profile={invalid}"))
733 .is_err()
734 );
735 }
736 let _guard = ShowDefaultsGuard::new(true);
737 let json = serde_json::to_value(SamplingRadialProfile::default()).unwrap();
738 assert!(json.get("scale").is_some());
739 assert!(json.get("shape").is_some());
740 }
741
742 #[test]
743 fn h_function_normalizations_match_bessel_integrals_in_native_precision() {
744 use symbolica::{
745 atom::{Atom, AtomView},
746 coefficient::CoefficientView,
747 domains::float::{Float, RealLike},
748 function,
749 transcendental::bessel_k,
750 };
751
752 use crate::utils::{ArbPrec, SamplingFloat, f128};
753
754 const ORACLE_PRECISION: u32 = 1200;
755 let scaled_bessel_k = |twice_order: usize| {
757 let value = function!(
758 bessel_k(),
759 Atom::num((twice_order as i64, 2)),
760 Atom::num(Float::with_val(ORACLE_PRECISION, 2))
761 );
762 let AtomView::Num(number) = value.as_view() else {
763 panic!("bessel_k did not evaluate: {value}");
764 };
765 let CoefficientView::Float(real, _) = number.get_coeff_view() else {
766 panic!("bessel_k did not evaluate to a float: {value}");
767 };
768 real.to_float() * Float::with_val(ORACLE_PRECISION, 2).exp()
769 };
770 let sqrt_pi_half = std::f64::consts::PI.sqrt() / 2.0;
772 let poly = [
773 (None, sqrt_pi_half),
774 (Some(1), 0.841_568_215_070_771_4),
775 (Some(3), 1.033_476_847_068_688_6),
776 (Some(4), 1.329_340_388_179_137),
777 (Some(6), 2.880_237_507_721_463_7),
778 (Some(7), 4.783_566_971_347_609),
779 (Some(9), 16.225_745_976_182_285),
780 (Some(10), 32.735_007_058_911_25),
781 (Some(12), 155.837_465_922_583_42),
782 (Some(13), 364.658_500_356_566_04),
783 (Some(15), 2_257.637_553_015_473),
784 (Some(16), 5_939.804_418_537_864),
785 ];
786 let left_right = [
787 (Some(0), 2.066_953_694_137_377),
788 (Some(1), 1.683_136_430_141_542_8),
789 (Some(3), 3.750_090_124_278_92),
790 (Some(4), 9.567_133_942_695_218),
791 (Some(6), 139.373_101_752_153_5),
792 (Some(7), 729.317_000_713_132_1),
793 (Some(9), 32_336.242_742_929_753),
794 (Some(10), 263_205.217_049_469),
795 (Some(12), 2.427_503_717_893_097_5e7),
796 (Some(13), 2.694_265_921_644_289e8),
797 (Some(15), 4.261_555_045_314_143e10),
798 (Some(16), 5.998_751_004_871_322e11),
799 ];
800 let tabulated = std::iter::once((HFunction::Exponential, None, sqrt_pi_half))
801 .chain(poly.map(|(power, value)| (HFunction::PolyExponential, power, value)))
802 .chain(
803 left_right
804 .map(|(power, value)| (HFunction::PolyLeftRightExponential, power, value)),
805 );
806 fn check<T: FloatLike>(sigma: F<T>, settings: &HFunctionSettings, expected: &Float) {
807 for multiplier in [1, 2, 3] {
809 let scaled_sigma = &sigma * sigma.from_usize(multiplier);
810 let expected =
811 expected.clone() * Float::with_val(ORACLE_PRECISION, multiplier as u32);
812 let actual: Float = settings.normalization(&scaled_sigma).0.into();
813 let relative = ((actual - &expected) / &expected).norm();
814 let epsilon: Float = sigma.epsilon().0.into();
816 assert!(
817 relative <= epsilon * 64,
818 "{settings:?} in {}: relative error {relative}",
819 std::any::type_name::<T>()
820 );
821 }
822 }
823 for (function, power, binary64) in tabulated {
824 let settings = HFunctionSettings {
825 function: function.clone(),
826 power,
827 ..Default::default()
828 };
829 let p = power.unwrap_or(0);
830 let expected = match function {
831 HFunction::Exponential => scaled_bessel_k(1),
832 HFunction::PolyExponential => scaled_bessel_k(p.abs_diff(1)),
833 _ => scaled_bessel_k(2 * p.abs_diff(1)) * 2,
834 };
835 assert!(
836 ((expected.to_f64() - binary64) / binary64).abs() < 4.0 * f64::EPSILON,
837 "{settings:?}: {expected} != tabulated {binary64}"
838 );
839 let expected = expected * Float::with_val(ORACLE_PRECISION, 1.25);
841 check(F(1.25_f64), &settings, &expected);
842 check(F(f128::from_f64(1.25)), &settings, &expected);
843 check(F(SamplingFloat::from_f64(1.25)), &settings, &expected);
844 check(F(ArbPrec::from_f64(1.25)), &settings, &expected);
845 }
846 let sigma = F(ArbPrec::from_f64(1.25));
848 assert_eq!(
849 HFunctionSettings::default().normalization(&sigma),
850 sigma.PI().sqrt() * &sigma / sigma.from_usize(2)
851 );
852 }
853
854 #[test]
855 fn test_integration_result_display() {
856 let result = IntegralEstimate {
857 neval: 1000000,
858 real_zero: 0,
859 im_zero: 0,
860 result: Complex::new(F(1.23456789e-3), F(4.56789e-5)),
861 error: Complex::new(F(1.2e-5), F(3.4e-7)),
862 real_chisq: F(1.05),
863 im_chisq: F(0.98),
864 };
865
866 let display_str = format!("{}", result);
867 println!("Integration result display: {}", display_str);
868
869 let real_only_result = IntegralEstimate {
871 neval: 500000,
872 real_zero: 0,
873 im_zero: 0,
874 result: Complex::new(F(2.345e-2), F(0.0)),
875 error: Complex::new(F(1.5e-4), F(0.0)),
876 real_chisq: F(1.12),
877 im_chisq: F(0.0),
878 };
879
880 let real_display_str = format!("{}", real_only_result);
881 println!("Real-only result display: {}", real_display_str);
882
883 let precision_str = format!("{:.2}", result);
885 println!("With precision 2: {}", precision_str);
886 }
887
888 #[test]
889 fn integrator_settings_target_accuracy_defaults_stay_optional() {
890 let settings = IntegratorSettings::default();
891
892 let hidden_defaults = toml::to_string(&settings).expect("serialize integrator defaults");
893 assert!(!hidden_defaults.contains("target_relative_accuracy"));
894 assert!(!hidden_defaults.contains("target_absolute_accuracy"));
895
896 let _guard = ShowDefaultsGuard::new(true);
897 let shown_defaults = toml::to_string(&settings).expect("serialize integrator defaults");
898 assert!(shown_defaults.contains("n_bins"));
899 assert!(!shown_defaults.contains("target_relative_accuracy"));
900 assert!(!shown_defaults.contains("target_absolute_accuracy"));
901 }
902
903 #[test]
904 fn integrator_settings_target_accuracy_serializes_when_configured() {
905 let settings = IntegratorSettings {
906 target_relative_accuracy: Some(0.05),
907 target_absolute_accuracy: Some(1.0e-6),
908 ..IntegratorSettings::default()
909 };
910
911 let serialized = toml::to_string(&settings).expect("serialize configured integrator");
912 assert!(serialized.contains("target_relative_accuracy = 0.05"));
913 assert!(serialized.contains("target_absolute_accuracy = "));
914
915 let deserialized: IntegratorSettings =
916 toml::from_str(&serialized).expect("deserialize configured integrator");
917 assert_eq!(settings, deserialized);
918 }
919
920 #[test]
921 fn stability_ecm_relative_tolerances_default_and_roundtrip() {
922 let _guard = ShowDefaultsGuard::new(false);
923 for settings in [
924 StabilityLevelSetting::default_double(),
925 StabilityLevelSetting::default_quad(),
926 StabilityLevelSetting::default_arb(),
927 ] {
928 let serialized = toml::to_string(&settings).unwrap();
929 assert!(!serialized.contains("ecm_relative_tolerance"));
930 assert_eq!(
931 toml::from_str::<StabilityLevelSetting>(&serialized).unwrap(),
932 settings
933 );
934 }
935
936 let settings = StabilityLevelSetting {
937 ecm_relative_tolerance_for_re: 1e-100,
938 ecm_relative_tolerance_for_im: 2e-90,
939 ..StabilityLevelSetting::default_arb()
940 };
941 let serialized = toml::to_string(&settings).unwrap();
942 assert_eq!(
943 toml::from_str::<StabilityLevelSetting>(&serialized).unwrap(),
944 settings
945 );
946 let schema = serde_json::to_value(schemars::schema_for!(StabilityLevelSetting)).unwrap();
947 for field in [
948 "ecm_relative_tolerance_for_re",
949 "ecm_relative_tolerance_for_im",
950 ] {
951 assert_eq!(schema["properties"][field]["minimum"], 0.0);
952 }
953
954 let defaults: StabilitySettings = toml::from_str("").unwrap();
955 assert_eq!(defaults.integrated_energy_dimension, None);
956 assert!(
957 !toml::to_string(&defaults)
958 .unwrap()
959 .contains("integrated_energy_dimension")
960 );
961 let declared = StabilitySettings {
962 integrated_energy_dimension: Some(-6),
963 levels: vec![settings],
964 ..defaults
965 };
966 assert_eq!(
967 toml::from_str::<StabilitySettings>(&toml::to_string(&declared).unwrap()).unwrap(),
968 declared
969 );
970 drop(_guard);
971 let _guard = ShowDefaultsGuard::new(true);
972 for settings in [
973 StabilityLevelSetting::default_double(),
974 StabilityLevelSetting::default_quad(),
975 StabilityLevelSetting::default_arb(),
976 ] {
977 let shown = serde_json::to_value(settings).unwrap();
978 assert_eq!(shown["ecm_relative_tolerance_for_re"], 0.0);
979 assert_eq!(shown["ecm_relative_tolerance_for_im"], 0.0);
980 }
981 assert_eq!(
982 serde_json::to_value(&declared).unwrap()["integrated_energy_dimension"],
983 -6
984 );
985 }
986
987 #[test]
988 fn stability_ecm_relative_tolerances_reject_invalid_values() {
989 let _guard = ShowDefaultsGuard::new(false);
990 let defaults = toml::to_string(&StabilityLevelSetting::default_arb()).unwrap();
991 for field in [
992 "ecm_relative_tolerance_for_re",
993 "ecm_relative_tolerance_for_im",
994 ] {
995 for invalid in ["-1.0", "nan", "inf", "-inf"] {
996 let text = format!("{defaults}\n{field} = {invalid}\n");
997 assert!(
998 toml::from_str::<StabilityLevelSetting>(&text).is_err(),
999 "{field} = {invalid}"
1000 );
1001 }
1002 }
1003 }
1004
1005 #[test]
1006 fn sampling_ose_and_named_weight_roundtrip() {
1007 let _guard = ShowDefaultsGuard::new(false);
1008 for (graphs, channels) in [
1009 ("summed", "summed"),
1010 ("monte_carlo", "summed"),
1011 ("monte_carlo", "monte_carlo"),
1012 ] {
1013 for (global, local) in [("ose", "map_density"), ("map_density", "ose")] {
1014 let text = format!(
1015 r#"graphs = "{graphs}"
1016sampling_multichanneling = true
1017sampling_channels = "{channels}"
1018sampling_channel_weight = "{global}"
1019alpha = 2.5
1020default_channel_selection = ["auto:optimized_lmb"]
1021[channel_definitions.G.soft]
1022around = "lmb(1,2)"
1023parent_lmb = [1,2]
1024channel_weight = "{local}"
1025"#
1026 );
1027 let settings: SamplingSettings = toml::from_str(&text).unwrap();
1028 let selection = settings
1029 .get_parameterization_settings()
1030 .unwrap()
1031 .sampling_channels;
1032 assert_eq!(selection.alpha, 2.5);
1033 assert_eq!(
1034 selection.weight == SamplingChannelWeight::Ose,
1035 global == "ose"
1036 );
1037 assert_eq!(
1038 selection.channel_definitions["G"]["soft"].channel_weight
1039 == Some(SamplingChannelWeight::Ose),
1040 local == "ose"
1041 );
1042 let serialized = toml::to_string(&settings).unwrap();
1043 assert_eq!(
1044 toml::from_str::<SamplingSettings>(&serialized).unwrap(),
1045 settings
1046 );
1047 }
1048 }
1049 let settings: SamplingSettings =
1050 toml::from_str("sampling_multichanneling=true\nsampling_channel_weight='ose'").unwrap();
1051 let selection = settings
1052 .get_parameterization_settings()
1053 .unwrap()
1054 .sampling_channels;
1055 assert_eq!(selection.weight, SamplingChannelWeight::Ose);
1056 assert_eq!(selection.alpha, 3.0);
1057 assert_eq!(
1058 selection.default_channel_selection,
1059 vec!["auto:optimized_lmb"]
1060 );
1061 assert!(!toml::to_string(&settings).unwrap().contains("alpha"));
1062 drop(_guard);
1063 let _guard = ShowDefaultsGuard::new(true);
1064 assert_eq!(serde_json::to_value(settings).unwrap()["alpha"], 3.0);
1065 }
1066
1067 #[test]
1068 fn sampling_ose_alpha_validation_and_zero_control() {
1069 for invalid in ["-1.0", "nan", "inf", "-inf"] {
1070 let text = format!("alpha={invalid}");
1071 assert!(toml::from_str::<SamplingSettings>(&text).is_err());
1072 assert!(toml::from_str::<SamplingChannelSelection>(&text).is_err());
1073 }
1074 for alpha in [-1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1075 assert!(
1076 SamplingSettings::from_parser(SamplingSettingsParser {
1077 alpha,
1078 ..Default::default()
1079 })
1080 .is_err()
1081 );
1082 }
1083 let settings: SamplingSettings = toml::from_str(
1084 "sampling_multichanneling=true\nsampling_channel_weight='ose'\nalpha=0.0",
1085 )
1086 .unwrap();
1087 assert_eq!(
1088 settings
1089 .get_parameterization_settings()
1090 .unwrap()
1091 .sampling_channels
1092 .alpha,
1093 0.0
1094 );
1095 assert_eq!(
1096 toml::from_str::<SamplingSettings>(&toml::to_string(&settings).unwrap()).unwrap(),
1097 settings
1098 );
1099 }
1100
1101 #[test]
1102 fn uv_localisation_smooth_sliver_defaults_and_roundtrips() {
1103 let defaults: UVLocalisationSettings = toml::from_str("").unwrap();
1104 assert!(!defaults.smooth_sliver);
1105 assert_eq!(defaults, UVLocalisationSettings::default());
1106 {
1107 let _guard = ShowDefaultsGuard::new(false);
1108 assert!(
1109 !toml::to_string(&defaults)
1110 .unwrap()
1111 .contains("smooth_sliver")
1112 );
1113 let settings = UVLocalisationSettings {
1114 smooth_sliver: true,
1115 ..defaults.clone()
1116 };
1117 let serialized = toml::to_string(&settings).unwrap();
1118 assert!(serialized.contains("smooth_sliver = true"));
1119 assert_eq!(
1120 toml::from_str::<UVLocalisationSettings>(&serialized).unwrap(),
1121 settings
1122 );
1123 }
1124 let _guard = ShowDefaultsGuard::new(true);
1125 assert!(
1126 toml::to_string(&defaults)
1127 .unwrap()
1128 .contains("smooth_sliver = false")
1129 );
1130 }
1131
1132 #[test]
1133 fn integral_unit_auto_resolves_by_initial_state_count() {
1134 assert_eq!(
1135 IntegralUnit::Auto.resolve_for_cross_section(1),
1136 IntegralUnit::None
1137 );
1138 assert_eq!(
1139 IntegralUnit::Auto.resolve_for_cross_section(2),
1140 IntegralUnit::Picobarn
1141 );
1142 }
1143
1144 #[test]
1145 fn integral_unit_relative_scalings_match_requested_barn_unit() {
1146 let one = F(1.0f64);
1147 assert_eq!(
1148 IntegralUnit::Picobarn.relative_to_picobarn_factor(&one),
1149 Some(F(1.0))
1150 );
1151 assert_eq!(
1152 IntegralUnit::Femtobarn.relative_to_picobarn_factor(&one),
1153 Some(F(1_000.0))
1154 );
1155 assert_eq!(
1156 IntegralUnit::Attobarn.relative_to_picobarn_factor(&one),
1157 Some(F(1_000_000.0))
1158 );
1159 assert_eq!(
1160 IntegralUnit::Millibarn.relative_to_picobarn_factor(&one),
1161 Some(F(1.0e-9))
1162 );
1163 assert_eq!(IntegralUnit::None.relative_to_picobarn_factor(&one), None);
1164 }
1165
1166 #[test]
1167 fn general_settings_store_mu_r_and_compute_mu_r_sq_internally() {
1168 let settings = GeneralSettings {
1169 mu_r: 91.188,
1170 ..GeneralSettings::default()
1171 };
1172
1173 assert_eq!(settings.mu_r, 91.188);
1174 assert!((settings.mu_r_sq() - 8315.251344).abs() < 1.0e-12);
1175
1176 let serialized = toml::to_string(&settings).expect("serialize general settings");
1177 assert!(serialized.contains("mu_r = 91.188"));
1178 assert!(!serialized.contains("mu_r_sq"));
1179 }
1180}
1181
1182#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
1183#[derive(Serialize, Deserialize, Debug, Clone, Encode, Decode, PartialEq, JsonSchema)]
1184#[serde(default, deny_unknown_fields)]
1185pub struct StabilitySettings {
1186 #[serde(skip_serializing_if = "IsDefault::is_default")]
1192 pub integrated_energy_dimension: Option<i32>,
1193 #[serde(skip_serializing_if = "is_default_rotation_axis")]
1195 pub rotation_axis: Vec<RotationSetting>,
1196 #[serde(skip_serializing_if = "is_default_stability_levels")]
1198 pub levels: Vec<StabilityLevelSetting>,
1199 #[serde(skip_serializing_if = "is_true")]
1201 pub check_on_norm: bool,
1202 #[serde(skip_serializing_if = "is_false")]
1204 pub escalate_if_exact_zero: bool,
1205 #[serde(
1211 default,
1212 deserialize_with = "deserialize_nonnegative_finite_f64",
1213 skip_serializing_if = "is_float::<0>"
1214 )]
1215 #[schemars(range(min = 0.0))]
1216 pub min_abs_wgt_for_escalation: f64,
1217 #[serde(skip_serializing_if = "is_float::<-1>")]
1219 pub loop_momenta_norm_escalation_factor: f64,
1220 #[serde(skip_serializing_if = "IsDefault::is_default")]
1222 pub recording: Option<StabilityRecordingSettings>,
1223}
1224
1225impl Default for StabilitySettings {
1226 fn default() -> Self {
1227 Self {
1228 integrated_energy_dimension: None,
1229 rotation_axis: _default_rotation_axis(),
1230 levels: _default_stability_levels(),
1231 check_on_norm: true,
1232 escalate_if_exact_zero: false,
1233 min_abs_wgt_for_escalation: 0.0,
1234 loop_momenta_norm_escalation_factor: -1.0,
1235 recording: None,
1236 }
1237 }
1238}
1239
1240#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
1241#[derive(Serialize, Deserialize, Debug, Clone, Copy, Encode, Decode, PartialEq, JsonSchema)]
1242#[serde(default, deny_unknown_fields)]
1243pub struct StabilityRecordingSettings {
1244 #[serde(skip_serializing_if = "is_false")]
1246 pub record_rotated_results: bool,
1247 #[serde(skip_serializing_if = "is_false")]
1249 pub record_all_stability_levels: bool,
1250 #[serde(skip_serializing_if = "is_false")]
1252 pub record_loop_momenta_escalation: bool,
1253}
1254
1255#[allow(clippy::derivable_impls)]
1256impl Default for StabilityRecordingSettings {
1257 fn default() -> Self {
1258 Self {
1259 record_rotated_results: false,
1260 record_all_stability_levels: false,
1261 record_loop_momenta_escalation: false,
1262 }
1263 }
1264}
1265
1266#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
1267#[derive(Serialize, Deserialize, Debug, Clone, Copy, Encode, Decode, PartialEq, JsonSchema)]
1268#[serde(deny_unknown_fields)]
1269pub struct StabilityLevelSetting {
1270 pub precision: Precision,
1272 pub required_precision_for_re: f64,
1274 pub required_precision_for_im: f64,
1276 #[serde(
1283 default,
1284 deserialize_with = "deserialize_nonnegative_finite_f64",
1285 skip_serializing_if = "is_float::<0>"
1286 )]
1287 #[schemars(range(min = 0.0))]
1288 pub ecm_relative_tolerance_for_re: f64,
1289 #[serde(
1292 default,
1293 deserialize_with = "deserialize_nonnegative_finite_f64",
1294 skip_serializing_if = "is_float::<0>"
1295 )]
1296 #[schemars(range(min = 0.0))]
1297 pub ecm_relative_tolerance_for_im: f64,
1298 pub escalate_for_large_weight_threshold: f64,
1300}
1301
1302impl StabilityLevelSetting {
1303 pub fn default_double() -> Self {
1304 Self {
1305 precision: Precision::Double,
1306 required_precision_for_re: 1e-5,
1307 required_precision_for_im: 1e-5,
1308 ecm_relative_tolerance_for_re: 0.0,
1309 ecm_relative_tolerance_for_im: 0.0,
1310 escalate_for_large_weight_threshold: 0.9,
1311 }
1312 }
1313
1314 pub fn default_quad() -> Self {
1315 Self {
1316 precision: Precision::Quad,
1317 required_precision_for_re: 1e-5,
1318 required_precision_for_im: 1e-5,
1319 ecm_relative_tolerance_for_re: 0.0,
1320 ecm_relative_tolerance_for_im: 0.0,
1321 escalate_for_large_weight_threshold: -1.0,
1322 }
1323 }
1324
1325 pub fn default_arb() -> Self {
1326 Self {
1327 precision: Precision::Arb,
1328 required_precision_for_re: 1e-5,
1329 required_precision_for_im: 1e-5,
1330 ecm_relative_tolerance_for_re: 0.0,
1331 ecm_relative_tolerance_for_im: 0.0,
1332 escalate_for_large_weight_threshold: -1.0,
1333 }
1334 }
1335}
1336
1337#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
1338#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Encode, Decode, JsonSchema)]
1339#[serde(tag = "type")]
1340#[serde(deny_unknown_fields)]
1341pub enum RotationSetting {
1342 #[serde(rename = "x")]
1343 Pi2X {},
1344 #[serde(rename = "y")]
1345 Pi2Y {},
1346 #[serde(rename = "z")]
1347 Pi2Z {},
1348 #[serde(rename = "none")]
1349 None {},
1350 #[serde(rename = "euler_angles")]
1351 EulerAngles {
1352 alpha: f64,
1354 beta: f64,
1356 gamma: f64,
1358 },
1359}
1360
1361impl Default for RotationSetting {
1362 fn default() -> Self {
1363 Self::Pi2Z {}
1364 }
1365}
1366
1367impl RotationSetting {
1368 pub(crate) fn rotation_method(&self) -> RotationMethod {
1369 match self {
1370 Self::Pi2X {} => RotationMethod::Pi2X,
1371 Self::Pi2Y {} => RotationMethod::Pi2Y,
1372 Self::Pi2Z {} => RotationMethod::Pi2Z,
1373 Self::None {} => RotationMethod::Identity,
1374 Self::EulerAngles { alpha, beta, gamma } => {
1375 RotationMethod::EulerAngles(*alpha, *beta, *gamma)
1376 }
1377 }
1378 }
1379
1380 pub(crate) fn _as_str(&self) -> String {
1381 match self {
1382 Self::Pi2X {} => "x".to_owned(),
1383 Self::Pi2Y {} => "y".to_owned(),
1384 Self::Pi2Z {} => "z".to_owned(),
1385 Self::None {} => "none".to_owned(),
1386 Self::EulerAngles { alpha, beta, gamma } => {
1387 format!("euler {} {} {}", alpha, beta, gamma)
1388 }
1389 }
1390 }
1391}
1392
1393#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
1394#[derive(
1395 Serialize, Deserialize, Debug, Clone, PartialEq, Copy, Hash, Eq, Encode, Decode, JsonSchema,
1396)]
1397#[serde(deny_unknown_fields)]
1398pub enum Precision {
1399 Double,
1400 Quad,
1401 Arb,
1402}
1403
1404impl Display for Precision {
1405 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1406 match self {
1407 Self::Double => write!(f, "f64"),
1408 Self::Quad => write!(f, "f128"),
1409 Self::Arb => write!(f, "arb"),
1410 }
1411 }
1412}
1413
1414#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
1415#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, Encode, Decode, JsonSchema)]
1416#[serde(deny_unknown_fields)]
1417pub enum ParameterizationMode {
1418 #[serde(rename = "cartesian")]
1419 Cartesian,
1420 #[serde(rename = "spherical")]
1421 #[default]
1422 Spherical,
1423 #[serde(rename = "hyperspherical")]
1424 HyperSpherical,
1425 #[serde(rename = "hyperspherical_flat")]
1426 HyperSphericalFlat,
1427 #[serde(rename = "momentum_space")]
1428 MomentumSpace,
1429 #[serde(rename = "relative_spherical")]
1430 RelativeSpherical,
1431 #[serde(rename = "spherical_common_radial")]
1432 SphericalCommonRadial,
1433 #[serde(rename = "spherical_product_common_radial")]
1434 SphericalProductCommonRadial,
1435}
1436
1437#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
1438#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1439#[serde(default, deny_unknown_fields)]
1440pub struct OutputMetadata {
1441 #[serde(skip_serializing_if = "IsDefault::is_default")]
1442 pub model_name: String,
1443 #[serde(skip_serializing_if = "IsDefault::is_default")]
1444 pub output_type: String,
1445 #[serde(skip_serializing_if = "IsDefault::is_default")]
1446 pub contents: Vec<String>,
1447}
1448
1449impl Display for ParameterizationMode {
1450 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1451 match self {
1452 ParameterizationMode::Cartesian => write!(f, "cartesian"),
1453 ParameterizationMode::Spherical => write!(f, "spherical"),
1454 ParameterizationMode::RelativeSpherical => write!(f, "relative spherical"),
1455 ParameterizationMode::SphericalCommonRadial => {
1456 write!(f, "common-radial spherical")
1457 }
1458 ParameterizationMode::SphericalProductCommonRadial => {
1459 write!(f, "product/common-radial spherical")
1460 }
1461 ParameterizationMode::HyperSpherical => write!(f, "hyperspherical"),
1462 ParameterizationMode::HyperSphericalFlat => write!(f, "flat hyperspherical"),
1463 ParameterizationMode::MomentumSpace => write!(f, "momentum space"),
1464 }
1465 }
1466}
1467
1468#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
1469#[derive(Debug, Clone, Deserialize, PartialEq, Default, Serialize, Encode, Decode, JsonSchema)]
1470#[serde(deny_unknown_fields)]
1471pub enum ParameterizationMapping {
1472 #[serde(rename = "log")]
1473 Log,
1474 #[serde(rename = "power")]
1475 Power,
1476 #[serde(rename = "linear")]
1477 #[default]
1478 Linear,
1479}
1480
1481#[derive(Debug, Clone, PartialEq, Encode, Decode)]
1482#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
1483pub enum SamplingSettings {
1484 Default(ParameterizationSettings),
1485 MultiChanneling(MultiChannelingSettings),
1486 DiscreteGraphs(DiscreteGraphSamplingSettings),
1487}
1488
1489#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
1490#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
1491#[serde(default, deny_unknown_fields)]
1492pub struct SamplingSettingsParser {
1493 #[serde(skip_serializing_if = "IsDefault::is_default")]
1495 pub graphs: SumMode,
1496 #[serde(skip_serializing_if = "IsDefault::is_default")]
1498 pub graph_names: Vec<String>,
1499 #[serde(skip_serializing_if = "IsDefault::is_default")]
1501 pub orientations: SumMode,
1502 #[serde(skip_serializing_if = "is_false")]
1504 #[serde(rename = "sampling_multichanneling", alias = "lmb_multichanneling")]
1505 pub sampling_multichanneling: bool,
1506 #[serde(skip_serializing_if = "IsDefault::is_default")]
1508 #[serde(rename = "sampling_channels", alias = "lmb_channels")]
1509 pub sampling_channels: SumMode,
1510 #[serde(
1512 deserialize_with = "deserialize_nonnegative_finite_f64",
1513 skip_serializing_if = "is_float::<3>"
1514 )]
1515 #[schemars(range(min = 0.0))]
1516 pub alpha: f64,
1517 #[serde(skip_serializing_if = "IsDefault::is_default")]
1519 #[serde(rename = "sampling_channel_weight", alias = "lmb_channel_weight")]
1520 pub sampling_channel_weight: SamplingChannelWeight,
1521 #[serde(skip_serializing_if = "IsDefault::is_default")]
1523 pub coordinate_system: CoordinateSystem,
1524 #[serde(skip_serializing_if = "IsDefault::is_default")]
1526 pub mapping: ParameterizationMapping,
1527 #[serde(skip_serializing_if = "is_float::<1>")]
1529 pub b: f64,
1530 #[serde(skip_serializing_if = "is_float::<1>")]
1532 pub power: f64,
1533 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
1535 pub lmb_basis_ids: BTreeMap<String, Vec<usize>>,
1536 #[serde(skip_serializing_if = "Vec::is_empty")]
1538 pub default_channel_selection: Vec<String>,
1539 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
1541 pub channel_selection: BTreeMap<String, Vec<String>>,
1542 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
1544 pub channel_definitions: BTreeMap<String, BTreeMap<String, SamplingChannelDefinition>>,
1545}
1546
1547#[cfg(feature = "python")]
1552#[pyo3::pymethods]
1553impl SamplingSettingsParser {
1554 #[getter(lmb_multichanneling)]
1555 fn legacy_lmb_multichanneling(&self) -> bool {
1556 self.sampling_multichanneling
1557 }
1558
1559 #[setter(lmb_multichanneling)]
1560 fn set_legacy_lmb_multichanneling(&mut self, value: bool) {
1561 self.sampling_multichanneling = value;
1562 }
1563
1564 #[getter(lmb_channels)]
1565 fn legacy_lmb_channels(&self) -> SumMode {
1566 self.sampling_channels.clone()
1567 }
1568
1569 #[setter(lmb_channels)]
1570 fn set_legacy_lmb_channels(&mut self, value: SumMode) {
1571 self.sampling_channels = value;
1572 }
1573
1574 #[getter(lmb_channel_weight)]
1575 fn legacy_lmb_channel_weight(&self) -> SamplingChannelWeight {
1576 self.sampling_channel_weight
1577 }
1578
1579 #[setter(lmb_channel_weight)]
1580 fn set_legacy_lmb_channel_weight(&mut self, value: SamplingChannelWeight) {
1581 self.sampling_channel_weight = value;
1582 }
1583}
1584
1585impl Default for SamplingSettingsParser {
1586 fn default() -> Self {
1587 Self {
1588 graphs: SumMode::Summed,
1589 graph_names: Vec::new(),
1590 orientations: SumMode::Summed,
1591 sampling_multichanneling: false,
1592 sampling_channels: SumMode::Summed,
1593 alpha: 3.0,
1594 sampling_channel_weight: SamplingChannelWeight::default(),
1595 coordinate_system: CoordinateSystem::Spherical,
1596 mapping: ParameterizationMapping::Linear,
1597 b: 1.0,
1598 power: 1.0,
1599 lmb_basis_ids: BTreeMap::new(),
1600 default_channel_selection: Vec::new(),
1601 channel_selection: BTreeMap::new(),
1602 channel_definitions: BTreeMap::new(),
1603 }
1604 }
1605}
1606
1607#[derive(
1613 Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Encode, Decode, JsonSchema,
1614)]
1615#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
1616pub enum SamplingChannelWeight {
1617 #[serde(rename = "map_density")]
1618 #[default]
1619 MapDensity,
1620 #[serde(rename = "ose")]
1621 Ose,
1622 #[serde(rename = "singularity_proxy")]
1623 SingularityProxy,
1624 #[serde(rename = "inverse_jacobian")]
1625 InverseJacobian,
1626}
1627
1628#[derive(
1630 Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Encode, Decode, JsonSchema,
1631)]
1632#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
1633#[serde(rename_all = "snake_case")]
1634pub enum SamplingRadialProfileKind {
1635 #[default]
1636 LuH,
1637}
1638
1639#[derive(
1640 Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Encode, Decode, JsonSchema,
1641)]
1642#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
1643#[serde(rename_all = "snake_case")]
1644pub enum SamplingRadialApproximation {
1645 #[default]
1646 LogLogistic,
1647}
1648
1649#[derive(Debug, Clone, Serialize, PartialEq, Encode, Decode, JsonSchema)]
1652#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
1653#[schemars(with = "SamplingRadialProfileInput")]
1654pub struct SamplingRadialProfile {
1655 pub kind: SamplingRadialProfileKind,
1656 #[serde(skip_serializing_if = "IsDefault::is_default")]
1657 pub approximation: SamplingRadialApproximation,
1658 pub broad_fraction: f64,
1659 #[serde(skip_serializing_if = "IsDefault::is_default")]
1660 pub scale: Option<f64>,
1661 #[serde(skip_serializing_if = "IsDefault::is_default")]
1662 pub shape: Option<f64>,
1663}
1664
1665impl Default for SamplingRadialProfile {
1666 fn default() -> Self {
1667 Self {
1668 kind: SamplingRadialProfileKind::LuH,
1669 approximation: SamplingRadialApproximation::LogLogistic,
1670 broad_fraction: 0.02,
1671 scale: None,
1672 shape: None,
1673 }
1674 }
1675}
1676
1677#[derive(Deserialize, JsonSchema)]
1680#[serde(untagged, deny_unknown_fields)]
1681enum SamplingRadialProfileInput {
1682 Name(SamplingRadialProfileKind),
1683 Settings {
1684 kind: SamplingRadialProfileKind,
1685 #[serde(default)]
1686 approximation: SamplingRadialApproximation,
1687 #[serde(default = "SamplingRadialProfile::default_broad_fraction")]
1688 broad_fraction: f64,
1689 #[serde(default)]
1690 scale: Option<f64>,
1691 #[serde(default)]
1692 shape: Option<f64>,
1693 },
1694}
1695
1696impl SamplingRadialProfile {
1697 fn default_broad_fraction() -> f64 {
1698 Self::default().broad_fraction
1699 }
1700}
1701
1702impl<'de> Deserialize<'de> for SamplingRadialProfile {
1703 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1704 Ok(
1705 match SamplingRadialProfileInput::deserialize(deserializer)? {
1706 SamplingRadialProfileInput::Name(kind) => Self {
1707 kind,
1708 ..Self::default()
1709 },
1710 SamplingRadialProfileInput::Settings {
1711 kind,
1712 approximation,
1713 broad_fraction,
1714 scale,
1715 shape,
1716 } => Self {
1717 kind,
1718 approximation,
1719 broad_fraction,
1720 scale,
1721 shape,
1722 },
1723 },
1724 )
1725 }
1726}
1727
1728#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
1732#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
1733#[serde(default, deny_unknown_fields)]
1734pub struct SamplingChannelDefinition {
1735 pub around: String,
1736 #[serde(skip_serializing_if = "IsDefault::is_default")]
1740 pub channel_weight: Option<SamplingChannelWeight>,
1741 #[serde(skip_serializing_if = "IsDefault::is_default")]
1742 pub radial_profile: Option<SamplingRadialProfile>,
1743 pub subspace_lmb: Vec<usize>,
1747 pub parent_lmb: Vec<usize>,
1748 pub on_cut: Vec<usize>,
1749 #[serde(skip_serializing_if = "Option::is_none")]
1756 pub singularity_proxy: Option<String>,
1757}
1758
1759#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
1761#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
1762#[serde(default, deny_unknown_fields)]
1763pub struct SamplingChannelSelection {
1764 #[serde(skip_serializing_if = "IsDefault::is_default")]
1765 pub weight: SamplingChannelWeight,
1766 #[serde(
1769 deserialize_with = "deserialize_nonnegative_finite_f64",
1770 skip_serializing_if = "is_float::<3>"
1771 )]
1772 #[schemars(range(min = 0.0))]
1773 pub alpha: f64,
1774 pub default_channel_selection: Vec<String>,
1775 pub channel_selection: BTreeMap<String, Vec<String>>,
1776 pub channel_definitions: BTreeMap<String, BTreeMap<String, SamplingChannelDefinition>>,
1777}
1778
1779impl Default for SamplingChannelSelection {
1780 fn default() -> Self {
1781 Self {
1782 weight: SamplingChannelWeight::default(),
1783 alpha: 3.0,
1784 default_channel_selection: Vec::new(),
1785 channel_selection: BTreeMap::new(),
1786 channel_definitions: BTreeMap::new(),
1787 }
1788 }
1789}
1790
1791#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
1792#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
1793pub enum SumMode {
1794 #[serde(rename = "summed")]
1795 #[default]
1796 Summed,
1797 #[serde(rename = "monte_carlo")]
1798 MonteCarlo,
1799}
1800
1801#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema, Default)]
1802#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
1803pub enum CoordinateSystem {
1804 #[serde(rename = "cartesian")]
1805 Cartesian,
1806 #[serde(rename = "spherical")]
1807 #[default]
1808 Spherical,
1809 #[serde(rename = "hyperspherical")]
1810 HyperSpherical,
1811 #[serde(rename = "hyperspherical_flat")]
1812 HyperSphericalFlat,
1813 #[serde(rename = "momentum_space")]
1814 MomentumSpace,
1815 #[serde(rename = "tropical")]
1816 MomTrop,
1817 #[serde(rename = "relative_spherical")]
1818 RelativeSpherical,
1819 #[serde(rename = "spherical_common_radial")]
1820 SphericalCommonRadial,
1821 #[serde(rename = "spherical_product_common_radial")]
1822 SphericalProductCommonRadial,
1823}
1824
1825impl Default for SamplingSettings {
1826 fn default() -> Self {
1827 Self::Default(ParameterizationSettings::default())
1828 }
1829}
1830
1831fn validate_lmb_basis_ids(lmb_basis_ids: &BTreeMap<String, Vec<usize>>) -> Result<(), String> {
1832 for (graph_name, basis_ids) in lmb_basis_ids {
1833 if basis_ids.is_empty() {
1834 return Err(format!(
1835 "Invalid sampling settings: lmb_basis_ids entry for graph '{graph_name}' cannot be empty."
1836 ));
1837 }
1838
1839 let mut seen = BTreeSet::new();
1840 for basis_id in basis_ids {
1841 if !seen.insert(*basis_id) {
1842 return Err(format!(
1843 "Invalid sampling settings: lmb_basis_ids entry for graph '{graph_name}' contains duplicate basis id {basis_id}."
1844 ));
1845 }
1846 }
1847 }
1848
1849 Ok(())
1850}
1851
1852fn validate_sampling_channel_selection(
1853 default_selection: &[String],
1854 selection: &BTreeMap<String, Vec<String>>,
1855 definitions: &BTreeMap<String, BTreeMap<String, SamplingChannelDefinition>>,
1856) -> Result<(), String> {
1857 for (label, entries) in [("default_channel_selection", default_selection)] {
1858 if entries.iter().any(|entry| entry.trim().is_empty()) {
1859 return Err(format!(
1860 "Invalid sampling settings: {label} contains an empty channel selector."
1861 ));
1862 }
1863 }
1864 for (graph, entries) in selection {
1865 if graph.trim().is_empty() {
1866 return Err(
1867 "Invalid sampling settings: channel_selection contains an empty graph name."
1868 .to_string(),
1869 );
1870 }
1871 if entries.iter().any(|entry| entry.trim().is_empty()) {
1872 return Err(format!(
1873 "Invalid sampling settings: channel_selection entry for graph '{graph}' contains an empty selector."
1874 ));
1875 }
1876 }
1877 for (graph, graph_definitions) in definitions {
1878 if graph.trim().is_empty() {
1879 return Err(
1880 "Invalid sampling settings: channel_definitions contains an empty graph name."
1881 .to_string(),
1882 );
1883 }
1884 for (name, definition) in graph_definitions {
1885 if name.trim().is_empty() || definition.around.trim().is_empty() {
1886 return Err(format!(
1887 "Invalid sampling settings: channel definition '{graph}.{name}' requires a non-empty around expression."
1888 ));
1889 }
1890 if definition.parent_lmb.is_empty() {
1891 return Err(format!(
1892 "Invalid sampling settings: channel definition '{graph}.{name}' requires a non-empty parent_lmb."
1893 ));
1894 }
1895 let mut seen = BTreeSet::new();
1896 if definition.parent_lmb.iter().any(|edge| !seen.insert(edge)) {
1897 return Err(format!(
1898 "Invalid sampling settings: channel definition '{graph}.{name}' contains duplicate parent_lmb edge ids."
1899 ));
1900 }
1901 let mut seen_subspace = BTreeSet::new();
1902 if definition
1903 .subspace_lmb
1904 .iter()
1905 .any(|edge| !seen_subspace.insert(edge))
1906 {
1907 return Err(format!(
1908 "Invalid sampling settings: channel definition '{graph}.{name}' contains duplicate subspace_lmb edge ids."
1909 ));
1910 }
1911 let mut seen_on_cut = BTreeSet::new();
1912 if definition
1913 .on_cut
1914 .iter()
1915 .any(|edge| !seen_on_cut.insert(edge))
1916 {
1917 return Err(format!(
1918 "Invalid sampling settings: channel definition '{graph}.{name}' contains duplicate on_cut edge ids."
1919 ));
1920 }
1921 SamplingMapDefinition::parse(&definition.around).map_err(|error| {
1922 format!(
1923 "Invalid sampling settings: channel definition '{graph}.{name}' has an invalid around expression: {error}"
1924 )
1925 })?;
1926 }
1927 }
1928 Ok(())
1929}
1930
1931impl SamplingSettings {
1932 pub fn as_parser(&self) -> SamplingSettingsParser {
1933 let mut parser = SamplingSettingsParser::default();
1934 match self {
1935 Self::Default(_) => {}
1936 Self::MultiChanneling(_) => parser.sampling_multichanneling = true,
1937 Self::DiscreteGraphs(settings) => {
1938 parser.graphs = SumMode::MonteCarlo;
1939 parser.graph_names = settings.graph_names.clone();
1940 parser.orientations = if settings.sample_orientations {
1941 SumMode::MonteCarlo
1942 } else {
1943 SumMode::Summed
1944 };
1945 parser.sampling_multichanneling = matches!(
1946 settings.sampling_type,
1947 DiscreteGraphSamplingType::MultiChanneling(_)
1948 | DiscreteGraphSamplingType::SamplingMultiChanneling(_)
1949 );
1950 if matches!(
1951 settings.sampling_type,
1952 DiscreteGraphSamplingType::SamplingMultiChanneling(_)
1953 ) {
1954 parser.sampling_channels = SumMode::MonteCarlo;
1955 }
1956 }
1957 }
1958 match self.get_parameterization_settings() {
1959 Some(settings) => SamplingSettingsParser {
1960 alpha: settings.sampling_channels.alpha,
1961 sampling_channel_weight: settings.sampling_channels.weight,
1962 coordinate_system: CoordinateSystem::from_mode(settings.mode),
1963 mapping: settings.mapping,
1964 b: settings.b,
1965 power: settings.power,
1966 lmb_basis_ids: settings.lmb_basis_ids,
1967 default_channel_selection: settings.sampling_channels.default_channel_selection,
1968 channel_selection: settings.sampling_channels.channel_selection,
1969 channel_definitions: settings.sampling_channels.channel_definitions,
1970 ..parser
1971 },
1972 None => SamplingSettingsParser {
1973 coordinate_system: CoordinateSystem::MomTrop,
1974 ..parser
1975 },
1976 }
1977 }
1978
1979 fn from_parser(parser: SamplingSettingsParser) -> Result<Self, String> {
1980 let SamplingSettingsParser {
1981 graphs,
1982 graph_names,
1983 orientations,
1984 sampling_multichanneling,
1985 sampling_channels,
1986 alpha,
1987 sampling_channel_weight,
1988 coordinate_system,
1989 mapping,
1990 b,
1991 power,
1992 lmb_basis_ids,
1993 default_channel_selection,
1994 channel_selection,
1995 channel_definitions,
1996 } = parser;
1997
1998 validate_lmb_basis_ids(&lmb_basis_ids)?;
1999 let default_channel_selection =
2000 if sampling_multichanneling && default_channel_selection.is_empty() {
2001 vec!["auto:optimized_lmb".to_string()]
2002 } else {
2003 default_channel_selection
2004 };
2005 validate_sampling_channel_selection(
2006 &default_channel_selection,
2007 &channel_selection,
2008 &channel_definitions,
2009 )?;
2010 if !alpha.is_finite() || alpha < 0.0 {
2011 return Err(
2012 "Invalid sampling settings: alpha must be finite and nonnegative.".to_string(),
2013 );
2014 }
2015
2016 let mut seen_graph_names = BTreeSet::new();
2017 for graph_name in &graph_names {
2018 if !seen_graph_names.insert(graph_name) {
2019 return Err(format!(
2020 "Invalid sampling settings: graph_names contains duplicate graph name '{graph_name}'."
2021 ));
2022 }
2023 }
2024 if !graph_names.is_empty() && matches!(graphs, SumMode::Summed) {
2025 return Err(
2026 "Invalid sampling settings: graph_names requires graphs = 'monte_carlo'."
2027 .to_string(),
2028 );
2029 }
2030
2031 let sample_orientations = match (graphs.clone(), orientations) {
2032 (SumMode::Summed, SumMode::Summed) => false,
2033 (SumMode::Summed, SumMode::MonteCarlo) => {
2034 return Err(
2035 "Invalid sampling settings: orientations can only be set to 'monte_carlo' when graphs is 'monte_carlo'.".to_string(),
2036 )
2037 }
2038 (SumMode::MonteCarlo, SumMode::Summed) => false,
2039 (SumMode::MonteCarlo, SumMode::MonteCarlo) => true,
2040 };
2041
2042 if matches!(coordinate_system, CoordinateSystem::MomTrop) {
2043 if sampling_channel_weight != SamplingChannelWeight::MapDensity
2044 || !default_channel_selection.is_empty()
2045 || !channel_selection.is_empty()
2046 || !channel_definitions.is_empty()
2047 {
2048 return Err(
2049 "Invalid sampling settings: advanced sampling channels are incompatible with tropical coordinates."
2050 .to_string(),
2051 );
2052 }
2053 if !lmb_basis_ids.is_empty() {
2054 return Err(
2055 "Invalid sampling settings: coordinate_system = 'tropical' is incompatible with lmb_basis_ids."
2056 .to_string(),
2057 );
2058 }
2059 if !matches!(graphs, SumMode::MonteCarlo) {
2060 return Err(
2061 "Invalid sampling settings: coordinate_system = 'tropical' requires graphs = 'monte_carlo'."
2062 .to_string(),
2063 );
2064 }
2065
2066 if sampling_multichanneling {
2067 return Err(
2068 "Invalid sampling settings: coordinate_system = 'tropical' is incompatible with sampling_multichanneling = true."
2069 .to_string(),
2070 );
2071 }
2072
2073 return Ok(SamplingSettings::DiscreteGraphs(
2074 DiscreteGraphSamplingSettings {
2075 graph_names,
2076 sample_orientations,
2077 sampling_type: DiscreteGraphSamplingType::TropicalSampling(
2078 GammaloopTropicalSamplingSettings::default(),
2079 ),
2080 },
2081 ));
2082 }
2083
2084 let mode = coordinate_system.into_mode();
2085 if matches!(mode, ParameterizationMode::SphericalProductCommonRadial)
2086 && (!sampling_multichanneling
2087 || sampling_channel_weight != SamplingChannelWeight::InverseJacobian)
2088 {
2089 return Err(
2090 "Invalid sampling settings: coordinate_system = 'spherical_product_common_radial' requires sampling_multichanneling = true and sampling_channel_weight = 'inverse_jacobian'."
2091 .to_string(),
2092 );
2093 }
2094 if sampling_channel_weight == SamplingChannelWeight::InverseJacobian
2095 && matches!(mode, ParameterizationMode::HyperSphericalFlat)
2096 {
2097 return Err(
2098 "Invalid sampling settings: sampling_channel_weight = 'inverse_jacobian' is incompatible with coordinate_system = 'hyperspherical_flat' because the inverse map is not available."
2099 .to_string(),
2100 );
2101 }
2102 if matches!(mapping, ParameterizationMapping::Power) {
2103 if !matches!(
2104 mode,
2105 ParameterizationMode::Spherical
2106 | ParameterizationMode::RelativeSpherical
2107 | ParameterizationMode::SphericalCommonRadial
2108 | ParameterizationMode::SphericalProductCommonRadial
2109 | ParameterizationMode::HyperSpherical
2110 | ParameterizationMode::HyperSphericalFlat
2111 ) {
2112 return Err(
2113 "Invalid sampling settings: mapping = 'power' requires a spherical coordinate system."
2114 .to_string(),
2115 );
2116 }
2117 if !power.is_finite() || power < 1.0 {
2118 return Err(
2119 "Invalid sampling settings: mapping = 'power' requires power >= 1.".to_string(),
2120 );
2121 }
2122 }
2123 let parameterization_settings = ParameterizationSettings {
2124 mode,
2125 mapping,
2126 b,
2127 power,
2128 lmb_basis_ids,
2129 sampling_channels: SamplingChannelSelection {
2130 weight: sampling_channel_weight,
2131 alpha,
2132 default_channel_selection,
2133 channel_selection,
2134 channel_definitions,
2135 },
2136 };
2137
2138 match graphs {
2139 SumMode::Summed => {
2140 if !matches!(sampling_channels, SumMode::Summed) {
2141 return Err(
2142 "Invalid sampling settings: sampling_channels = 'monte_carlo' requires graphs = 'monte_carlo'."
2143 .to_string(),
2144 );
2145 }
2146
2147 if sample_orientations {
2148 return Err(
2149 "Invalid sampling settings: orientations can only be Monte Carlo sampled when graphs are Monte Carlo sampled."
2150 .to_string(),
2151 );
2152 }
2153
2154 if sampling_multichanneling {
2155 Ok(SamplingSettings::MultiChanneling(MultiChannelingSettings {
2156 parameterization_settings,
2157 }))
2158 } else {
2159 Ok(SamplingSettings::Default(parameterization_settings))
2160 }
2161 }
2162 SumMode::MonteCarlo => {
2163 let sampling_type = if sampling_multichanneling {
2164 let settings = MultiChannelingSettings {
2165 parameterization_settings,
2166 };
2167
2168 match sampling_channels {
2169 SumMode::Summed => DiscreteGraphSamplingType::MultiChanneling(settings),
2170 SumMode::MonteCarlo => {
2171 DiscreteGraphSamplingType::SamplingMultiChanneling(settings)
2172 }
2173 }
2174 } else {
2175 DiscreteGraphSamplingType::Default(parameterization_settings)
2176 };
2177
2178 Ok(SamplingSettings::DiscreteGraphs(
2179 DiscreteGraphSamplingSettings {
2180 graph_names,
2181 sample_orientations,
2182 sampling_type,
2183 },
2184 ))
2185 }
2186 }
2187 }
2188}
2189
2190impl Serialize for SamplingSettings {
2191 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2192 where
2193 S: Serializer,
2194 {
2195 self.as_parser().serialize(serializer)
2196 }
2197}
2198
2199impl<'de> Deserialize<'de> for SamplingSettings {
2200 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2201 where
2202 D: Deserializer<'de>,
2203 {
2204 let parser = SamplingSettingsParser::deserialize(deserializer)?;
2205 SamplingSettings::from_parser(parser).map_err(serde::de::Error::custom)
2206 }
2207}
2208
2209impl JsonSchema for SamplingSettings {
2210 fn schema_name() -> std::borrow::Cow<'static, str> {
2211 "SamplingSettings".into()
2212 }
2213
2214 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
2215 SamplingSettingsParser::json_schema(generator)
2216 }
2217}
2218
2219impl CoordinateSystem {
2220 fn from_mode(mode: ParameterizationMode) -> Self {
2221 match mode {
2222 ParameterizationMode::Cartesian => Self::Cartesian,
2223 ParameterizationMode::Spherical => Self::Spherical,
2224 ParameterizationMode::RelativeSpherical => Self::RelativeSpherical,
2225 ParameterizationMode::SphericalCommonRadial => Self::SphericalCommonRadial,
2226 ParameterizationMode::SphericalProductCommonRadial => {
2227 Self::SphericalProductCommonRadial
2228 }
2229 ParameterizationMode::HyperSpherical => Self::HyperSpherical,
2230 ParameterizationMode::HyperSphericalFlat => Self::HyperSphericalFlat,
2231 ParameterizationMode::MomentumSpace => Self::MomentumSpace,
2232 }
2233 }
2234
2235 fn into_mode(self) -> ParameterizationMode {
2236 match self {
2237 CoordinateSystem::Cartesian => ParameterizationMode::Cartesian,
2238 CoordinateSystem::Spherical => ParameterizationMode::Spherical,
2239 CoordinateSystem::RelativeSpherical => ParameterizationMode::RelativeSpherical,
2240 CoordinateSystem::SphericalCommonRadial => ParameterizationMode::SphericalCommonRadial,
2241 CoordinateSystem::SphericalProductCommonRadial => {
2242 ParameterizationMode::SphericalProductCommonRadial
2243 }
2244 CoordinateSystem::HyperSpherical => ParameterizationMode::HyperSpherical,
2245 CoordinateSystem::HyperSphericalFlat => ParameterizationMode::HyperSphericalFlat,
2246 CoordinateSystem::MomentumSpace => ParameterizationMode::MomentumSpace,
2247 CoordinateSystem::MomTrop => {
2248 unreachable!("tropical coordinate system has no ParameterizationMode equivalent")
2249 }
2250 }
2251 }
2252}
2253
2254impl SamplingSettings {
2255 pub(crate) fn uses_sampling_channels(&self) -> bool {
2259 match self {
2260 Self::MultiChanneling(_) => true,
2261 Self::DiscreteGraphs(settings) => matches!(
2262 &settings.sampling_type,
2263 DiscreteGraphSamplingType::MultiChanneling(_)
2264 | DiscreteGraphSamplingType::SamplingMultiChanneling(_)
2265 ),
2266 Self::Default(_) => false,
2267 }
2268 }
2269
2270 pub fn selected_graph_names(&self) -> &[String] {
2271 match self {
2272 SamplingSettings::DiscreteGraphs(settings) => &settings.graph_names,
2273 SamplingSettings::Default(_) | SamplingSettings::MultiChanneling(_) => &[],
2274 }
2275 }
2276
2277 pub fn get_parameterization_settings(&self) -> Option<ParameterizationSettings> {
2278 match self {
2279 SamplingSettings::Default(settings) => Some(settings.clone()),
2280 SamplingSettings::MultiChanneling(settings) => {
2281 Some(settings.parameterization_settings.clone())
2282 }
2283 SamplingSettings::DiscreteGraphs(settings) => match &settings.sampling_type {
2284 DiscreteGraphSamplingType::Default(settings) => Some(settings.clone()),
2285 DiscreteGraphSamplingType::MultiChanneling(settings) => {
2286 Some(settings.parameterization_settings.clone())
2287 }
2288 DiscreteGraphSamplingType::SamplingMultiChanneling(settings) => {
2289 Some(settings.parameterization_settings.clone())
2290 }
2291 DiscreteGraphSamplingType::TropicalSampling(_) => None,
2292 },
2293 }
2294 }
2295
2296 pub(crate) fn discrete_depth(&self) -> usize {
2297 match self {
2298 SamplingSettings::Default(_) => 0,
2299 SamplingSettings::MultiChanneling(_) => 0,
2300 SamplingSettings::DiscreteGraphs(settings) => {
2301 let depth_from_orientations = settings.sample_orientations as usize;
2302
2303 match &settings.sampling_type {
2304 DiscreteGraphSamplingType::Default(_) => 1 + depth_from_orientations,
2305 DiscreteGraphSamplingType::MultiChanneling(_) => 1 + depth_from_orientations,
2306 DiscreteGraphSamplingType::SamplingMultiChanneling(_) => {
2307 2 + depth_from_orientations
2308 }
2309 DiscreteGraphSamplingType::TropicalSampling(_) => 1 + depth_from_orientations,
2310 }
2311 }
2312 }
2313 }
2314
2315 pub(crate) fn describe_settings(&self) -> String {
2316 match self {
2317 SamplingSettings::Default(settings) => {
2318 format!("{} coordinates", settings.mode)
2319 }
2320 SamplingSettings::MultiChanneling(settings) => {
2321 format!(
2322 "lmb multichanneling in {} coordinates",
2323 settings.parameterization_settings.mode
2324 )
2325 }
2326 SamplingSettings::DiscreteGraphs(settings) => {
2327 let discrete_graph_string = if settings.graph_names.is_empty() {
2328 "Monte Carlo over graphs".to_string()
2329 } else {
2330 format!(
2331 "Monte Carlo over selected graph groups [{}]",
2332 settings.graph_names.join(", ")
2333 )
2334 };
2335 let orientation_sampling_string = if settings.sample_orientations {
2336 "and Monte Carlo over orientations"
2337 } else {
2338 ""
2339 };
2340
2341 match &settings.sampling_type {
2342 DiscreteGraphSamplingType::Default(settings) => {
2343 format!(
2344 "{} {} in {} coordinates",
2345 discrete_graph_string, orientation_sampling_string, settings.mode
2346 )
2347 }
2348 DiscreteGraphSamplingType::MultiChanneling(settings) => {
2349 format!(
2350 "{}, lmb multichanneling in {} coordinates {}",
2351 discrete_graph_string,
2352 settings.parameterization_settings.mode,
2353 orientation_sampling_string,
2354 )
2355 }
2356 DiscreteGraphSamplingType::SamplingMultiChanneling(settings) => {
2357 format!(
2358 "{}, {} and monte carlo over lmbs in {} coordinates",
2359 discrete_graph_string,
2360 orientation_sampling_string,
2361 settings.parameterization_settings.mode
2362 )
2363 }
2364 DiscreteGraphSamplingType::TropicalSampling(_) => {
2365 format!(
2366 "{} {} using 🌴🥥 tropical sampling 🥥🌴",
2367 discrete_graph_string, orientation_sampling_string,
2368 )
2369 }
2370 }
2371 }
2372 }
2373 }
2374}
2375
2376#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
2377#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
2378#[serde(deny_unknown_fields, default)]
2379pub struct MultiChannelingSettings {
2380 #[serde(skip_serializing_if = "IsDefault::is_default")]
2382 pub parameterization_settings: ParameterizationSettings,
2383}
2384
2385#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
2386#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
2387#[serde(deny_unknown_fields, default)]
2388pub struct GammaloopTropicalSamplingSettings {
2389 #[serde(skip_serializing_if = "is_true")]
2391 pub upcast_on_failure: bool,
2392 #[serde(skip_serializing_if = "IsDefault::is_default")]
2394 pub matrix_stability_test: Option<f64>,
2395}
2396
2397impl Default for GammaloopTropicalSamplingSettings {
2398 fn default() -> Self {
2399 Self {
2400 upcast_on_failure: true,
2401 matrix_stability_test: None,
2402 }
2403 }
2404}
2405
2406impl GammaloopTropicalSamplingSettings {
2407 pub fn into_tropical_sampling_settings(&self) -> momtrop::TropicalSamplingSettings {
2408 momtrop::TropicalSamplingSettings {
2409 matrix_stability_test: self.matrix_stability_test,
2410 print_debug_info: false,
2411 return_metadata: false,
2412 }
2413 }
2414}
2415
2416#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
2417#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
2418#[serde(tag = "subtype", deny_unknown_fields)]
2419pub enum DiscreteGraphSamplingType {
2420 #[serde(rename = "default")]
2421 Default(ParameterizationSettings),
2422 #[serde(rename = "multi_channeling")]
2423 MultiChanneling(MultiChannelingSettings),
2424 #[serde(rename = "discrete_multi_channeling")]
2425 SamplingMultiChanneling(MultiChannelingSettings),
2426 #[serde(rename = "tropical")]
2427 TropicalSampling(GammaloopTropicalSamplingSettings),
2428}
2429
2430impl Default for DiscreteGraphSamplingType {
2431 fn default() -> Self {
2432 DiscreteGraphSamplingType::Default(ParameterizationSettings::default())
2433 }
2434}
2435
2436#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
2437#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema, Default)]
2438#[serde(deny_unknown_fields, default)]
2439pub struct DiscreteGraphSamplingSettings {
2440 #[serde(skip_serializing_if = "IsDefault::is_default")]
2442 pub graph_names: Vec<String>,
2443 #[serde(skip_serializing_if = "is_false")]
2445 pub sample_orientations: bool,
2446 #[serde(skip_serializing_if = "IsDefault::is_default")]
2448 pub sampling_type: DiscreteGraphSamplingType,
2449}
2450
2451#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
2452#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
2453#[serde(deny_unknown_fields, default)]
2454pub struct LocalCounterTermSettings {
2455 #[serde(skip_serializing_if = "IsDefault::is_default")]
2457 pub uv_localisation: UVLocalisationSettings,
2458}
2459
2460#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
2461#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
2462#[serde(default, deny_unknown_fields)]
2463pub struct UVLocalisationSettings {
2464 #[serde(skip_serializing_if = "is_float::<10>")]
2466 pub sliver_width: f64,
2467 #[serde(skip_serializing_if = "is_false")]
2469 pub smooth_sliver: bool,
2470 #[serde(skip_serializing_if = "is_false")]
2472 pub dynamic_width: bool,
2473 #[serde(skip_serializing_if = "is_float::<1>")]
2475 pub gaussian_width: f64,
2476 #[serde(skip_serializing_if = "is_false")]
2478 pub force_uv_dampers_to_one: bool,
2479}
2480
2481impl Default for UVLocalisationSettings {
2482 fn default() -> Self {
2483 Self {
2484 sliver_width: 10.0,
2485 smooth_sliver: false,
2486 dynamic_width: false,
2487 gaussian_width: 1.0,
2488 force_uv_dampers_to_one: false,
2489 }
2490 }
2491}
2492
2493#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
2494#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
2495#[serde(deny_unknown_fields, default)]
2496pub struct IntegratedCounterTermSettings {
2497 #[serde(skip_serializing_if = "IsDefault::is_default")]
2499 pub range: IntegratedCounterTermRange,
2500}
2501
2502#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
2503#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
2504#[serde(tag = "type")]
2505#[serde(deny_unknown_fields)]
2506pub enum IntegratedCounterTermRange {
2507 #[serde(rename = "infinite")]
2509 Infinite {
2510 h_function_settings: HFunctionSettings,
2512 },
2513 #[serde(rename = "compact")]
2515 Compact {},
2516}
2517
2518impl Default for IntegratedCounterTermRange {
2519 fn default() -> Self {
2520 Self::Infinite {
2521 h_function_settings: HFunctionSettings::default(),
2522 }
2523 }
2524}
2525
2526#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object, get_all, set_all))]
2527#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
2528#[serde(deny_unknown_fields)]
2529#[serde(default)]
2530pub struct OverlapSettings {
2531 #[serde(skip_serializing_if = "IsDefault::is_default")]
2534 pub force_global_center: Option<Vec<[f64; 3]>>,
2535 #[serde(skip_serializing_if = "is_true")]
2537 pub check_global_center: bool,
2538 #[serde(skip_serializing_if = "is_true")]
2540 pub try_origin: bool,
2541 #[serde(skip_serializing_if = "is_false")]
2543 pub try_origin_all_lmbs: bool,
2544}
2545
2546impl Default for OverlapSettings {
2547 fn default() -> Self {
2548 Self {
2549 force_global_center: None,
2550 check_global_center: true,
2551 try_origin: true,
2552 try_origin_all_lmbs: false,
2553 }
2554 }
2555}
2556
2557#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
2558#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
2559#[serde(deny_unknown_fields)]
2560pub enum HFunction {
2562 #[default]
2563 #[serde(rename = "poly_exponential")]
2564 PolyExponential,
2565 #[serde(rename = "exponential")]
2566 Exponential,
2567 #[serde(rename = "poly_left_right_exponential")]
2568 PolyLeftRightExponential,
2569 #[serde(rename = "exponential_ct")]
2570 ExponentialCT,
2571}
2572
2573#[cfg_attr(feature = "python", pyo3::pyclass(from_py_object))]
2574#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Encode, Decode, JsonSchema)]
2575#[serde(deny_unknown_fields)]
2576#[serde(default)]
2577pub struct HFunctionSettings {
2578 #[serde(skip_serializing_if = "IsDefault::is_default")]
2580 pub function: HFunction,
2581 #[serde(skip_serializing_if = "is_float::<1>")]
2583 pub sigma: f64,
2584 #[serde(skip_serializing_if = "is_true")]
2586 pub enabled_dampening: bool,
2587 #[serde(skip_serializing_if = "IsDefault::is_default")]
2589 pub power: Option<usize>,
2590}
2591
2592impl Default for HFunctionSettings {
2593 fn default() -> Self {
2594 Self {
2595 sigma: 1.0,
2596 function: HFunction::default(),
2597 enabled_dampening: true,
2598 power: None,
2599 }
2600 }
2601}
2602
2603impl HFunctionSettings {
2604 const NORMALIZED_POWERS: [usize; 12] = [0, 1, 3, 4, 6, 7, 9, 10, 12, 13, 15, 16];
2606
2607 pub(crate) fn normalization<T: FloatLike>(&self, sigma: &F<T>) -> F<T> {
2613 let one = sigma.one();
2614 let power = self.power.unwrap_or(0);
2615 if self.function != HFunction::Exponential && !Self::NORMALIZED_POWERS.contains(&power) {
2616 panic!("Value {power} of power in poly exponential h function not supported");
2617 }
2618 let unit = match self.function {
2619 HFunction::Exponential => Self::scaled_bessel_k_at_two(1, &one),
2620 HFunction::PolyExponential => Self::scaled_bessel_k_at_two(power.abs_diff(1), &one),
2621 HFunction::PolyLeftRightExponential => {
2622 one.from_usize(2) * Self::scaled_bessel_k_at_two(2 * power.abs_diff(1), &one)
2623 }
2624 HFunction::ExponentialCT => {
2625 unreachable!("exponential_ct is a threshold localization without a normalization")
2626 }
2627 };
2628 unit * sigma
2629 }
2630
2631 fn scaled_bessel_k_at_two<T: FloatLike>(twice_order: usize, one: &F<T>) -> F<T> {
2640 use std::{
2641 any::{Any, TypeId},
2642 cell::RefCell,
2643 collections::HashMap,
2644 };
2645 use symbolica::domains::float::SingleFloat;
2646
2647 if twice_order % 2 == 1 {
2648 let n = twice_order / 2;
2649 let sum = (0..=n).fold(Rational::zero(), |sum, k| {
2650 sum + Rational::from((
2651 Integer::factorial((n + k) as u32),
2652 Integer::factorial(k as u32)
2653 * Integer::factorial((n - k) as u32)
2654 * Integer::from(4).pow(k as u64),
2655 ))
2656 });
2657 return one.PI().sqrt() * one.from_rational(&(sum / Rational::from(2)));
2658 }
2659 type NativeValues = HashMap<(TypeId, u32, usize), Box<dyn Any>>;
2663 thread_local! {
2664 static CACHE: RefCell<NativeValues> = RefCell::default();
2665 }
2666 let key = (TypeId::of::<T>(), one.0.get_precision(), twice_order);
2667 if let Some(value) = CACHE.with_borrow(|cache| {
2668 cache.get(&key).map(|value| {
2669 value
2670 .downcast_ref::<F<T>>()
2671 .expect("cache key includes the numeric type")
2672 .clone()
2673 })
2674 }) {
2675 return value;
2676 }
2677
2678 let (mut i0, mut i1, mut harmonic_sum) = (one.zero(), one.zero(), one.zero());
2680 let (mut inverse_square, mut harmonic) = (one.clone(), one.zero());
2681 for k in 1.. {
2682 i0 += &inverse_square;
2683 i1 += &inverse_square / one.from_usize(k);
2684 inverse_square = &inverse_square / one.from_usize(k * k);
2685 harmonic += one.from_usize(k).inv();
2686 let term = &harmonic * &inverse_square;
2688 harmonic_sum += &term;
2689 if term <= &harmonic_sum * one.epsilon() {
2690 break;
2691 }
2692 }
2693 let k0 = harmonic_sum - one.euler() * &i0;
2694 let k1 = (one.from_usize(2).inv() - &i1 * &k0) / &i0;
2695 let order = twice_order / 2;
2696 let (mut lower, mut upper) = (k0, k1);
2697 for n in 1..order {
2698 (lower, upper) = (upper.clone(), lower + one.from_usize(n) * &upper);
2699 }
2700 let value = one.from_usize(2).exp() * if order == 0 { lower } else { upper };
2701 CACHE.with_borrow_mut(|cache| {
2702 if cache.len() == 1024 {
2704 cache.clear();
2705 }
2706 cache.insert(key, Box::new(value.clone()));
2707 });
2708 value
2709 }
2710
2711 pub(crate) fn validate_normalized_profile(&self) -> Result<()> {
2714 if !self.sigma.is_finite() || self.sigma <= 0.0 {
2715 return Err(eyre::eyre!(
2716 "A normalized h_function profile requires a positive finite h_function.sigma; got {}",
2717 self.sigma
2718 ));
2719 }
2720 match self.function {
2721 HFunction::Exponential => Ok(()),
2722 HFunction::PolyExponential | HFunction::PolyLeftRightExponential => {
2723 let power = self.power.unwrap_or(0);
2724 if Self::NORMALIZED_POWERS.contains(&power) {
2725 Ok(())
2726 } else {
2727 Err(eyre::eyre!(
2728 "Unsupported h_function.power {power}; supported powers are {:?}",
2729 Self::NORMALIZED_POWERS
2730 ))
2731 }
2732 }
2733 HFunction::ExponentialCT => Err(eyre::eyre!(
2734 "exponential_ct is a local threshold localization, not a normalized h_function profile"
2735 )),
2736 }
2737 }
2738}