1use std::{
2 collections::{BTreeMap, HashMap},
3 fmt,
4 fs::{self, File},
5 io::Write,
6 iter,
7 path::Path,
8 sync::Arc,
9};
10
11use ahash::AHashSet;
12use bincode_trait_derive::{Decode, Encode};
14use color_eyre::Result;
15use momtrop::SampleGenerator;
16
17use idenso::dirac::GammaSimplifier;
18use rayon::{
19 ThreadPool,
20 iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator},
21};
22use spenso::algebra::complex::Complex;
23use tracing::{info_span, instrument};
24use tracing_indicatif::{span_ext::IndicatifSpanExt, style::ProgressStyle};
25use vakint::{EvaluationMethod, NumericalEvaluationResult, Vakint, vakint_symbol};
26
27use crate::{
28 GammaLoopContext, GammaLoopContextContainer,
29 cff::{
30 CutCFFIndex,
31 esurface::{GroupEsurfaceId, RaisedEsurfaceData, RaisedEsurfaceGroup, RaisedEsurfaceId},
32 },
33 feyngen::GenerationType,
34 graph::{
35 GraphGroup, GraphGroupPosition, GroupId, LMBext, LmbIndex, LoopMomentumBasis,
36 cuts::{CutSet, ResidueSelector},
37 threshold_counterterms::{
38 ThresholdCountertermMultiplier, ThresholdCountertermSpec, ThresholdCountertermVariant,
39 },
40 },
41 integrands::process::{
42 GenericEvaluator, LmbMultiChannelingSetup,
43 amplitude::{AmplitudeGraphTerm, AmplitudeIntegrand, AmplitudeIntegrandData},
44 graph_to_group_id_for_group_structure,
45 param_builder::FnMapEntry,
46 },
47 model::ArcParticle,
48 momentum::{
49 sample::{ExternalIndex, SubspaceData},
50 signature::SignatureLike,
51 },
52 processes::{
53 DotExportSettings, EvaluatorSettings, GraphGenerationStats, GraphGroupSelectionPlan,
54 GraphGroupSelectionSpec, NamedGraphGenerationReport,
55 ResolvedThresholdCountertermAssociation, ResolvedThresholdCountertermVariant,
56 ResolvedThresholdCounterterms, SingleThresholdPieces, StandaloneExportSettings,
57 ThresholdCountertermOrigin, ThresholdCountertermSide, ThresholdCountertermVariantId,
58 build_derivative_structure_atom, params_for_derivative_order,
59 },
60 settings::{
61 GlobalSettings, RuntimeSettings, global::OrientationPattern, runtime::LockedRuntimeSettings,
62 },
63 subtraction::amplitude_counterterm::AmplitudeCountertermAtom,
64 utils::{F, GS, Length, W_},
65 uv::{
66 Integrands, RenormalizationPart, UVgenerationSettings, UltravioletGraph,
67 approx::{CutStructure, OrientationProjection, integrated::to_vakint_integrand},
68 settings::VakintSettings,
69 },
70};
71use eyre::{Context, eyre};
72use itertools::Itertools;
73use linnet::{
74 half_edge::{
75 involution::{EdgeIndex, Flow, HedgePair},
76 subgraph::{ModifySubSet, SuBitGraph, SubGraphLike, SubSetOps},
77 },
78 parser::DotGraph,
79};
80use spenso::shadowing::symbolica_utils::LogPrint;
81use symbolica::{atom::Var, prelude::*};
82use three_dimensional_reps::GeneratedThreeDExpression;
83use tracing::{debug, info};
84use typed_index_collections::{TiVec, ti_vec};
85
86use super::generation_progress::{self, GenerationProcessKind, GenerationProgressPhase};
87
88use crate::{
89 cff::esurface::EsurfaceID,
90 graph::{FeynmanGraph, Graph},
91 integrands::process::ProcessIntegrand,
92 model::Model,
93 settings::global::{GenerationSettings, MediumMode},
94};
95
96use crate::graph::parse::complete_group_parsing;
97
98#[derive(Clone, Encode, Decode)]
99#[trait_decode(trait = GammaLoopContext)]
100pub struct Amplitude {
101 pub name: String,
102 pub integrand: Option<ProcessIntegrand>,
103 pub graphs: Vec<AmplitudeGraph>,
104 pub graph_group_structure: TiVec<GroupId, GraphGroup>,
105 pub external_particles: Vec<ArcParticle>,
106 pub external_signature: SignatureLike<ExternalIndex>,
107 pub group_derived_data: TiVec<GroupId, GroupDerivedData>,
108}
109
110#[derive(Clone, Encode, Decode)]
111#[trait_decode(trait = GammaLoopContext)]
112pub struct GroupDerivedData {
113 pub esurface_map: TiVec<GroupEsurfaceId, TiVec<GraphGroupPosition, Option<RaisedEsurfaceId>>>,
114 pub esurface_atoms: TiVec<GroupEsurfaceId, Atom>,
115}
116
117impl Amplitude {
118 pub fn plan_graph_group_selection(
119 &self,
120 spec: &GraphGroupSelectionSpec,
121 ) -> Result<GraphGroupSelectionPlan> {
122 spec.plan(&self.graph_group_structure, |graph_id| {
123 self.graphs.get(graph_id).map(|graph| &graph.graph)
124 })
125 }
126
127 pub fn validate_graph_group_selection_plan(
128 &self,
129 plan: &GraphGroupSelectionPlan,
130 ) -> Result<()> {
131 if !self.group_derived_data.is_empty()
132 && self.group_derived_data.len() != self.graph_group_structure.len()
133 {
134 return Err(eyre!(
135 "Amplitude '{}' has {} group-derived-data entries for {} graph groups.",
136 self.name,
137 self.group_derived_data.len(),
138 self.graph_group_structure.len()
139 ));
140 }
141
142 for &old_group_id in plan.retained_group_ids() {
143 plan.new_group_id_for_old(old_group_id).ok_or_else(|| {
144 eyre!(
145 "Selection plan is missing compact group id for old group {}.",
146 old_group_id.0
147 )
148 })?;
149 if old_group_id.0 >= self.graph_group_structure.len() {
150 return Err(eyre!(
151 "Selection plan refers to missing graph group {}.",
152 old_group_id.0
153 ));
154 }
155 let group = &self.graph_group_structure[old_group_id];
156 for old_graph_id in group {
157 if old_graph_id >= self.graphs.len() {
158 return Err(eyre!(
159 "Graph group {} refers to missing graph id {}.",
160 old_group_id.0,
161 old_graph_id
162 ));
163 }
164 }
165 let master = group.master();
166 if master >= self.graphs.len() {
167 return Err(eyre!(
168 "Graph group {} refers to missing master graph id {}.",
169 old_group_id.0,
170 master
171 ));
172 }
173 }
174
175 Ok(())
176 }
177
178 pub fn apply_graph_group_selection(&mut self, plan: &GraphGroupSelectionPlan) -> Result<()> {
179 self.validate_graph_group_selection_plan(plan)?;
180
181 let mut old_graph_to_new_group = vec![None; self.graphs.len()];
182 let mut old_graph_is_master = vec![false; self.graphs.len()];
183 for &old_group_id in plan.retained_group_ids() {
184 let new_group_id = plan.new_group_id_for_old(old_group_id).ok_or_else(|| {
185 eyre!(
186 "Selection plan is missing compact group id for old group {}.",
187 old_group_id.0
188 )
189 })?;
190 let group = &self.graph_group_structure[old_group_id];
191 for old_graph_id in group {
192 old_graph_to_new_group[old_graph_id] = Some(new_group_id);
193 }
194 old_graph_is_master[group.master()] = true;
195 }
196
197 let mut new_graphs = self
198 .graphs
199 .iter()
200 .cloned()
201 .enumerate()
202 .filter_map(|(old_graph_id, mut graph)| {
203 let new_group_id = old_graph_to_new_group[old_graph_id]?;
204 graph.graph.group_id = Some(new_group_id);
205 graph.graph.is_group_master = old_graph_is_master[old_graph_id];
206 if let Some(multi_channeling_setup) = &mut graph.derived_data.multi_channeling_setup
207 {
208 multi_channeling_setup.graph.group_id = Some(new_group_id);
209 multi_channeling_setup.graph.is_group_master =
210 old_graph_is_master[old_graph_id];
211 }
212 Some(graph)
213 })
214 .collect::<Vec<_>>();
215
216 let mut parsed_graphs = new_graphs
217 .iter()
218 .map(|graph| graph.graph.clone())
219 .collect::<Vec<_>>();
220 let new_graph_group_structure = complete_group_parsing(&mut parsed_graphs)?;
221 for (graph, parsed_graph) in new_graphs.iter_mut().zip(parsed_graphs) {
222 graph.graph.group_id = parsed_graph.group_id;
223 graph.graph.is_group_master = parsed_graph.is_group_master;
224 if let Some(multi_channeling_setup) = &mut graph.derived_data.multi_channeling_setup {
225 multi_channeling_setup.graph.group_id = graph.graph.group_id;
226 multi_channeling_setup.graph.is_group_master = graph.graph.is_group_master;
227 }
228 }
229
230 let new_group_derived_data = if self.group_derived_data.is_empty() {
231 TiVec::new()
232 } else {
233 plan.retained_group_ids()
234 .iter()
235 .map(|&old_group_id| self.group_derived_data[old_group_id].clone())
236 .collect::<TiVec<GroupId, _>>()
237 };
238
239 self.graphs = new_graphs;
240 self.graph_group_structure = new_graph_group_structure;
241 self.group_derived_data = new_group_derived_data;
242 self.integrand = None;
243 Ok(())
244 }
245
246 pub fn export_standalone(
247 &self,
248 path: impl AsRef<Path>,
249 settings: &StandaloneExportSettings,
250 ) -> Result<()> {
251 if let Some(integrand) = &self.integrand {
252 integrand.export_standalone(path, settings)?
253 } else {
254 return Err(eyre!(
255 "Cannot warm up amplitude {} without integrand",
256 self.name
257 ));
258 }
259
260 Ok(())
261 }
262
263 #[instrument(
264 skip_all,
265 fields(
266 amplitude.name = %self.name,
267 )
268 )]
269 pub(crate) fn warm_up(&mut self, model: &Model) -> Result<()> {
270 if let Some(integrand) = &mut self.integrand {
271 integrand.warm_up(model)
272 } else {
273 Err(eyre!(
274 "Cannot warm up amplitude {} without integrand",
275 self.name
276 ))
277 }
278 }
279
280 #[instrument(
281 skip_all,
282 fields(
283 path = %path.as_ref().display(),
284 )
285 )]
286 pub(crate) fn load(path: impl AsRef<Path>, context: GammaLoopContextContainer) -> Result<Self> {
287 let binary = fs::read(path.as_ref().join("amp.bin"))?;
288 let (mut amp, _): (Self, _) =
289 bincode::decode_from_slice_with_context(&binary, bincode::config::standard(), context)?;
290
291 if path.as_ref().join("integrand").exists() {
292 let integrand = AmplitudeIntegrand::load(path.as_ref().join("integrand"), context)?;
293 amp.integrand = Some(ProcessIntegrand::Amplitude(integrand));
294 }
295
296 Ok(amp)
297 }
298
299 #[instrument(
300 skip_all,
301 fields(
302 amplitude.name = %self.name,
303 )
304 )]
305 pub fn compile(
306 &mut self,
307 path: impl AsRef<Path>,
308 override_existing: bool,
309 thread_pool: &ThreadPool,
310 ) -> Result<Vec<NamedGraphGenerationReport>> {
311 info!("Compiling amplitude {}", self.name);
312 let p = path.as_ref().join(&self.name);
313
314 let r = fs::create_dir_all(&p).with_context(|| {
315 format!(
316 "Trying to create directory to save amplitude {}",
317 p.display()
318 )
319 });
320 if override_existing {
321 r?;
322 }
323 if let Some(integrand) = &mut self.integrand {
324 let compile_times = integrand.compile(&p, override_existing, thread_pool)?;
325 return Ok(compile_times
326 .into_iter()
327 .map(|(graph_name, duration)| NamedGraphGenerationReport {
328 integrand_name: self.name.clone(),
329 graph_name,
330 stats: GraphGenerationStats {
331 total_time: duration,
332 evaluator_compile_time: duration,
333 ..GraphGenerationStats::default()
334 },
335 })
336 .collect());
337 };
338 Ok(Vec::new())
339 }
340
341 #[instrument(
342 skip_all,
343 fields(
344 amplitude.name = %self.name,
345 path = %path.as_ref().display(),
346 )
347 )]
348 pub fn save(&mut self, path: impl AsRef<Path>, override_existing: bool) -> Result<()> {
349 let p = path.as_ref().join(&self.name);
350
351 let r = fs::create_dir_all(&p).with_context(|| {
352 format!(
353 "Trying to create directory to save amplitude {}",
354 p.display()
355 )
356 });
357 if override_existing {
358 r?;
359 }
360
361 let integrand = self.integrand.take();
362 if let Some(integrand) = &integrand {
363 integrand.save(&p, override_existing)?;
364 };
365
366 let binary = bincode::encode_to_vec(&(*self), bincode::config::standard())?;
367 if override_existing {
368 fs::write(p.join("amp.bin"), binary)?;
369 } else {
370 let mut file = File::create_new(p.join("amp.bin"))?;
371 file.write_all(&binary)?;
372 }
373
374 self.integrand = integrand;
375 Ok(())
376 }
377
378 #[instrument(
379 skip_all,
380 fields(
381 amplitude.name = %self.name,
382 )
383 )]
384 pub fn preprocess(
385 &mut self,
386 model: &Model,
387 settings: &GenerationSettings,
388 locked_runtime_settings: &LockedRuntimeSettings,
389 thread_pool: &ThreadPool,
390 ) -> Result<Vec<NamedGraphGenerationReport>> {
391 settings.validate_for_process(GenerationType::Amplitude)?;
392 for group in &self.graph_group_structure {
396 if group.into_iter().all(|id| {
397 self.graphs[id]
398 .graph
399 .threshold_counterterms
400 .is_legacy_equivalent()
401 }) {
402 for graph_id in group {
404 self.graphs[graph_id].derived_data.threshold_topology = None;
405 }
406 continue;
407 }
408 let master = &self.graphs[group.master()].graph;
409 let topology = (
410 master.clone(),
411 master.generate_loop_momentum_bases_of(&master.no_dummy()),
412 );
413 for graph_id in group {
414 self.graphs[graph_id]
415 .graph
416 .threshold_counterterms
417 .validate_for_graph(&topology.0)?;
418 self.graphs[graph_id].derived_data.threshold_topology = Some(topology.clone());
419 }
420 let mut explicit = BTreeMap::new();
421 let all_lmbs = &self.graphs[group.master()]
422 .derived_data
423 .threshold_topology
424 .as_ref()
425 .unwrap()
426 .1;
427 for id in group {
428 for (label, subspace, description) in
429 self.graphs[id].explicit_threshold_group_members()?
430 {
431 explicit
432 .entry(label)
433 .or_insert_with(Vec::new)
434 .push((subspace.solve_signature(all_lmbs), description));
435 }
436 }
437 let conflicts = explicit
438 .into_iter()
439 .filter_map(|(label, members)| {
440 (members.iter().map(|(key, _)| key).unique().count() > 1).then(|| {
441 format!(
442 "group_id={label}: {}",
443 members
444 .into_iter()
445 .map(|(key, description)| format!(
446 "{description}, signed cycles {key:?}"
447 ))
448 .join("; ")
449 )
450 })
451 })
452 .collect_vec();
453 if !conflicts.is_empty() {
454 return Err(eyre!(
455 "Amplitude group master '{}' has incompatible threshold solve groups:\n{}",
456 self.graphs[group.master()].graph.name,
457 conflicts.join("\n")
458 ));
459 }
460 }
461 let integrand_name = self.name.clone();
462
463 let preprocess_span = if generation_progress::detailed_progress_enabled() {
464 let span = info_span!("Preprocessing graphs", indicatif.pb_show = true);
465 span.pb_set_style(&ProgressStyle::with_template(
466 "{wide_bar} {pos}/{len} {msg}",
467 )?);
468 span.pb_set_length(self.graphs.len() as u64);
469 span.pb_set_message("Preprocessing graphs");
470 Some(span)
471 } else {
472 None
473 };
474 let preprocess_span_enter = preprocess_span.as_ref().map(|span| span.enter());
475
476 let preprocess_reports = thread_pool.install(|| {
477 let parent = preprocess_span.clone();
478 self.graphs
479 .par_iter_mut()
480 .map(|amplitude_graph| {
481 if crate::is_interrupted() {
482 return Err(eyre!("Generation interrupted by user"));
483 }
484 let graph_name = amplitude_graph.graph.name.clone();
485 generation_progress::graph_started(
486 GenerationProcessKind::Amplitude,
487 &integrand_name,
488 &graph_name,
489 None,
490 );
491 let _guard = parent.as_ref().map(|span| span.enter());
492 let stats =
493 amplitude_graph.preprocess(model, settings, locked_runtime_settings);
494 if let Some(span) = &parent {
495 span.pb_inc(1);
496 }
497
498 let stats = stats?;
499 if crate::is_interrupted() {
500 return Err(eyre!("Generation interrupted by user"));
501 }
502 generation_progress::graph_finished(
503 GenerationProcessKind::Amplitude,
504 &integrand_name,
505 &graph_name,
506 &stats,
507 None,
508 );
509
510 Ok(NamedGraphGenerationReport {
511 integrand_name: integrand_name.clone(),
512 graph_name,
513 stats,
514 })
515 })
516 .collect::<Result<Vec<_>>>()
517 })?;
518
519 drop(preprocess_span_enter);
520 drop(preprocess_span);
521
522 for group in &self.graph_group_structure {
525 if group.into_iter().any(|id| {
526 self.graphs[id]
527 .derived_data
528 .resolved_threshold_counterterms
529 .as_ref()
530 .is_some_and(|resolved| !resolved.legacy_equivalent)
531 }) {
532 for id in group {
533 if let Some(resolved) =
534 &mut self.graphs[id].derived_data.resolved_threshold_counterterms
535 {
536 resolved.legacy_equivalent = false;
537 }
538 }
539 }
540 }
541 self.generate_grouped_derived_data()?;
542
543 Ok(preprocess_reports)
544 }
545
546 #[instrument(
547 skip_all,
548 fields(
549 amplitude.name = %self.name,
550 )
551 )]
552 pub fn build_integrand(
553 &mut self,
554 model: &Model,
555 process_name: &str,
556 global_settings: &GlobalSettings,
557 runtime_default: LockedRuntimeSettings,
558 thread_pool: &ThreadPool,
559 ) -> Result<Vec<NamedGraphGenerationReport>> {
560 let started = std::time::Instant::now();
561 crate::debug_tags!(#generation, #profile, #graph, #summary;
562 stage = "amplitude_build_integrand_start",
563 integrand = %self.name,
564 graph_count = self.graphs.len(),
565 "Generation timing milestone"
566 );
567 if crate::is_interrupted() {
568 return Err(eyre!("Generation interrupted by user"));
569 }
570 let integrand_name = self.name.clone();
571 generation_progress::begin_phase(
572 GenerationProgressPhase::GraphGeneration,
573 GenerationProcessKind::Amplitude,
574 process_name,
575 &integrand_name,
576 self.graphs.len(),
577 None,
578 );
579 let mut graph_reports = Vec::new();
580 let terms: Vec<_> = thread_pool.install(|| {
581 self.graphs
582 .par_iter_mut()
583 .enumerate()
584 .map(|(graph_id, graph)| {
585 if crate::is_interrupted() {
586 return Err(eyre!("Generation interrupted by user"));
587 }
588 let graph_started = std::time::Instant::now();
589 let group_id = graph.graph.group_id.unwrap(); let esurface_map = &self.group_derived_data[group_id].esurface_map;
591 let group_pos = self.graph_group_structure[group_id]
592 .find_position(graph_id)
593 .unwrap();
594
595 crate::debug_tags!(#generation, #profile, #graph, #summary;
596 stage = "amplitude_generate_term_for_graph_start",
597 integrand = %integrand_name,
598 graph = %graph.graph.name,
599 graph_id,
600 group_id = %group_id.0,
601 "Generation timing milestone"
602 );
603 generation_progress::graph_started(
604 GenerationProcessKind::Amplitude,
605 &integrand_name,
606 &graph.graph.name,
607 None,
608 );
609 let _progress_context_guard =
610 generation_progress::enter_progress_context(graph.graph.name.clone());
611 let (term, mut stats) = graph.generate_term_for_graph(
612 model,
613 group_pos,
614 esurface_map.clone(),
615 global_settings,
616 )?;
617 if crate::is_interrupted() {
618 return Err(eyre!("Generation interrupted by user"));
619 }
620 stats.evaluator_count = term.generic_evaluator_count();
621 stats.total_time += graph_started.elapsed();
622 crate::debug_tags!(#generation, #profile, #graph, #summary;
623 stage = "amplitude_generate_term_for_graph_done",
624 integrand = %integrand_name,
625 graph = %graph.graph.name,
626 graph_id,
627 group_id = %group_id.0,
628 elapsed_ms = graph_started.elapsed().as_secs_f64() * 1000.0,
629 "Generation timing milestone"
630 );
631 generation_progress::graph_finished(
632 GenerationProcessKind::Amplitude,
633 &integrand_name,
634 &graph.graph.name,
635 &stats,
636 None,
637 );
638 Ok((
639 term,
640 NamedGraphGenerationReport {
641 integrand_name: integrand_name.clone(),
642 graph_name: graph.graph.name.clone(),
643 stats,
644 },
645 ))
646 })
647 .collect::<Result<Vec<_>>>()
648 })?;
649 if crate::is_interrupted() {
650 return Err(eyre!("Generation interrupted by user"));
651 }
652 for (_, report) in &terms {
653 graph_reports.push(report.clone());
654 }
655 let mut terms = terms.into_iter().map(|(term, _)| term).collect::<Vec<_>>();
656
657 for group in self.graph_group_structure.iter() {
658 let master_graph_id = group.master();
659 let mc_of_master = self.graphs[master_graph_id]
660 .derived_data
661 .multi_channeling_setup
662 .as_ref()
663 .unwrap();
664 let master_graph = &self.graphs[master_graph_id].graph;
665 let master_external_signature = master_graph.get_external_signature();
666 let master_external_pdgs = master_graph
667 .get_external_partcles()
668 .into_iter()
669 .map(|particle| particle.pdg_code)
670 .collect_vec();
671
672 for graph_id in group.into_iter() {
673 terms[graph_id].multi_channeling_setup = mc_of_master.clone();
674 terms[graph_id].master_external_signature = master_external_signature.clone();
675 terms[graph_id].master_external_pdgs = master_external_pdgs.clone();
676 }
677 }
678
679 let backend_started = std::time::Instant::now();
680 let graph_count = terms.len();
681 crate::debug_tags!(#generation, #profile, #compile, #graph, #summary;
682 stage = "amplitude_prepare_runtime_backends_start",
683 integrand = %self.name,
684 graph_count,
685 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
686 "Generation timing milestone"
687 );
688 generation_progress::backend_started(
689 GenerationProcessKind::Amplitude,
690 &self.name,
691 graph_count,
692 );
693 let mut amplitude_integrand = AmplitudeIntegrand {
694 settings: runtime_default.into_with_modified_kinematics(
695 &self.external_signature,
696 &self.graphs[0].graph.get_external_masses(model),
697 )?,
698 data: AmplitudeIntegrandData {
699 name: self.name.clone(),
700 compilation: global_settings
701 .generation
702 .compile
703 .frozen_mode(&global_settings.generation.evaluator),
704 rotations: None,
705 loop_cache_id: 0,
706 external_cache_id: 0,
707 base_external_cache_id: 0,
708 graph_terms: terms,
709 external_signature: self.external_signature.clone(),
710 graph_group_structure: self.graph_group_structure.clone(),
711 graph_to_group_id: graph_to_group_id_for_group_structure(
712 &self.graph_group_structure,
713 ),
714 group_derived_data: self.group_derived_data.clone(),
715 explicit_orientation_sum_only: global_settings
716 .generation
717 .explicit_orientation_sum_only,
718 },
719 event_processing_runtime: Default::default(),
720 active_f64_backend: Default::default(),
721 };
722 let compile_times =
723 amplitude_integrand.prepare_runtime_backends_after_generation_with_compile_times()?;
724 crate::debug_tags!(#generation, #profile, #compile, #graph, #summary;
725 stage = "amplitude_prepare_runtime_backends_done",
726 integrand = %self.name,
727 graph_count,
728 elapsed_ms = backend_started.elapsed().as_secs_f64() * 1000.0,
729 total_elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
730 "Generation timing milestone"
731 );
732 generation_progress::backend_finished(
733 GenerationProcessKind::Amplitude,
734 &self.name,
735 backend_started.elapsed(),
736 );
737 for (report, compile_time) in graph_reports.iter_mut().zip(compile_times) {
738 report.stats.evaluator_compile_time += compile_time;
739 report.stats.total_time += compile_time;
740 }
741 self.integrand = Some(ProcessIntegrand::Amplitude(amplitude_integrand));
742 crate::debug_tags!(#generation, #profile, #graph, #summary;
743 stage = "amplitude_build_integrand_done",
744 integrand = %self.name,
745 graph_count = self.graphs.len(),
746 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
747 "Generation timing milestone"
748 );
749 Ok(graph_reports)
750 }
751
752 #[instrument(
753 skip_all,
754 fields(
755 amplitude.name = %self.name,
756 )
757 )]
758 #[allow(dead_code)]
759 pub(crate) fn write_dot<W: std::io::Write>(
760 &self,
761 writer: &mut W,
762 settings: &DotExportSettings,
763 ) -> Result<(), std::io::Error> {
764 for graph in &self.graphs {
765 graph.write_dot(writer, settings)?;
766 writeln!(writer)?;
767 }
768 Ok(())
769 }
770
771 #[instrument(
772 skip_all,
773 fields(
774 amplitude.name = %self.name,
775 )
776 )]
777 pub fn write_dot_fmt<W: fmt::Write>(
778 &self,
779 writer: &mut W,
780 settings: &DotExportSettings,
781 ) -> Result<(), std::fmt::Error> {
782 for graph in &self.graphs {
783 graph.write_dot_fmt(writer, settings)?;
784 writeln!(writer)?;
785 }
786 Ok(())
787 }
788
789 pub fn generate_grouped_derived_data(&mut self) -> Result<()> {
790 let group_derived_data = self
793 .graph_group_structure
794 .iter()
795 .map(|group| {
796 let mut group_esurface_structure =
797 BTreeMap::<Atom, TiVec<GraphGroupPosition, Option<RaisedEsurfaceId>>>::default(
798 );
799
800 for (graph_group_position, graph_id) in group.iter_enumerated() {
801 let amplitude_graph = &self.graphs[graph_id];
802 let lmb_reps = amplitude_graph.graph.integrand_replacement(
803 &litude_graph.graph.full_filter(),
804 &litude_graph.graph.loop_momentum_basis,
805 &[W_.x___],
806 );
807
808 let esurfaces = &litude_graph.graph.surface_cache.esurface_cache;
809
810 for (raised_esurface_id, raised_group) in amplitude_graph
812 .derived_data
813 .raised_data
814 .raised_groups
815 .iter_enumerated()
816 .filter(|(_, raised_group)| raised_group.max_occurence > 0)
817 {
818 let esurface = &esurfaces[raised_group.esurface_ids[0]];
819 let esurface_atom = esurface.lmb_atom(&litude_graph.graph, &lmb_reps);
820
821 group_esurface_structure
822 .entry(esurface_atom)
823 .or_insert(ti_vec![None; group.len()])[graph_group_position] =
824 Some(raised_esurface_id);
825 }
826 }
827
828 let (surface_atoms, esurface_map) = group_esurface_structure.into_iter().unzip();
829
830 GroupDerivedData {
831 esurface_map,
832 esurface_atoms: surface_atoms,
833 }
834 })
835 .collect::<TiVec<GroupId, _>>();
836
837 let _: () = self.group_derived_data = group_derived_data;
838 Ok(())
839 }
840}
841
842#[derive(Clone, Encode, Decode)]
843#[trait_decode(trait= GammaLoopContext)]
844pub struct AmplitudeGraph {
845 pub graph: Graph,
846 pub derived_data: AmplitudeDerivedData,
847}
848
849#[derive(Clone)]
850struct ResolvedAmplitudeThresholdCountertermDraft {
851 name: String,
852 group_id: Option<usize>,
853 origin: ThresholdCountertermOrigin,
854 disable: bool,
855 requested_subspace: Option<Vec<EdgeIndex>>,
856 requested_parent_lmb: Option<Vec<EdgeIndex>>,
857 multiplier: Option<ThresholdCountertermMultiplier>,
858 subspace: SubspaceData,
859}
860
861impl ResolvedAmplitudeThresholdCountertermDraft {
862 fn is_compatible_with(
863 &self,
864 other: &Self,
865 all_lmbs: &TiVec<LmbIndex, LoopMomentumBasis>,
866 ) -> bool {
867 self.name == other.name
868 && self.group_id == other.group_id
869 && self.disable == other.disable
870 && self.multiplier == other.multiplier
871 && self
872 .subspace
873 .has_equivalent_embedding(&other.subspace, all_lmbs)
874 }
875}
876
877struct ResolvedAmplitudeThresholdAssociationDraft {
878 esurface_id: EsurfaceID,
879 threshold_edges: Vec<EdgeIndex>,
880 origin: ThresholdCountertermOrigin,
881 variant_subspaces: Vec<SubspaceData>,
882}
883
884struct ResolvedAmplitudeThresholdGroupDraft {
885 raised_esurface_group: RaisedEsurfaceGroup,
886 associations: Vec<ResolvedAmplitudeThresholdAssociationDraft>,
887 variants: Vec<ResolvedAmplitudeThresholdCountertermDraft>,
888}
889
890struct AmplitudeThresholdCountertermBuild {
891 legacy_counterterms: TiVec<RaisedEsurfaceId, AmplitudeCountertermAtom>,
892 variants: TiVec<ThresholdCountertermVariantId, AmplitudeThresholdCountertermVariant>,
893 resolved: ResolvedThresholdCounterterms,
894 raised_esurface_ids: TiVec<EsurfaceID, RaisedEsurfaceId>,
895}
896
897pub struct AnalyticalEvaluationConfig<'a> {
898 pub model: &'a Model,
899 pub evaluate_numerically: bool,
900 pub vakint: &'a Vakint,
901 pub true_settings: &'a vakint::VakintSettings,
902 pub settings: &'a VakintSettings,
903 pub run_time_settings: &'a RuntimeSettings,
904 pub include_global_numerator: bool,
905}
906
907impl AmplitudeGraph {
908 pub(crate) fn new(graph: Graph) -> Self {
909 AmplitudeGraph {
910 graph,
911 derived_data: AmplitudeDerivedData {
912 threshold_topology: None,
913 all_mighty_integrand: Atom::Zero,
914 all_mighty_numerators: Vec::new(),
915 cff_expression: None,
916
917 lmbs: None,
918 tropical_sampler: None,
919 multi_channeling_setup: None,
920 threshold_counterterms: TiVec::new(),
921 threshold_counterterm_variants: TiVec::new(),
922 resolved_threshold_counterterms: None,
923 raised_data: RaisedEsurfaceData {
924 raised_groups: TiVec::new(),
925 pass_two_evaluator: None,
926 },
927 raised_esurface_ids: TiVec::new(),
928 },
929 }
930 }
931}
932
933impl AmplitudeGraph {
934 pub fn renormalization_part(
935 &mut self,
936 settings: &UVgenerationSettings,
937 ) -> Result<RenormalizationPart> {
938 if self.derived_data.cff_expression.is_none() {
939 self.graph.ensure_energy_convergent_cycles(
942 &self
943 .graph
944 .no_dummy()
945 .subtract(&self.graph.initial_state_cut),
946 )?;
947 }
948 settings.orchestrator.renormalization_part(
949 &mut self.graph,
950 OrientationProjection::four_d(&OrientationPattern::default()),
953 settings,
954 )
955 }
956
957 #[allow(dead_code)]
958 pub(crate) fn write_dot<W: std::io::Write>(
959 &self,
960 writer: &mut W,
961 settings: &DotExportSettings,
962 ) -> Result<(), std::io::Error> {
963 if let Some(graph) = self.graph_with_materialized_threshold_counterterms(settings) {
964 graph.dot_serialize_io(writer, settings)
965 } else {
966 self.graph.dot_serialize_io(writer, settings)
967 }
968 }
969
970 pub(crate) fn write_dot_fmt<W: fmt::Write>(
971 &self,
972 writer: &mut W,
973 settings: &DotExportSettings,
974 ) -> Result<(), std::fmt::Error> {
975 if let Some(graph) = self.graph_with_materialized_threshold_counterterms(settings) {
976 graph.dot_serialize_fmt(writer, settings)
977 } else {
978 self.graph.dot_serialize_fmt(writer, settings)
979 }
980 }
981
982 fn graph_with_materialized_threshold_counterterms(
983 &self,
984 settings: &DotExportSettings,
985 ) -> Option<Graph> {
986 if !settings.include_autogenerated_fields
989 || (self.graph.group_id.is_some() && !self.graph.is_group_master)
990 {
991 return None;
992 }
993 let resolved = self.derived_data.resolved_threshold_counterterms.as_ref()?;
994 let all_lmbs = self
995 .derived_data
996 .threshold_topology
997 .as_ref()
998 .map(|(_, lmbs)| lmbs)
999 .or(self.derived_data.lmbs.as_ref())?;
1000 let mut graph = self.graph.clone();
1001 graph.threshold_counterterms = crate::graph::autogen::Autogen::explicit(
1002 resolved.materialized_spec(&self.graph.threshold_counterterms, all_lmbs),
1003 );
1004 Some(graph)
1005 }
1006
1007 #[instrument(skip_all, err)]
1008 pub(crate) fn generate_cff(&mut self, settings: &GenerationSettings) -> Result<()> {
1009 self.graph.set_medium_mode(settings.medium.mode)?;
1010 self.graph.ensure_energy_convergent_cycles(
1011 &self
1012 .graph
1013 .no_dummy()
1014 .subtract(&self.graph.initial_state_cut),
1015 )?;
1016 let _progress_guard = generation_progress::enter_detailed_progress_span("Generating CFF");
1017 let shift_rewrite = self
1018 .graph
1019 .get_esurface_canonization(&self.graph.loop_momentum_basis);
1020
1021 let contract_edges = self
1022 .graph
1023 .iter_edges_of(
1024 &self
1025 .graph
1026 .tree_edges
1027 .subtract(&self.graph.initial_state_cut)
1028 .subtract(&self.graph.external_filter::<SuBitGraph>()),
1029 )
1030 .map(|x| x.1)
1031 .collect_vec();
1032
1033 let options = self.graph.production_cff_3d_expression_options(settings)?;
1037 let generated = self.graph.generate_3d_expression_for_integrand(
1038 &contract_edges,
1039 &shift_rewrite,
1040 &options,
1041 None,
1042 )?;
1043
1044 self.derived_data.cff_expression = Some(generated);
1045
1046 Ok(())
1047 }
1048
1049 #[instrument(skip_all, err)]
1050 pub(crate) fn preprocess(
1051 &mut self,
1052 model: &Model,
1053 settings: &GenerationSettings,
1054 locked_runtime_settings: &LockedRuntimeSettings,
1055 ) -> Result<GraphGenerationStats> {
1056 let _progress_guard = generation_progress::enter_detailed_progress_span("preprocessing");
1057 let preprocess_started = std::time::Instant::now();
1058 let vk = crate::utils::vakint()?;
1059
1060 self.generate_cff(settings)?;
1061
1062 let raised_data = settings.threshold_subtraction.enable_thresholds.then(|| {
1065 self.graph.determine_raised_esurfaces_from_expression(
1066 &self
1067 .derived_data
1068 .cff_expression
1069 .as_ref()
1070 .expect("cff_expression should have been created")
1071 .expression,
1072 )
1073 });
1074
1075 self.build_integrands(settings, vk)?;
1076
1077 if self.graph.is_group_master {
1078 self.build_tropical_sampler(settings)?;
1079 }
1080
1081 self.build_lmbs();
1082
1083 if self.graph.is_group_master {
1084 self.build_multi_channeling_channels(settings.override_lmb_heuristics);
1085 }
1086
1087 if let Some(mut raised_data) = raised_data {
1088 let max_order = raised_data
1089 .raised_groups
1090 .iter()
1091 .map(|raised_group| raised_group.max_occurence)
1092 .max()
1093 .unwrap_or(0);
1094 if max_order > 1 {
1095 self.graph.param_builder.initialize_duals(max_order);
1096 }
1097 raised_data.pass_two_evaluator = Some(
1098 (1..=max_order)
1099 .map(|order| {
1100 threshold_counterterm_helper(
1101 order as u8,
1102 self.graph.get_loop_number(),
1103 &settings.evaluator,
1104 )
1105 })
1106 .collect(),
1107 );
1108
1109 let build = self.build_threshold_counterterm_parametric_integrand(
1110 &raised_data,
1111 settings,
1112 vk,
1113 locked_runtime_settings,
1114 model,
1115 )?;
1116 self.derived_data.threshold_counterterms = build.legacy_counterterms;
1117 self.derived_data.threshold_counterterm_variants = build.variants;
1118 self.derived_data.resolved_threshold_counterterms = Some(build.resolved);
1119 self.derived_data.raised_esurface_ids = build.raised_esurface_ids;
1120 self.derived_data.raised_data = raised_data;
1121 } else {
1122 self.derived_data.resolved_threshold_counterterms = None;
1123 self.derived_data.threshold_counterterms.clear();
1124 self.derived_data.threshold_counterterm_variants.clear();
1125 self.derived_data.raised_esurface_ids.clear();
1126 self.derived_data.raised_data.raised_groups.clear();
1127 self.derived_data.raised_data.pass_two_evaluator = None;
1128 }
1129
1130 Ok(GraphGenerationStats {
1131 total_time: preprocess_started.elapsed(),
1132 ..GraphGenerationStats::default()
1133 })
1134 }
1135
1136 #[instrument(skip_all)]
1137 fn build_multi_channeling_channels(&mut self, override_lmb_heuristics: bool) {
1138 let _progress_guard =
1139 generation_progress::enter_detailed_progress_span("Building Multi-Channeling Channels");
1140 let channels = self.graph.build_multi_channeling_channels(
1141 self.derived_data.lmbs.as_ref().unwrap(),
1142 override_lmb_heuristics,
1143 );
1144
1145 self.derived_data.multi_channeling_setup = Some(channels)
1146 }
1147
1148 pub fn to_numerical(
1201 numerical_result: AtomView,
1202 true_settings: &vakint::VakintSettings,
1203 ) -> Result<NumericalEvaluationResult> {
1204 Ok(NumericalEvaluationResult::from_atom(
1205 numerical_result,
1206 vakint_symbol!(&true_settings.epsilon_symbol),
1207 true_settings,
1208 )?)
1209 }
1210
1211 pub fn analytical_evaluation<S: SubGraphLike<Base = SuBitGraph> + SubSetOps>(
1212 &self,
1213 component: &S,
1214 config: AnalyticalEvaluationConfig<'_>,
1215 ) -> Result<Atom> {
1216 if self.graph.param_builder.medium_mode != MediumMode::Vacuum {
1217 return Err(eyre!(
1218 "Analytical evaluation with Vakint does not support thermal modes (graph '{}', mode {:?}); use inspect or integrate for medium-dependent results",
1219 self.graph.name,
1220 self.graph.param_builder.medium_mode
1221 ));
1222 }
1223 let mut true_settings = config.true_settings.clone();
1224 let pysec_dec_enabled_in_vakint = true_settings.evaluation_order.0.iter().find_map(|o| {
1225 if let EvaluationMethod::PySecDec(opts) = o {
1226 Some(opts)
1227 } else {
1228 None
1229 }
1230 });
1231
1232 let complex_params_vakint =
1233 if config.evaluate_numerically || pysec_dec_enabled_in_vakint.is_some() {
1234 let mut param_builder = self.graph.param_builder.clone(); param_builder.update_model_values(config.model)?;
1236 param_builder.m_uv_value(Complex::new_re(F(config.run_time_settings.general.m_uv)));
1237 param_builder.renormalization_localization_scale_value(Complex::new_re(F(config
1238 .run_time_settings
1239 .general
1240 .renormalization_localization_scale)));
1241 param_builder.mu_r_sq_value(Complex::new_re(F(config
1242 .run_time_settings
1243 .general
1244 .mu_r_sq())));
1245 param_builder.inverse_temperature_value(Complex::new_re(F(config
1246 .run_time_settings
1247 .general
1248 .inverse_temperature)));
1249
1250 let mut complex_params: HashMap<String, symbolica::domains::float::Complex<f64>> =
1253 HashMap::default();
1254 for params in param_builder.pairs.into_iter() {
1255 let ps: crate::integrands::process::ParamValuePairs = params;
1256 for (p_name, p_value) in
1257 ps.params
1258 .iter()
1259 .zip(ps.value_range)
1260 .map(|(a, value_index)| {
1261 (
1262 a.to_canonical_string(),
1263 param_builder.values[0][value_index],
1264 )
1265 })
1266 {
1267 complex_params.insert(
1268 p_name,
1269 symbolica::domains::float::Complex::new(
1270 p_value.re.into(),
1271 p_value.im.into(),
1272 ),
1273 );
1274 }
1275 }
1276 for atom in &[
1278 Atom::Var(Var::new(Symbol::PI)),
1279 Atom::Var(Var::new(vakint_symbol!("EulerGamma"))),
1280 function!(Symbol::LOG, Atom::num(2)),
1281 ] {
1282 _ = complex_params.remove(&atom.to_string());
1283 }
1284
1285 config
1287 .vakint
1288 .params_from_complex_f64(&true_settings, &complex_params)
1289 } else {
1290 HashMap::default()
1291 };
1292
1293 if let Some(pysec_dec_opts) = pysec_dec_enabled_in_vakint {
1294 true_settings.evaluation_order.adjust(
1295 None,
1296 pysec_dec_opts.relative_precision,
1297 &HashMap::default(),
1298 &complex_params_vakint,
1299 &HashMap::default(),
1300 )?;
1301 }
1302
1303 let mut num = self
1304 .graph
1305 .numerator(component, &self.graph.empty_subgraph());
1306 if config.include_global_numerator {
1307 num.state.expr *= &self.graph.global_prefactor.num;
1308 }
1309
1310 let before_gamma = num.to_d_dim(GS.dim).get_single_atom().unwrap();
1311 let before_gamma_plain = before_gamma.to_plain_string();
1312 let four_dimensional_numerator = before_gamma.simplify_gamma();
1313 let after_gamma_plain = four_dimensional_numerator.to_plain_string();
1314 crate::debug_tags!(#uv, #integrated, #vakint, #profile, #trace;
1315 stage = "amplitude_to_vakint_after_simplify_gamma",
1316 changed = before_gamma_plain != after_gamma_plain,
1317 before_gamma_count = %before_gamma_plain.matches("spenso::gamma").count(),
1318 after_gamma_count = %after_gamma_plain.matches("spenso::gamma").count(),
1319 before_chain_count = %before_gamma_plain.matches("spenso::chain").count(),
1320 after_chain_count = %after_gamma_plain.matches("spenso::chain").count(),
1321 log.before_gamma = before_gamma,
1322 log.after_gamma = four_dimensional_numerator,
1323 "Gamma simplification before Vakint"
1324 );
1325
1326 let mut four_dimensional_integrand = four_dimensional_numerator
1327 / self
1328 .graph
1329 .denominator(component, |e| e.extra_data.vakint_edge_power.unwrap_or(1));
1330
1331 let component_lmb = self.graph.lmb_of(component);
1334 let mom_reps = self.graph.uv_wrapped_replacement(
1335 &self.graph.full_filter(),
1336 &component_lmb,
1337 &[W_.x___],
1338 );
1339
1340 four_dimensional_integrand = four_dimensional_integrand.replace_multiple(&mom_reps);
1347
1348 let mut vakint_integrand = to_vakint_integrand(
1363 &four_dimensional_integrand,
1364 &self.graph,
1365 &self.graph.full_filter(),
1366 &self.graph.empty_subgraph::<SuBitGraph>(),
1367 &[(
1368 component.included().clone(),
1369 component.included().clone(),
1370 component_lmb,
1371 )],
1372 config.settings,
1373 false,
1374 )?;
1375
1376 vakint_integrand.canonicalize(&true_settings, &config.vakint.topologies, false)?;
1377 vakint_integrand.tensor_reduce(config.vakint, &true_settings)?;
1379 vakint_integrand.evaluate_integral(config.vakint, &true_settings)?;
1381 let analytical_evaluation: Atom = vakint_integrand.into();
1383 if !config.evaluate_numerically {
1388 Ok(analytical_evaluation)
1389 } else {
1390 let (numerical_evaluation, _error) = config
1391 .vakint
1392 .numerical_evaluation(
1393 &true_settings,
1394 analytical_evaluation.as_view(),
1395 &HashMap::default(),
1396 &complex_params_vakint,
1397 None,
1398 )
1399 .unwrap();
1400
1401 let numerical_evaluation_atom =
1404 numerical_evaluation.to_atom(vakint_symbol!(true_settings.epsilon_symbol.clone()));
1405
1406 Ok(numerical_evaluation_atom)
1407 }
1408 }
1409
1410 #[instrument(skip_all, err)]
1411 pub(crate) fn build_integrands(
1412 &mut self,
1413 settings: &GenerationSettings,
1414 vakint: &Vakint,
1415 ) -> Result<()> {
1416 let _progress_guard =
1417 generation_progress::enter_detailed_progress_span("Building Parametric Integrand");
1418 let started = std::time::Instant::now();
1419 crate::debug_tags!(#generation, #profile, #uv, #graph, #summary;
1420 stage = "amplitude_graph_build_integrands_start",
1421 graph = %self.graph.name,
1422 subtract_uv = settings.uv.subtract_uv,
1423 generate_integrated = settings.uv.generate_integrated,
1424 only = %settings.uv.final_integrand,
1425 "Generation timing milestone"
1426 );
1427 let cff_options = self.graph.production_cff_3d_expression_options(settings)?;
1428 let production_expression = self
1429 .derived_data
1430 .cff_expression
1431 .as_ref()
1432 .expect("cff_expression should have been created");
1433 let production_orientations = &production_expression.expression.orientations;
1434 crate::debug_tags!(#generation, #profile, #graph, #orientation, #summary;
1435 stage = "amplitude_graph_valid_orientations_done",
1436 graph = %self.graph.name,
1437 orientation_count = production_orientations.len(),
1438 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1439 "Generation timing milestone"
1440 );
1441 let cutstructure = CutStructure::empty(&self.graph);
1442 let orchestration_started = std::time::Instant::now();
1443 let parametric_exprs = settings.uv.orchestrator.parametric_integrands(
1444 &mut self.graph,
1445 cutstructure,
1446 vakint,
1447 OrientationProjection::exact_expression(
1448 production_expression,
1449 &cff_options,
1450 &settings.orientation_pattern,
1451 settings.explicit_orientation_sum_only,
1452 )
1453 .with_vacuum_subtraction(settings.medium.vacuum_subtraction),
1454 settings,
1455 )?;
1456 crate::debug_tags!(#generation, #profile, #uv, #graph, #summary;
1457 stage = "amplitude_graph_parametric_orchestration_done",
1458 graph = %self.graph.name,
1459 parametric_integrand_count = parametric_exprs.len(),
1460 elapsed_ms = orchestration_started.elapsed().as_secs_f64() * 1000.0,
1461 total_elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1462 "Generation timing milestone"
1463 );
1464
1465 let assign_started = std::time::Instant::now();
1466 let [expr] = parametric_exprs.as_slice() else {
1468 return Err(eyre!(
1469 "amplitude UV construction must produce exactly one cut integrand, got {}",
1470 parametric_exprs.len(),
1471 ));
1472 };
1473 let mut roots = expr.integrands.iter();
1474 let (index, integrand) = roots
1475 .next()
1476 .ok_or_else(|| eyre!("amplitude UV integrand has no root residue"))?;
1477 if *index != CutCFFIndex::new_all_none() || roots.next().is_some() {
1478 return Err(eyre!(
1479 "amplitude UV integrand must contain exactly one root residue"
1480 ));
1481 }
1482 self.derived_data.all_mighty_integrand = integrand.clone();
1483 self.derived_data.all_mighty_numerators = expr.integrands.numerators().to_vec();
1484 crate::debug_tags!(#generation, #profile, #graph, #summary;
1485 stage = "amplitude_graph_build_integrands_done",
1486 graph = %self.graph.name,
1487 assign_elapsed_ms = assign_started.elapsed().as_secs_f64() * 1000.0,
1488 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1489 "Generation timing milestone"
1490 );
1491
1492 Ok(())
1493 }
1494
1495 fn configured_amplitude_threshold_variants(
1496 spec: &ThresholdCountertermSpec,
1497 threshold_edges: &[EdgeIndex],
1498 ) -> Vec<(ThresholdCountertermVariant, ThresholdCountertermOrigin)> {
1499 let configured = spec
1500 .cuts
1501 .iter()
1502 .find(|cut| cut.edges.is_empty())
1503 .and_then(|cut| {
1504 cut.thresholds
1505 .iter()
1506 .find(|threshold| threshold.edges == threshold_edges)
1507 })
1508 .filter(|threshold| !threshold.counterterms.is_empty());
1509
1510 match configured {
1511 Some(threshold) => threshold
1512 .counterterms
1513 .iter()
1514 .cloned()
1515 .map(|mut variant| {
1516 if let Some(multiplier) = &mut variant.multiplier {
1517 for (name, definition) in &spec.function_map {
1518 multiplier
1519 .function_map
1520 .entry(name.clone())
1521 .or_insert_with(|| definition.clone());
1522 }
1523 }
1524 (variant, ThresholdCountertermOrigin::Explicit)
1525 })
1526 .collect(),
1527 None => vec![(
1528 ThresholdCountertermVariant {
1529 name: Some("default".to_string()),
1530 subspace: None,
1531 parent_lmb: None,
1532 disable: false,
1533 multiplier: None,
1534 group_id: None,
1535 },
1536 ThresholdCountertermOrigin::Autogenerated,
1537 )],
1538 }
1539 }
1540
1541 fn preferred_amplitude_parent_lmb(&self) -> Result<LmbIndex> {
1542 let (topology, all_lmbs) = self.derived_data.threshold_topology.as_ref().map_or_else(
1543 || {
1544 (
1545 &self.graph,
1546 self.derived_data
1547 .lmbs
1548 .as_ref()
1549 .expect("amplitude threshold resolution requires loop-momentum bases"),
1550 )
1551 },
1552 |(graph, lmbs)| (graph, lmbs),
1553 );
1554 if let Some((lmb_index, _)) = all_lmbs
1555 .iter_enumerated()
1556 .find(|(_, lmb)| lmb.loop_edges == topology.loop_momentum_basis.loop_edges)
1557 {
1558 return Ok(lmb_index);
1559 }
1560
1561 all_lmbs
1562 .iter_enumerated()
1563 .next()
1564 .map(|(lmb_index, _)| lmb_index)
1565 .ok_or_else(|| eyre!("Graph '{}' has no generated LMBs", self.graph.name))
1566 }
1567
1568 fn resolve_amplitude_threshold_variant_subspace(
1569 &self,
1570 variant: &ThresholdCountertermVariant,
1571 legacy_subspace: &SubspaceData,
1572 context: &str,
1573 ) -> Result<SubspaceData> {
1574 let (topology, all_lmbs) = self.derived_data.threshold_topology.as_ref().map_or_else(
1575 || {
1576 (
1577 &self.graph,
1578 self.derived_data
1579 .lmbs
1580 .as_ref()
1581 .expect("amplitude threshold resolution requires loop-momentum bases"),
1582 )
1583 },
1584 |(graph, lmbs)| (graph, lmbs),
1585 );
1586 let containing_subgraph = topology.no_dummy();
1587 let build_in_parent = |parent_lmb_index| match &variant.subspace {
1588 Some(requested_basis_edges) => SubspaceData::new_from_parent_basis_edges(
1589 requested_basis_edges,
1590 &containing_subgraph,
1591 parent_lmb_index,
1592 topology,
1593 all_lmbs,
1594 ),
1595 None => SubspaceData::new_with_user_selected_lmb(
1596 containing_subgraph.clone(),
1597 parent_lmb_index,
1598 topology,
1599 all_lmbs,
1600 ),
1601 };
1602
1603 if let Some(requested_parent_lmb) = &variant.parent_lmb {
1604 let matching_lmbs = all_lmbs
1605 .iter_enumerated()
1606 .filter_map(|(lmb_index, lmb)| {
1607 lmb.loop_edges
1608 .iter()
1609 .eq(requested_parent_lmb.iter())
1610 .then_some(lmb_index)
1611 })
1612 .collect_vec();
1613 if matching_lmbs.is_empty() {
1614 return Err(eyre!(
1615 "{context} requests parent_lmb {:?}, which is not among graph '{}' generated LMBs",
1616 requested_parent_lmb,
1617 self.graph.name,
1618 ));
1619 }
1620 let mut built = matching_lmbs
1621 .iter()
1622 .map(|&lmb_index| build_in_parent(lmb_index).map(|subspace| (lmb_index, subspace)))
1623 .collect::<Result<Vec<_>>>()
1624 .with_context(|| {
1625 format!("{context} is incompatible with its requested parent_lmb")
1626 })?;
1627 let (_, selected) = built.remove(0);
1628 if built
1629 .iter()
1630 .all(|(_, candidate)| selected.has_equivalent_embedding(candidate, all_lmbs))
1631 {
1632 return Ok(selected);
1633 }
1634 return Err(eyre!(
1635 "{context} parent_lmb {:?} has genuinely different generated embeddings",
1636 requested_parent_lmb,
1637 ));
1638 }
1639
1640 if variant.subspace.is_none() {
1641 return Ok(legacy_subspace.clone());
1642 }
1643
1644 let preferred_parent = legacy_subspace.parent_lmb_index();
1645 if let Ok(subspace) = build_in_parent(preferred_parent) {
1646 return Ok(subspace);
1647 }
1648
1649 let mut compatible = Vec::new();
1650 let mut rejections = Vec::new();
1651 for (lmb_index, lmb) in all_lmbs.iter_enumerated() {
1652 if lmb_index == preferred_parent {
1653 continue;
1654 }
1655 match build_in_parent(lmb_index) {
1656 Ok(subspace) => compatible.push((lmb_index, subspace)),
1657 Err(error) => rejections.push(format!("parent {:?}: {error:#}", lmb.loop_edges,)),
1658 }
1659 }
1660
1661 match compatible.len() {
1662 1 => Ok(compatible.pop().unwrap().1),
1663 0 => Err(eyre!(
1664 "{context} cannot resolve subspace {:?} in any generated parent LMB. Rejections:\n{}",
1665 variant.subspace,
1666 rejections.join("\n"),
1667 )),
1668 _ => {
1669 let (_, selected) = compatible.remove(0);
1670 if compatible
1671 .iter()
1672 .all(|(_, candidate)| selected.has_equivalent_embedding(candidate, all_lmbs))
1673 {
1674 Ok(selected)
1675 } else {
1676 Err(eyre!(
1677 "{context} subspace {:?} has multiple genuinely different non-preferred parent LMB embeddings {:?}; specify parent_lmb",
1678 variant.subspace,
1679 compatible
1680 .iter()
1681 .map(|(lmb_index, _)| &all_lmbs[*lmb_index].loop_edges)
1682 .collect_vec(),
1683 ))
1684 }
1685 }
1686 }
1687 }
1688
1689 fn explicit_threshold_group_members(&self) -> Result<Vec<(usize, SubspaceData, String)>> {
1690 let (topology, all_lmbs) = self.derived_data.threshold_topology.as_ref().map_or_else(
1691 || {
1692 (
1693 &self.graph,
1694 self.derived_data
1695 .lmbs
1696 .as_ref()
1697 .expect("threshold validation requires LMBs"),
1698 )
1699 },
1700 |(graph, lmbs)| (graph, lmbs),
1701 );
1702 let default = SubspaceData::new_with_user_selected_lmb(
1703 topology.no_dummy(),
1704 self.preferred_amplitude_parent_lmb()?,
1705 topology,
1706 all_lmbs,
1707 )?;
1708 self.graph
1709 .threshold_counterterms
1710 .cuts
1711 .iter()
1712 .flat_map(|cut| &cut.thresholds)
1713 .flat_map(|threshold| {
1714 threshold.counterterms.iter().filter_map(move |variant| {
1715 variant.group_id.map(|label| (threshold, variant, label))
1716 })
1717 })
1718 .map(|(threshold, variant, label)| {
1719 let description = format!(
1720 "graph '{}' threshold {:?} variant '{}' parent {:?} subspace {:?}",
1721 self.graph.name,
1722 threshold.edges,
1723 variant.name.as_deref().unwrap_or("default"),
1724 variant.parent_lmb,
1725 variant.subspace
1726 );
1727 self.resolve_amplitude_threshold_variant_subspace(variant, &default, &description)
1728 .map(|subspace| (label, subspace, description))
1729 })
1730 .collect()
1731 }
1732
1733 fn resolve_amplitude_threshold_counterterm_directives(
1734 &self,
1735 raised_data: &RaisedEsurfaceData,
1736 settings: &GenerationSettings,
1737 locked_runtime_settings: &LockedRuntimeSettings,
1738 model: &Model,
1739 ) -> Result<(
1740 ResolvedThresholdCounterterms,
1741 TiVec<EsurfaceID, RaisedEsurfaceId>,
1742 )> {
1743 let global_cff = self
1744 .derived_data
1745 .cff_expression
1746 .as_ref()
1747 .expect("cff_expression should have been created");
1748 let (topology, all_lmbs) = self.derived_data.threshold_topology.as_ref().map_or_else(
1749 || {
1750 (
1751 &self.graph,
1752 self.derived_data
1753 .lmbs
1754 .as_ref()
1755 .expect("amplitude threshold resolution requires loop-momentum bases"),
1756 )
1757 },
1758 |(graph, lmbs)| (graph, lmbs),
1759 );
1760 let preferred_parent = self.preferred_amplitude_parent_lmb()?;
1761 let legacy_subspace = SubspaceData::new_with_user_selected_lmb(
1762 topology.no_dummy(),
1763 preferred_parent,
1764 topology,
1765 all_lmbs,
1766 )
1767 .with_context(|| {
1768 format!(
1769 "Graph '{}' cannot construct its legacy maximal amplitude threshold subspace",
1770 self.graph.name,
1771 )
1772 })?;
1773
1774 let mut explicit = BTreeMap::new();
1775 for (label, subspace, description) in self.explicit_threshold_group_members()? {
1776 explicit
1777 .entry(label)
1778 .or_insert_with(Vec::new)
1779 .push((subspace.solve_signature(all_lmbs), description));
1780 }
1781 let conflicts = explicit
1782 .into_iter()
1783 .filter_map(|(label, members)| {
1784 (members.iter().map(|(key, _)| key).unique().count() > 1).then(|| {
1785 format!(
1786 "group_id={label}: {}",
1787 members
1788 .into_iter()
1789 .map(|(key, description)| format!(
1790 "{description}, signed cycles {key:?}"
1791 ))
1792 .join("; ")
1793 )
1794 })
1795 })
1796 .collect_vec();
1797 if !conflicts.is_empty() {
1798 return Err(eyre!(
1799 "Amplitude graph '{}' has incompatible threshold solve groups:\n{}",
1800 self.graph.name,
1801 conflicts.join("\n")
1802 ));
1803 }
1804
1805 let mut raised_esurface_ids: TiVec<EsurfaceID, Option<RaisedEsurfaceId>> =
1806 ti_vec![None; global_cff.expression.surfaces.esurface_cache.len()];
1807 for (raised_esurface_id, raised_group) in raised_data.raised_groups.iter_enumerated() {
1808 for &esurface_id in &raised_group.esurface_ids {
1809 raised_esurface_ids[esurface_id] = Some(raised_esurface_id);
1810 }
1811 }
1812 let raised_esurface_ids = raised_esurface_ids
1813 .into_iter()
1814 .enumerate()
1815 .map(|(esurface_id, raised_esurface_id)| {
1816 raised_esurface_id.ok_or_else(|| {
1817 eyre!(
1818 "Graph '{}' E-surface {esurface_id} is missing from raised-esurface data",
1819 self.graph.name,
1820 )
1821 })
1822 })
1823 .collect::<Result<TiVec<EsurfaceID, RaisedEsurfaceId>>>()?;
1824
1825 let external_filter: SuBitGraph = self.graph.external_filter();
1826 let mut incoming_externals = Vec::new();
1827 let mut outgoing_externals = Vec::new();
1828 for (edge, edge_id, _) in self.graph.iter_edges_of(&external_filter) {
1829 match edge {
1830 HedgePair::Unpaired {
1831 flow: Flow::Sink, ..
1832 } => incoming_externals.push(edge_id),
1833 HedgePair::Unpaired {
1834 flow: Flow::Source, ..
1835 } => outgoing_externals.push(edge_id),
1836 _ => unreachable!("the external filter must contain only unpaired edges"),
1837 }
1838 }
1839 let masses = settings
1840 .threshold_subtraction
1841 .check_esurface_at_generation
1842 .then(|| self.graph.get_real_mass_vector(model));
1843 let selected_esurface_ids = global_cff
1846 .expression
1847 .orientations
1848 .iter()
1849 .filter(|orientation| settings.orientation_pattern.filter(*orientation))
1850 .flat_map(|orientation| {
1851 orientation
1852 .iter_denominator_nodes()
1853 .filter_map(|node| match node.data {
1854 crate::cff::surface::HybridSurfaceID::Esurface(esurface_id) => {
1855 Some(esurface_id)
1856 }
1857 _ => None,
1858 })
1859 })
1860 .collect::<AHashSet<_>>();
1861
1862 let configured_thresholds = self
1863 .graph
1864 .threshold_counterterms
1865 .cuts
1866 .iter()
1867 .flat_map(|cut| cut.thresholds.iter().map(|threshold| &threshold.edges))
1868 .collect_vec();
1869 if !configured_thresholds.is_empty() {
1870 let contracted_edges = topology
1875 .iter_edges_of(
1876 &topology
1877 .tree_edges
1878 .subtract(&topology.initial_state_cut)
1879 .subtract(&topology.external_filter::<SuBitGraph>()),
1880 )
1881 .map(|(_, edge_id, _)| edge_id)
1882 .collect::<AHashSet<_>>();
1883 let no_dummy = topology.no_dummy();
1884 let external_count = topology
1885 .iter_edges_of(&no_dummy)
1886 .filter(|(pair, _, _)| matches!(pair, HedgePair::Unpaired { .. }))
1887 .count();
1888 let topology_thresholds = topology
1889 .underlying
1890 .all_bonds_of(&no_dummy, &(1..))
1891 .into_iter()
1892 .filter_map(|bond| {
1893 let mut edges = Vec::new();
1894 let mut bond_external_count = 0;
1895 for (pair, edge_id, _) in topology.iter_edges_of(&bond) {
1896 match pair {
1897 HedgePair::Split { .. } => edges.push(edge_id),
1898 HedgePair::Unpaired { .. } => bond_external_count += 1,
1899 HedgePair::Paired { .. } => {
1900 unreachable!("a topology bond contains only boundary half-edges")
1901 }
1902 }
1903 }
1904 edges.sort_unstable();
1905 (!edges.is_empty()
1906 && !edges.iter().any(|edge| contracted_edges.contains(edge))
1907 && bond_external_count > 0
1908 && bond_external_count < external_count)
1909 .then_some(edges)
1910 })
1911 .collect::<AHashSet<_>>();
1912
1913 for threshold_edges in configured_thresholds {
1914 if !topology_thresholds.contains(threshold_edges) {
1915 return Err(eyre!(
1916 "Amplitude graph '{}' threshold_counterterms threshold {:?} does not match a topology-discovered E-surface",
1917 self.graph.name,
1918 threshold_edges,
1919 ));
1920 }
1921 }
1922 }
1923
1924 let mut group_drafts = Vec::new();
1925 let mut legacy_equivalent = true;
1926 for (raised_esurface_id, raised_group) in raised_data.raised_groups.iter_enumerated() {
1927 let selected_group_esurface_ids = raised_group
1928 .esurface_ids
1929 .iter()
1930 .copied()
1931 .filter(|esurface_id| selected_esurface_ids.contains(esurface_id))
1932 .collect_vec();
1933 let Some(&representative_esurface_id) = selected_group_esurface_ids.first() else {
1934 debug!(
1935 "Leaving amplitude graph '{}' raised threshold group {} dormant because none of its E-surfaces occurs in the selected orientations",
1936 self.graph.name, raised_esurface_id.0,
1937 );
1938 continue;
1939 };
1940 let representative_esurface =
1941 &global_cff.expression.surfaces.esurface_cache[representative_esurface_id];
1942 if representative_esurface.external_shift.is_empty() {
1943 continue;
1944 }
1945 if let Some(masses) = &masses
1946 && !locked_runtime_settings.existence_check(
1947 representative_esurface,
1948 masses,
1949 &self.graph.get_external_signature(),
1950 &self.graph.loop_momentum_basis,
1951 settings.threshold_subtraction.esurface_existence_threshold,
1952 )
1953 {
1954 continue;
1955 }
1956 if settings
1957 .threshold_subtraction
1958 .assume_positive_external_energies
1959 && masses.is_none()
1960 && !representative_esurface
1961 .external_shift_is_strictly_negative_for_positive_energies(
1962 &incoming_externals,
1963 &outgoing_externals,
1964 )
1965 {
1966 continue;
1967 }
1968
1969 let mut associations = Vec::new();
1970 let mut representative_variants =
1971 None::<Vec<ResolvedAmplitudeThresholdCountertermDraft>>;
1972 for &esurface_id in &selected_group_esurface_ids {
1973 let mut threshold_edges = global_cff.expression.surfaces.esurface_cache
1974 [esurface_id]
1975 .energies
1976 .iter()
1977 .copied()
1978 .collect_vec();
1979 threshold_edges.sort_unstable();
1980 let configured = Self::configured_amplitude_threshold_variants(
1981 &self.graph.threshold_counterterms,
1982 &threshold_edges,
1983 );
1984 let mut variants = Vec::with_capacity(configured.len());
1985 for (variant, origin) in configured {
1986 if !variant.disable
1987 && variant
1988 .multiplier
1989 .as_ref()
1990 .is_some_and(|multiplier| multiplier.symmetrize)
1991 {
1992 unimplemented!(
1993 "symmetrized threshold-counterterm multipliers are not implemented (amplitude graph '{}', threshold {:?}, variant '{}')",
1994 self.graph.name,
1995 threshold_edges,
1996 variant.name.as_deref().unwrap_or("default"),
1997 );
1998 }
1999 let name = variant
2000 .name
2001 .clone()
2002 .unwrap_or_else(|| "default".to_string());
2003 let context = format!(
2004 "amplitude graph '{}' threshold {:?} variant '{}'",
2005 self.graph.name, threshold_edges, name,
2006 );
2007 let subspace = self.resolve_amplitude_threshold_variant_subspace(
2008 &variant,
2009 &legacy_subspace,
2010 &context,
2011 )?;
2012 variants.push(ResolvedAmplitudeThresholdCountertermDraft {
2013 name,
2014 group_id: variant.group_id,
2015 origin,
2016 disable: variant.disable,
2017 requested_subspace: variant.subspace,
2018 requested_parent_lmb: variant.parent_lmb,
2019 multiplier: variant.multiplier,
2020 subspace,
2021 });
2022 }
2023 let association_origin = variants
2024 .first()
2025 .map(|variant| variant.origin)
2026 .unwrap_or(ThresholdCountertermOrigin::Autogenerated);
2027 let variant_subspaces = variants
2028 .iter()
2029 .map(|variant| variant.subspace.clone())
2030 .collect();
2031
2032 if let Some(expected) = &representative_variants {
2033 if variants.len() != expected.len()
2034 || variants.iter().zip(expected).any(|(variant, expected)| {
2035 !variant.is_compatible_with(expected, all_lmbs)
2036 })
2037 {
2038 return Err(eyre!(
2039 "Amplitude graph '{}' raised threshold {:?} resolves incompatible variants for E-surfaces {} and {}: threshold {:?} [{}]; threshold {:?} [{}]. Constituents of one raised residue must retain the same variant names, group IDs, subspaces and multipliers",
2040 self.graph.name,
2041 raised_group.esurface_ids,
2042 representative_esurface_id.0,
2043 esurface_id.0,
2044 representative_esurface.energies,
2045 expected
2046 .iter()
2047 .map(|variant| format!(
2048 "'{}' group_id={:?} parent={:?} cycles={:?}",
2049 variant.name,
2050 variant.group_id,
2051 variant.requested_parent_lmb,
2052 variant.subspace.solve_signature(all_lmbs)
2053 ))
2054 .join("; "),
2055 threshold_edges,
2056 variants
2057 .iter()
2058 .map(|variant| format!(
2059 "'{}' group_id={:?} parent={:?} cycles={:?}",
2060 variant.name,
2061 variant.group_id,
2062 variant.requested_parent_lmb,
2063 variant.subspace.solve_signature(all_lmbs)
2064 ))
2065 .join("; "),
2066 ));
2067 }
2068 } else {
2069 representative_variants = Some(variants);
2070 }
2071 associations.push(ResolvedAmplitudeThresholdAssociationDraft {
2072 esurface_id,
2073 threshold_edges,
2074 origin: association_origin,
2075 variant_subspaces,
2076 });
2077 }
2078
2079 let variants = representative_variants.unwrap_or_default();
2080 if variants.len() != 1
2081 || variants[0].disable
2082 || variants[0].group_id.is_some()
2083 || variants[0].multiplier.is_some()
2084 || !variants[0]
2085 .subspace
2086 .has_equivalent_embedding(&legacy_subspace, all_lmbs)
2087 {
2088 legacy_equivalent = false;
2089 }
2090 group_drafts.push(ResolvedAmplitudeThresholdGroupDraft {
2091 raised_esurface_group: raised_group.clone(),
2092 associations,
2093 variants,
2094 });
2095 }
2096
2097 let mut variants =
2098 TiVec::<ThresholdCountertermVariantId, ResolvedThresholdCountertermVariant>::new();
2099 for group in group_drafts {
2100 for (variant_index, variant) in group.variants.into_iter().enumerate() {
2101 if variant.disable {
2102 continue;
2103 }
2104 let parent_lmb_index = variant.subspace.parent_lmb_index();
2105 variants.push(ResolvedThresholdCountertermVariant {
2106 name: variant.name,
2107 group_id: variant.group_id,
2108 cut_group_id: None,
2109 associations: group
2110 .associations
2111 .iter()
2112 .map(|association| {
2113 let subspace = association.variant_subspaces[variant_index].clone();
2114 ResolvedThresholdCountertermAssociation {
2115 cut_id: None,
2116 cut_edges: Vec::new(),
2117 threshold_edges: association.threshold_edges.clone(),
2118 esurface_id: association.esurface_id,
2119 requires_explicit_parent_lmb: subspace.parent_lmb_index()
2120 != legacy_subspace.parent_lmb_index(),
2121 subspace,
2122 eligible: true,
2123 origin: association.origin,
2124 }
2125 })
2126 .collect(),
2127 side: ThresholdCountertermSide::Amplitude,
2128 threshold_esurface_ids: group.raised_esurface_group.esurface_ids.clone(),
2129 raised_esurface_group: group.raised_esurface_group.clone(),
2130 requested_subspace: variant.requested_subspace,
2131 requested_parent_lmb: variant.requested_parent_lmb,
2132 resolved_parent_lmb: all_lmbs[parent_lmb_index].loop_edges.clone().into(),
2133 subspace_loop_count: variant.subspace.loopcount(),
2134 subspace: variant.subspace,
2135 multiplier: variant.multiplier,
2136 });
2137 }
2138 }
2139
2140 Ok((
2141 ResolvedThresholdCounterterms {
2142 legacy_equivalent,
2143 variants,
2144 cross_section_cut_groups: TiVec::new(),
2145 },
2146 raised_esurface_ids,
2147 ))
2148 }
2149
2150 #[instrument(skip_all, err)]
2151 fn build_threshold_counterterm_parametric_integrand(
2152 &mut self,
2153 raised_data: &RaisedEsurfaceData,
2154 settings: &GenerationSettings,
2155 vakint: &Vakint,
2156 locked_runtime_settings: &LockedRuntimeSettings,
2157 model: &Model,
2158 ) -> Result<AmplitudeThresholdCountertermBuild> {
2159 let _progress_guard =
2160 generation_progress::enter_detailed_progress_span("Building Threshold Counterterms");
2161 let cff_options = self.graph.production_cff_3d_expression_options(settings)?;
2162 let production_expression = self
2163 .derived_data
2164 .cff_expression
2165 .as_ref()
2166 .expect("cff_expression should have been created");
2167 let (resolved, raised_esurface_ids) = self
2168 .resolve_amplitude_threshold_counterterm_directives(
2169 raised_data,
2170 settings,
2171 locked_runtime_settings,
2172 model,
2173 )?;
2174 let mut cuts = Vec::with_capacity(resolved.variants.len());
2175 for variant in &resolved.variants {
2176 let mut cut_union: SuBitGraph = self.graph.empty_subgraph();
2177 let representative_esurface = &self
2178 .derived_data
2179 .cff_expression
2180 .as_ref()
2181 .expect("cff_expression should have been created")
2182 .expression
2183 .surfaces
2184 .esurface_cache[variant.raised_esurface_group.esurface_ids[0]];
2185 for energy in &representative_esurface.energies {
2186 let (_, hedge_pair) = self.graph[energy];
2187 match hedge_pair {
2188 HedgePair::Paired { source, sink } => {
2189 cut_union.add(source);
2190 cut_union.add(sink);
2191 }
2192 _ => unreachable!(),
2193 }
2194 }
2195
2196 let cutset = CutSet {
2197 residue_selector: ResidueSelector {
2198 lu: None,
2199 left_th_cut: Some(variant.raised_esurface_group.clone()),
2200 right_th_cut: None,
2201 },
2202 union: cut_union,
2203 canonicalize_external_shifts: false,
2204 };
2205
2206 cuts.push(cutset);
2207 }
2208 let cut_structure = CutStructure { cuts };
2209 let exprs: Vec<_> = settings.uv.orchestrator.parametric_integrands(
2210 &mut self.graph,
2211 cut_structure,
2212 vakint,
2213 OrientationProjection::exact_expression(
2214 production_expression,
2215 &cff_options,
2216 &settings.orientation_pattern,
2217 settings.explicit_orientation_sum_only,
2218 )
2219 .with_vacuum_subtraction(settings.medium.vacuum_subtraction),
2220 settings,
2221 )?;
2222
2223 let mut variants =
2224 TiVec::<ThresholdCountertermVariantId, AmplitudeThresholdCountertermVariant>::new();
2225 for ((variant_id, variant), expr) in
2226 resolved.variants.iter_enumerated().zip(exprs.into_iter())
2227 {
2228 let loop_number = self.graph.n_loops(&self.graph.underlying.full_filter());
2229 let jacobian_factor = Atom::var(GS.radius_star_left).pow(loop_number as i32 * 3 - 1);
2230
2231 let expr = expr.map(|integrand| integrand * &jacobian_factor);
2232 let counterterm_atom = AmplitudeCountertermAtom {
2233 parametric: expr.integrands,
2234 };
2235 let raised_group = expr.cuts.residue_selector.left_th_cut.ok_or_else(|| {
2236 eyre!(
2237 "Threshold orchestrator amplitude result {} for graph '{}' has no threshold residue selector",
2238 variant_id.0,
2239 self.graph.name,
2240 )
2241 })?;
2242 if raised_group != variant.raised_esurface_group {
2243 return Err(eyre!(
2244 "Threshold orchestrator amplitude result {} for graph '{}' returned raised group {:?}, expected {:?}",
2245 variant_id.0,
2246 self.graph.name,
2247 raised_group.esurface_ids,
2248 variant.raised_esurface_group.esurface_ids,
2249 ));
2250 }
2251 let raised_esurface_id = raised_esurface_ids[raised_group.esurface_ids[0]];
2252 debug!("raised_esurface_id: {}", raised_esurface_id.0);
2253
2254 if tracing::enabled!(tracing::Level::DEBUG) {
2255 for (_, integrand) in counterterm_atom.parametric.resolved()?.iter() {
2256 debug!("counterterm integrand: {}", integrand.log_print(Some(100)));
2257 }
2258 }
2259
2260 variants.push(AmplitudeThresholdCountertermVariant {
2261 raised_esurface_id,
2262 atom: counterterm_atom,
2263 });
2264 }
2265 let legacy_counterterms = if resolved.legacy_equivalent {
2266 let mut legacy_counterterms = ti_vec![
2267 AmplitudeCountertermAtom::new();
2268 raised_data.raised_groups.len()
2269 ];
2270 for variant in &variants {
2271 if legacy_counterterms[variant.raised_esurface_id].is_generated() {
2272 return Err(eyre!(
2273 "Legacy-equivalent amplitude graph '{}' generated duplicate threshold variants for raised group {}",
2274 self.graph.name,
2275 variant.raised_esurface_id.0,
2276 ));
2277 }
2278 legacy_counterterms[variant.raised_esurface_id] = variant.atom.clone();
2279 }
2280 legacy_counterterms
2281 } else {
2282 TiVec::new()
2285 };
2286
2287 Ok(AmplitudeThresholdCountertermBuild {
2288 legacy_counterterms,
2289 variants,
2290 resolved,
2291 raised_esurface_ids,
2292 })
2293 }
2294
2295 #[instrument(skip_all)]
2296 fn build_lmbs(&mut self) {
2297 let _progress_guard =
2298 generation_progress::enter_detailed_progress_span("Building Loop Momentum Bases");
2299 let lmbs = self
2300 .graph
2301 .generate_loop_momentum_bases_of(&self.graph.no_dummy());
2302
2303 self.derived_data.lmbs = Some(lmbs)
2304 }
2305
2306 #[instrument(skip_all, err)]
2307 fn build_tropical_sampler(&mut self, process_settings: &GenerationSettings) -> Result<()> {
2308 let _progress_guard =
2309 generation_progress::enter_detailed_progress_span("Building Tropical Sampler");
2310 if process_settings
2311 .tropical_subgraph_table
2312 .disable_tropical_generation
2313 {
2314 debug!("Tropical subgraph table generation is disabled.");
2315 return Ok(());
2316 }
2317 let num_virtual_loop_edges = self.graph.iter_loop_edges().count();
2318
2319 if num_virtual_loop_edges == 0 {
2320 debug!("Graph has no loop edges, skipping tropical sampler generation.");
2321 return Ok(());
2322 }
2323
2324 let num_loops = self.graph.loop_momentum_basis.loop_edges.len();
2325 let target_omega = process_settings.tropical_subgraph_table.target_omega;
2326
2327 let weight = (target_omega + (3 * num_loops) as f64 / 2.) / num_virtual_loop_edges as f64;
2328
2329 debug!(
2330 "Building tropical subgraph table with all edge weights set to: {}",
2331 weight
2332 );
2333
2334 let tropical_edges = self
2335 .graph
2336 .iter_loop_edges()
2337 .map(|(pair, _edge_id, edge)| {
2338 let is_massive = edge.data.particle.is_massive();
2339
2340 let vertices = match pair {
2341 HedgePair::Paired { source, sink } => (
2342 self.graph.underlying.node_id(source).0 as u8,
2343 self.graph.underlying.node_id(sink).0 as u8,
2344 ),
2345 _ => unreachable!(),
2346 };
2347
2348 momtrop::Edge {
2349 is_massive,
2350 weight,
2351 vertices,
2352 }
2353 })
2354 .collect_vec();
2355
2356 let mut external_vertices_pool = AHashSet::new();
2357
2358 for (pair, _, _) in self.graph.iter_non_loop_edges() {
2359 match pair {
2360 HedgePair::Paired { source, sink } => {
2361 let source_id = self.graph.underlying.node_id(source).0 as u8;
2362 let sink_id = self.graph.underlying.node_id(sink).0 as u8;
2363
2364 external_vertices_pool.insert(source_id);
2365 external_vertices_pool.insert(sink_id);
2366 }
2367 HedgePair::Unpaired { hedge, .. } => {
2368 let id = self.graph.underlying.node_id(hedge).0 as u8;
2369 external_vertices_pool.insert(id);
2370 }
2371 _ => unreachable!(),
2372 }
2373 }
2374
2375 let mut external_vertices = vec![];
2376
2377 for tropical_edge in &tropical_edges {
2378 if external_vertices_pool.contains(&tropical_edge.vertices.0) {
2379 external_vertices.push(tropical_edge.vertices.0);
2380 }
2381
2382 if external_vertices_pool.contains(&tropical_edge.vertices.1) {
2383 external_vertices.push(tropical_edge.vertices.1);
2384 }
2385 }
2386
2387 let tropical_graph = momtrop::Graph {
2388 edges: tropical_edges,
2389 externals: external_vertices,
2390 };
2391
2392 let loop_part = self
2393 .graph
2394 .iter_loop_edges()
2395 .map(|(_, edge_id, _edge)| {
2396 self.graph.loop_momentum_basis.edge_signatures[edge_id]
2397 .internal
2398 .clone()
2399 .to_momtrop_format()
2400 })
2401 .collect_vec();
2402
2403 let sampler = tropical_graph
2404 .build_sampler(loop_part)
2405 .map_err(|e| eyre!(e))?;
2406
2407 let _: () = self.derived_data.tropical_sampler = Some(sampler);
2408 Ok(())
2409 }
2410
2411 #[instrument(
2413 name = "generate_term_for_graph",
2414 level = "info",
2415 skip(self, model, global_settings),
2416 fields(
2417 graph.name = %self.graph.name
2418
2419 ),
2420 err
2421 )]
2422 fn generate_term_for_graph(
2423 &self,
2424 model: &Model,
2425 own_group_position: GraphGroupPosition,
2426 esurface_map: TiVec<GroupEsurfaceId, TiVec<GraphGroupPosition, Option<RaisedEsurfaceId>>>,
2427 global_settings: &GlobalSettings,
2428 ) -> Result<(AmplitudeGraphTerm, GraphGenerationStats)> {
2429 let _progress_guard = generation_progress::enter_detailed_progress_span(&format!(
2430 "Generating Evaluators for {}",
2431 self.graph.name
2432 ));
2433 AmplitudeGraphTerm::from_amplitude_graph(
2434 self,
2435 own_group_position,
2436 esurface_map,
2437 model,
2438 global_settings,
2439 )
2440 }
2441}
2442
2443#[derive(Clone, Encode, Decode)]
2444#[trait_decode(trait = GammaLoopContext)]
2445pub struct AmplitudeThresholdCountertermVariant {
2446 pub raised_esurface_id: RaisedEsurfaceId,
2447 pub atom: AmplitudeCountertermAtom,
2448}
2449
2450#[derive(Clone, Encode, Decode)]
2451#[trait_decode(trait = GammaLoopContext)]
2452pub struct AmplitudeDerivedData {
2453 pub threshold_topology: Option<(Graph, TiVec<LmbIndex, LoopMomentumBasis>)>,
2455 pub all_mighty_integrand: Atom,
2456 pub all_mighty_numerators: Vec<Arc<FnMapEntry>>,
2457 pub threshold_counterterms: TiVec<RaisedEsurfaceId, AmplitudeCountertermAtom>,
2459 pub threshold_counterterm_variants:
2461 TiVec<ThresholdCountertermVariantId, AmplitudeThresholdCountertermVariant>,
2462 pub resolved_threshold_counterterms: Option<ResolvedThresholdCounterterms>,
2463 pub raised_data: RaisedEsurfaceData,
2464 pub raised_esurface_ids: TiVec<EsurfaceID, RaisedEsurfaceId>,
2465 pub multi_channeling_setup: Option<LmbMultiChannelingSetup>,
2466 pub lmbs: Option<TiVec<LmbIndex, LoopMomentumBasis>>,
2467 pub tropical_sampler: Option<SampleGenerator<3>>,
2468 pub cff_expression: Option<
2469 GeneratedThreeDExpression<crate::cff::esurface::Esurface, crate::cff::hsurface::Hsurface>,
2470 >,
2471}
2472
2473impl AmplitudeDerivedData {
2474 pub(crate) fn resolved_integrand(&self) -> Result<Atom> {
2475 let integrands = Integrands::from_iter([(
2476 CutCFFIndex::new_all_none(),
2477 self.all_mighty_integrand.clone(),
2478 )])
2479 .with_numerators(self.all_mighty_numerators.iter().cloned())?
2480 .resolved()?;
2481 Ok(integrands
2482 .iter()
2483 .next()
2484 .expect("the amplitude semantic view retains its root residue")
2485 .1
2486 .clone())
2487 }
2488}
2489
2490pub trait AmplitudeState:
2491 Clone + std::fmt::Debug + bincode::Encode + for<'a> bincode::Decode<GammaLoopContextContainer<'a>>
2492{
2493}
2494impl AmplitudeState for () {}
2495
2496#[derive(Clone, Encode, Decode, Debug)]
2497pub struct Processed {}
2498impl AmplitudeState for Processed {}
2499
2500impl Amplitude {
2505 pub fn from_dot_string<Str: AsRef<str>>(s: Str, name: String, model: &Model) -> Result<Self> {
2506 let graphs = Graph::from_string(s, model)?;
2507
2508 let mut amp = Amplitude::new(name);
2509 for g in graphs {
2510 amp.add_graph(g)?;
2511 }
2512 Ok(amp)
2513 }
2514
2515 pub fn from_dot_file<P>(p: P, name: String, model: &Model) -> Result<Self>
2516 where
2517 P: AsRef<Path>,
2518 {
2519 let graphs = Graph::from_file(p, model)?;
2520
2521 let mut amp = Amplitude::new(name);
2522 for g in graphs {
2523 amp.add_graph(g)?;
2524 }
2525 Ok(amp)
2526 }
2527
2528 pub fn from_graph_list(name: impl ToString, mut graphs: Vec<Graph>) -> Result<Self> {
2529 let mut amplitude: Amplitude = Amplitude::new(name);
2530 amplitude.graph_group_structure = complete_group_parsing(&mut graphs)?;
2531 for group in &litude.graph_group_structure {
2532 ThresholdCountertermSpec::validate_group_ids(
2533 group
2534 .into_iter()
2535 .map(|id| &*graphs[id].threshold_counterterms),
2536 )
2537 .with_context(|| {
2538 format!(
2539 "Invalid threshold group IDs for amplitude group master '{}'",
2540 graphs[group.master()].name,
2541 )
2542 })?;
2543 }
2544
2545 for amplitude_graph in graphs {
2546 amplitude.add_graph(amplitude_graph)?;
2547 }
2548 Ok(amplitude)
2549 }
2550
2551 pub(crate) fn new(name: impl ToString) -> Self {
2552 Self {
2553 integrand: None,
2554 name: name.to_string(),
2555 graphs: vec![],
2556 graph_group_structure: TiVec::new(),
2557 external_particles: vec![],
2558 external_signature: SignatureLike::from_iter(iter::empty::<i8>()),
2559 group_derived_data: TiVec::new(),
2560 }
2561 }
2562
2563 fn add_graph(&mut self, graph: Graph) -> Result<()> {
2564 let new_external_particels = graph.get_external_partcles();
2565 let new_external_signature = graph.get_external_signature();
2566
2567 if !self.graphs.is_empty() {
2568 if self.external_particles != new_external_particels {
2569 return Err(eyre!("amplitude graph has different externals")).with_context(|| {
2570 format!(
2571 "Found {} externals, expected {} for the graph {}",
2572 new_external_particels.len(),
2573 self.external_particles.len(),
2574 DotGraph::from(&graph).debug_dot()
2575 )
2576 });
2577 }
2578
2579 if self.external_signature != new_external_signature {
2580 return Err(eyre!("wrong external signature"));
2581 }
2582 } else {
2583 self.external_particles = new_external_particels;
2584 self.external_signature = new_external_signature;
2585 }
2586
2587 self.graphs.push(AmplitudeGraph::new(graph));
2588
2589 Ok(())
2591 }
2592}
2593
2594fn threshold_counterterm_helper_atoms(
2595 order: u8,
2596 loop_number: usize,
2597) -> (SingleThresholdPieces<Atom>, Atom) {
2598 let loop_3 = loop_number as i64 * 3;
2599
2600 let laurent_coeff_indices = (1..=order).map(|i| -(i as i8));
2601
2602 let mut laurent_coeffs = laurent_coeff_indices.map(|laurent_coeff_index| {
2603 build_derivative_structure_atom(order, laurent_coeff_index)
2604 .replace(GS.rescale_star)
2605 .with(GS.radius_star_left)
2606 });
2607
2608 let i = Atom::i();
2609
2610 let radius = Atom::var(GS.radius_left);
2611 let radius_star = Atom::var(GS.radius_star_left);
2612 let uv_damp_plus = Atom::var(GS.uv_damp_plus_left);
2613 let uv_damp_minus = Atom::var(GS.uv_damp_minus_left);
2614 let hfunction = Atom::var(GS.hfunction_left_th);
2615
2616 let delta_r_plus = &radius - &radius_star;
2617 let delta_r_minus = -&radius - &radius_star;
2618
2619 let jacobian_ratio = (Atom::one() / &radius).pow(loop_3 - 1);
2620
2621 let local_prefactor =
2622 &jacobian_ratio * (uv_damp_plus / &delta_r_plus + uv_damp_minus / &delta_r_minus);
2623
2624 let integrated_prefactor = -i * Atom::var(GS.pi) * &jacobian_ratio * hfunction;
2625
2626 let leading_laurent_coeff = laurent_coeffs.next().unwrap();
2627 let mut raised_local = Atom::Zero;
2628
2629 for pow in 2..=order {
2630 raised_local += laurent_coeffs.next().unwrap()
2631 * &jacobian_ratio
2632 * (Atom::one() / delta_r_plus.pow(pow as i64)
2633 + Atom::one() / delta_r_minus.pow(pow as i64));
2634 }
2635
2636 let local = local_prefactor.clone() * &leading_laurent_coeff + &raised_local;
2637 let integrated = integrated_prefactor.clone() * &leading_laurent_coeff;
2638 let legacy = (local_prefactor + integrated_prefactor) * leading_laurent_coeff + raised_local;
2640
2641 debug!(
2642 "Threshold counterterm helper atoms for order {} and loop number {}: local={}, integrated={}, legacy={}",
2643 order, loop_number, local, integrated, legacy,
2644 );
2645 (SingleThresholdPieces { local, integrated }, legacy)
2646}
2647
2648enum ThresholdCountertermHelperOutputs {
2649 Legacy,
2650 Pieces,
2651 LegacyAndPieces,
2652}
2653
2654fn build_threshold_counterterm_helper(
2655 order: u8,
2656 loop_number: usize,
2657 evaluator_settings: &EvaluatorSettings,
2658 outputs: ThresholdCountertermHelperOutputs,
2659) -> GenericEvaluator {
2660 let (pieces, legacy) = threshold_counterterm_helper_atoms(order, loop_number);
2661 let atoms = match outputs {
2662 ThresholdCountertermHelperOutputs::Legacy => vec![legacy],
2663 ThresholdCountertermHelperOutputs::Pieces => vec![pieces.local, pieces.integrated],
2664 ThresholdCountertermHelperOutputs::LegacyAndPieces => {
2665 vec![legacy, pieces.local, pieces.integrated]
2666 }
2667 };
2668 let fn_map = FunctionMap::default();
2669
2670 let mut params = params_for_derivative_order(order)
2671 .into_iter()
2672 .map(|param| param.replace(GS.rescale_star).with(GS.radius_star_left))
2673 .collect_vec();
2674
2675 let radius = Atom::var(GS.radius_left);
2676 let radius_star = Atom::var(GS.radius_star_left);
2677 let uv_damp_plus = Atom::var(GS.uv_damp_plus_left);
2678 let uv_damp_minus = Atom::var(GS.uv_damp_minus_left);
2679 let hfunction = Atom::var(GS.hfunction_left_th);
2680
2681 params.push(radius);
2682 params.push(radius_star);
2683 params.push(uv_damp_plus);
2684 params.push(uv_damp_minus);
2685 params.push(hfunction);
2686
2687 GenericEvaluator::new_from_raw_params(
2688 atoms,
2689 ¶ms,
2690 &fn_map,
2691 vec![],
2692 evaluator_settings.optimization_settings(),
2693 None,
2694 evaluator_settings,
2695 )
2696 .unwrap()
2697 .into_eager_only()
2698}
2699
2700pub(crate) fn threshold_counterterm_helper(
2701 order: u8,
2702 loop_number: usize,
2703 evaluator_settings: &EvaluatorSettings,
2704) -> GenericEvaluator {
2705 build_threshold_counterterm_helper(
2706 order,
2707 loop_number,
2708 evaluator_settings,
2709 ThresholdCountertermHelperOutputs::Legacy,
2710 )
2711}
2712
2713pub(crate) fn threshold_counterterm_pieces_helper(
2714 order: u8,
2715 loop_number: usize,
2716 evaluator_settings: &EvaluatorSettings,
2717) -> GenericEvaluator {
2718 build_threshold_counterterm_helper(
2719 order,
2720 loop_number,
2721 evaluator_settings,
2722 ThresholdCountertermHelperOutputs::Pieces,
2723 )
2724}
2725
2726pub(crate) fn threshold_counterterm_recording_helper(
2727 order: u8,
2728 loop_number: usize,
2729 evaluator_settings: &EvaluatorSettings,
2730) -> GenericEvaluator {
2731 build_threshold_counterterm_helper(
2732 order,
2733 loop_number,
2734 evaluator_settings,
2735 ThresholdCountertermHelperOutputs::LegacyAndPieces,
2736 )
2737}
2738
2739#[cfg(test)]
2740pub mod test {
2741
2742 use std::{
2743 fs,
2744 io::Cursor,
2745 time::{SystemTime, UNIX_EPOCH},
2746 };
2747
2748 use crate::{
2749 DependentMomentaConstructor, GammaLoopContextContainer,
2750 cff::{CutCFFIndex, esurface::ExistingEsurfaceId, expression::OrientationID},
2751 dot,
2752 graph::{
2753 FeynmanGraph, Graph, GraphGroupPosition,
2754 autogen::Autogen,
2755 parse::IntoGraph,
2756 threshold_counterterms::{
2757 ThresholdCountertermCut, ThresholdCountertermMultiplier, ThresholdCountertermSpec,
2758 ThresholdCountertermThreshold, ThresholdCountertermVariant,
2759 },
2760 },
2761 initialisation::test_initialise,
2762 integrands::process::{
2763 MomentumSpaceEvaluationInput, ProcessIntegrand, amplitude::AmplitudeGraphTerm,
2764 },
2765 momentum::{
2766 Dep, ExternalMomenta, Helicity, ThreeMomentum,
2767 sample::{LoopIndex, MomentumSample},
2768 },
2769 processes::{
2770 AmplitudeGraph, DotExportSettings, ThresholdCountertermComponentKind,
2771 ThresholdCountertermOrigin, ThresholdCountertermSide,
2772 },
2773 settings::{
2774 GlobalSettings, RuntimeSettings,
2775 global::{GenerationSettings, OrientationPattern, ThresholdSubtractionSettings},
2776 runtime::kinematic::{
2777 Externals, KinematicsSettings, improvement::PhaseSpaceImprovementSettings,
2778 },
2779 },
2780 subtraction::amplitude_counterterm::AmplitudeCountertermComponentEvaluation,
2781 utils::{ArbPrec, F, load_generic_model},
2782 uv::{
2783 UVOrchestrator, UVgenerationSettings,
2784 approx::{CutStructure, OrientationProjection},
2785 },
2786 };
2787 use itertools::Itertools;
2788 use spenso::algebra::complex::Complex;
2789 use symbolica::state::State;
2790 use typed_index_collections::TiVec;
2791
2792 #[test]
2793 fn metadata_free_amplitude_group_retains_native_member_geometry() {
2794 std::thread::Builder::new()
2795 .stack_size(32 * 1024 * 1024)
2796 .spawn(|| {
2797 test_initialise().unwrap();
2798 let model = load_generic_model("scalars");
2799 let graphs = Graph::from_string(
2800 include_str!(concat!(
2801 env!("CARGO_MANIFEST_DIR"),
2802 "/../../tests/resources/graphs/grouped_subtraction/group.dot"
2803 )),
2804 &model,
2805 )
2806 .unwrap();
2807 let mut amplitude =
2808 super::Amplitude::from_graph_list("native_group_defaults", graphs).unwrap();
2809 let settings = GenerationSettings {
2810 threshold_subtraction: ThresholdSubtractionSettings {
2811 enable_thresholds: true,
2812 assume_positive_external_energies: false,
2813 check_esurface_at_generation: false,
2814 ..Default::default()
2815 },
2816 ..Default::default()
2817 };
2818 let pool = rayon::ThreadPoolBuilder::new()
2819 .num_threads(1)
2820 .build()
2821 .unwrap();
2822 amplitude
2823 .preprocess(&model, &settings, &(&RuntimeSettings::default()).into(), &pool)
2824 .unwrap();
2825 let master = &litude.graphs[amplitude.graph_group_structure[super::GroupId(0)].master()];
2826 assert!(amplitude.graphs.iter().any(|member| member.graph.n_edges() > master.graph.n_edges()));
2827 for member in &litude.graphs {
2828 let resolved = member.derived_data.resolved_threshold_counterterms.as_ref().unwrap();
2829 assert!(resolved.legacy_equivalent);
2830 assert!(!resolved.variants.is_empty());
2831 let lmbs = member.derived_data.lmbs.as_ref().unwrap();
2832 for variant in &resolved.variants {
2833 assert_eq!(
2834 variant.subspace.get_lmb(lmbs).edge_signatures,
2835 member.graph.loop_momentum_basis.edge_signatures,
2836 "implicit full-space defaults must retain the member's physical propagator routing",
2837 );
2838 }
2839 }
2840 let mut exported = String::new();
2841 for member in &litude.graphs {
2842 member.write_dot_fmt(&mut exported, &DotExportSettings {
2843 include_autogenerated_fields: true,
2844 ..Default::default()
2845 }).unwrap();
2846 }
2847 let reimported = super::Amplitude::from_graph_list(
2848 "native_group_defaults_roundtrip", Graph::from_string(&exported, &model).unwrap()
2849 ).unwrap();
2850 assert!(reimported.graphs[1].graph.threshold_counterterms.autogenerated,
2851 "export must not reinterpret a native default as explicit master metadata");
2852 })
2853 .unwrap()
2854 .join()
2855 .unwrap();
2856 }
2857
2858 #[test]
2859 fn amplitude_group_ids_are_contiguous_across_members() {
2860 test_initialise().unwrap();
2861 let graph: Graph = dot!(
2862 digraph shared_group_id_namespace {
2863 graph [group_id=0]
2864 edge [particle=scalar_0]
2865 node [num=1]
2866 e [style=invis]
2867 e -> A:0 [id=3]
2868 B:1 -> e [id=2]
2869 A -> B [id=1]
2870 A -> B [id=0 lmb_id=0]
2871 },
2872 "scalars"
2873 )
2874 .unwrap();
2875 let members = (0..2)
2876 .map(|group_id| {
2877 let mut member = graph.clone();
2878 member.name = format!("namespace_member_{group_id}");
2879 member.is_group_master = group_id == 0;
2880 member.threshold_counterterms = Autogen::explicit(ThresholdCountertermSpec {
2881 schema_version: 1,
2882 function_map: Default::default(),
2883 cuts: vec![ThresholdCountertermCut {
2884 edges: Vec::new(),
2885 thresholds: vec![ThresholdCountertermThreshold {
2886 edges: vec![super::EdgeIndex(0), super::EdgeIndex(1)],
2887 counterterms: vec![ThresholdCountertermVariant {
2888 name: Some(format!("group_{group_id}")),
2889 group_id: Some(group_id),
2890 subspace: Some(vec![super::EdgeIndex(0)]),
2891 parent_lmb: Some(vec![super::EdgeIndex(0)]),
2892 disable: true,
2893 multiplier: None,
2894 }],
2895 }],
2896 }],
2897 });
2898 member
2900 .threshold_counterterms
2901 .validate_for_graph(&member)
2902 .unwrap();
2903 member
2904 })
2905 .collect();
2906 let amplitude = super::Amplitude::from_graph_list("shared_namespace", members).unwrap();
2907 assert_eq!(amplitude.graph_group_structure.len(), 1);
2908 assert_eq!(amplitude.graphs.len(), 2);
2909 }
2910
2911 #[test]
2912 fn amplitude_directive_defaults_and_explicit_variants_use_the_empty_cut() {
2913 let threshold_edges = vec![super::EdgeIndex::from(0), super::EdgeIndex::from(1)];
2914 let mut spec = ThresholdCountertermSpec {
2915 schema_version: 1,
2916 function_map: Default::default(),
2917 cuts: vec![ThresholdCountertermCut {
2918 edges: Vec::new(),
2919 thresholds: vec![ThresholdCountertermThreshold {
2920 edges: threshold_edges.clone(),
2921 counterterms: Vec::new(),
2922 }],
2923 }],
2924 };
2925
2926 let default =
2927 super::AmplitudeGraph::configured_amplitude_threshold_variants(&spec, &threshold_edges);
2928 assert_eq!(default.len(), 1);
2929 assert_eq!(default[0].0.name.as_deref(), Some("default"));
2930 assert_eq!(default[0].1, ThresholdCountertermOrigin::Autogenerated);
2931
2932 spec.cuts[0].thresholds[0].counterterms = vec![
2933 ThresholdCountertermVariant {
2934 group_id: None,
2935 name: Some("disabled".to_string()),
2936 subspace: None,
2937 parent_lmb: Some(vec![super::EdgeIndex::from(0)]),
2938 disable: true,
2939 multiplier: None,
2940 },
2941 ThresholdCountertermVariant {
2942 group_id: None,
2943 name: Some("duplicate".to_string()),
2944 subspace: None,
2945 parent_lmb: Some(vec![super::EdgeIndex::from(0)]),
2946 disable: false,
2947 multiplier: None,
2948 },
2949 ];
2950 spec.function_map = [
2951 ("shared".to_string(), "7".to_string()),
2952 ("scale".to_string(), "2".to_string()),
2953 ]
2954 .into();
2955 spec.cuts[0].thresholds[0].counterterms[1].multiplier =
2956 Some(ThresholdCountertermMultiplier {
2957 expression: "shared*scale".to_string(),
2958 function_map: [("scale".to_string(), "3".to_string())].into(),
2959 symmetrize: false,
2960 opaque_derivatives: true,
2961 });
2962 let explicit =
2963 super::AmplitudeGraph::configured_amplitude_threshold_variants(&spec, &threshold_edges);
2964 assert_eq!(explicit.len(), 2);
2965 assert!(explicit[0].0.disable);
2966 let definitions = &explicit[1].0.multiplier.as_ref().unwrap().function_map;
2967 assert_eq!(definitions["shared"], "7");
2968 assert_eq!(definitions["scale"], "3");
2969 assert_eq!(spec.function_map["scale"], "2");
2970 assert!(
2971 explicit
2972 .iter()
2973 .all(|(_, origin)| *origin == ThresholdCountertermOrigin::Explicit)
2974 );
2975 }
2976
2977 #[test]
2978 fn member_threshold_subspace_is_interpreted_on_master_topology() {
2979 test_initialise().unwrap();
2980 let master: Graph = dot!(digraph metadata_master {
2981 ext [style=invis]
2982 node [num=1]
2983 edge [num=1 mass=0]
2984 ext -> a [id=0]
2985 ext -> b [id=1]
2986 ext -> c [id=2]
2987 a -> b [id=3]
2988 b -> c [id=4]
2989 c -> a [id=5 lmb_id=0]
2990 })
2991 .unwrap();
2992 let member: Graph = dot!(digraph metadata_member {
2995 ext [style=invis]
2996 node [num=1]
2997 edge [num=1 mass=0]
2998 ext -> a [id=0]
2999 ext -> b [id=1]
3000 ext -> c [id=2]
3001 b -> a [id=3]
3002 b -> c [id=4]
3003 c -> a [id=5 lmb_id=0]
3004 })
3005 .unwrap();
3006 let edges = vec![super::EdgeIndex::from(5)];
3007 let master_lmbs = typed_index_collections::ti_vec![master.loop_momentum_basis.clone()];
3008 let member_lmbs = typed_index_collections::ti_vec![member.loop_momentum_basis.clone()];
3009 let parent = super::LmbIndex::from(0);
3010 let expected = super::SubspaceData::new_from_parent_basis_edges(
3011 &edges,
3012 &master.full_filter(),
3013 parent,
3014 &master,
3015 &master_lmbs,
3016 )
3017 .unwrap();
3018 let native = super::SubspaceData::new_from_parent_basis_edges(
3019 &edges,
3020 &member.full_filter(),
3021 parent,
3022 &member,
3023 &member_lmbs,
3024 )
3025 .unwrap();
3026 assert_ne!(
3027 expected.solve_signature(&master_lmbs),
3028 native.solve_signature(&member_lmbs)
3029 );
3030 let mut member = AmplitudeGraph::new(member);
3031 member.derived_data.lmbs = Some(member_lmbs);
3032 member.derived_data.threshold_topology = Some((master, master_lmbs.clone()));
3033 let resolved = member
3034 .resolve_amplitude_threshold_variant_subspace(
3035 &ThresholdCountertermVariant {
3036 name: Some("master_coordinates".to_owned()),
3037 group_id: Some(0),
3038 subspace: Some(edges.clone()),
3039 parent_lmb: Some(edges),
3040 disable: false,
3041 multiplier: None,
3042 },
3043 &expected,
3044 "member metadata in master topology",
3045 )
3046 .unwrap();
3047 assert_eq!(
3048 resolved.solve_signature(&master_lmbs),
3049 expected.solve_signature(&master_lmbs)
3050 );
3051 }
3052
3053 #[test]
3054 fn amplitude_legacy_parent_falls_back_when_the_generation_lmb_is_not_generated() {
3055 test_initialise().unwrap();
3056 let model = load_generic_model("scalars");
3057 let mut graph: AmplitudeGraph =
3058 include_str!("../../../../tests/resources/graphs/uv_tests/dotted_sunrise.dot")
3059 .into_graph(&model)
3060 .unwrap();
3061 graph.build_lmbs();
3062
3063 let all_lmbs = graph.derived_data.lmbs.as_ref().unwrap();
3064 let generation_lmb = graph.graph.loop_momentum_basis.loop_edges.clone();
3065 assert!(
3066 all_lmbs.iter().all(|lmb| lmb.loop_edges != generation_lmb),
3067 "the dotted-sunrise topology must exercise the legacy LMB fallback",
3068 );
3069 let fallback_parent = all_lmbs.iter_enumerated().next().unwrap().0;
3070 assert_eq!(
3071 graph.preferred_amplitude_parent_lmb().unwrap(),
3072 fallback_parent,
3073 );
3074
3075 let legacy_subspace = super::SubspaceData::new_with_user_selected_lmb(
3076 graph.graph.no_dummy(),
3077 fallback_parent,
3078 &graph.graph,
3079 all_lmbs,
3080 )
3081 .unwrap();
3082 let error = graph
3083 .resolve_amplitude_threshold_variant_subspace(
3084 &ThresholdCountertermVariant {
3085 group_id: None,
3086 name: Some("explicit_missing_parent".to_string()),
3087 subspace: None,
3088 parent_lmb: Some(generation_lmb.iter().copied().collect()),
3089 disable: false,
3090 multiplier: None,
3091 },
3092 &legacy_subspace,
3093 "dotted-sunrise explicit-parent test",
3094 )
3095 .unwrap_err();
3096 assert!(
3097 error
3098 .to_string()
3099 .contains("is not among graph 'sunrise' generated LMBs")
3100 );
3101 }
3102
3103 #[test]
3104 fn amplitude_rejects_a_valid_edge_set_that_is_not_a_topological_threshold() {
3105 test_initialise().unwrap();
3106 let mut graph: AmplitudeGraph = dot!(
3107 digraph unmatched_amplitude_threshold {
3108 edge [particle=scalar_1]
3109 node [num=1]
3110 e [style=invis]
3111 e -> A:0 [id=3]
3112 B:1 -> e [id=2]
3113 A -> B [id=1]
3114 A -> B [id=0]
3115 },
3116 "scalars"
3117 )
3118 .unwrap();
3119 graph.graph.threshold_counterterms = Autogen::explicit(ThresholdCountertermSpec {
3120 schema_version: 1,
3121 function_map: Default::default(),
3122 cuts: vec![ThresholdCountertermCut {
3123 edges: Vec::new(),
3124 thresholds: vec![ThresholdCountertermThreshold {
3125 edges: vec![super::EdgeIndex(0)],
3128 counterterms: Vec::new(),
3129 }],
3130 }],
3131 });
3132
3133 let settings = GenerationSettings {
3134 threshold_subtraction: ThresholdSubtractionSettings {
3135 enable_thresholds: true,
3136 assume_positive_external_energies: false,
3137 check_esurface_at_generation: false,
3138 ..Default::default()
3139 },
3140 ..Default::default()
3141 };
3142 graph.generate_cff(&settings).unwrap();
3143 let raised_data = graph.graph.determine_raised_esurfaces_from_expression(
3144 &graph
3145 .derived_data
3146 .cff_expression
3147 .as_ref()
3148 .unwrap()
3149 .expression,
3150 );
3151 graph.build_lmbs();
3152
3153 let error = graph
3154 .resolve_amplitude_threshold_counterterm_directives(
3155 &raised_data,
3156 &settings,
3157 &(&RuntimeSettings::default()).into(),
3158 &load_generic_model("scalars"),
3159 )
3160 .unwrap_err();
3161 assert!(
3162 error
3163 .to_string()
3164 .contains("does not match a topology-discovered E-surface")
3165 );
3166 }
3167
3168 #[test]
3169 fn amplitude_orientation_excluded_topological_threshold_remains_dormant() {
3170 test_initialise().unwrap();
3171 let mut graph: AmplitudeGraph = dot!(
3172 digraph dormant_amplitude_threshold {
3173 edge [particle=scalar_1]
3174 node [num=1]
3175 e [style=invis]
3176 e -> A:0 [id=4]
3177 C:1 -> e [id=3]
3178 A -> B [id=0]
3179 B -> C [id=1]
3180 A -> C [id=2]
3181 },
3182 "scalars"
3183 )
3184 .unwrap();
3185 let selected_graph = graph.graph.clone();
3186 graph.generate_cff(&GenerationSettings::default()).unwrap();
3187 let full_cff = graph.derived_data.cff_expression.as_ref().unwrap();
3188 let physical_thresholds = full_cff
3189 .expression
3190 .surfaces
3191 .esurface_cache
3192 .iter_enumerated()
3193 .filter(|(_, esurface)| !esurface.external_shift.is_empty())
3194 .map(|(esurface_id, esurface)| {
3195 (
3196 esurface_id,
3197 esurface.energies.iter().copied().sorted().collect_vec(),
3198 )
3199 })
3200 .collect_vec();
3201 let (orientation_pattern, dormant_threshold_edges) = full_cff
3202 .expression
3203 .orientations
3204 .iter()
3205 .find_map(|orientation| {
3206 let present = orientation
3207 .iter_denominator_nodes()
3208 .filter_map(|node| match node.data {
3209 crate::cff::surface::HybridSurfaceID::Esurface(esurface_id) => Some(
3210 full_cff.expression.surfaces.esurface_cache[esurface_id]
3211 .energies
3212 .iter()
3213 .copied()
3214 .sorted()
3215 .collect_vec(),
3216 ),
3217 _ => None,
3218 })
3219 .collect::<std::collections::BTreeSet<_>>();
3220 physical_thresholds
3221 .iter()
3222 .find(|(_, edges)| !present.contains(edges))
3223 .map(|(_, edges)| {
3224 (
3225 OrientationPattern::from_orientation(orientation),
3226 edges.clone(),
3227 )
3228 })
3229 })
3230 .expect("the triangle must have a physical threshold absent from one orientation");
3231
3232 let mut graph = AmplitudeGraph::new(selected_graph);
3233 graph.graph.threshold_counterterms = Autogen::explicit(ThresholdCountertermSpec {
3234 schema_version: 1,
3235 function_map: Default::default(),
3236 cuts: vec![ThresholdCountertermCut {
3237 edges: Vec::new(),
3238 thresholds: vec![ThresholdCountertermThreshold {
3239 edges: dormant_threshold_edges.clone(),
3240 counterterms: vec![ThresholdCountertermVariant {
3241 group_id: None,
3242 name: Some("dormant".to_string()),
3243 subspace: None,
3244 parent_lmb: Some(
3245 graph
3246 .graph
3247 .loop_momentum_basis
3248 .loop_edges
3249 .iter()
3250 .copied()
3251 .collect(),
3252 ),
3253 disable: false,
3254 multiplier: None,
3255 }],
3256 }],
3257 }],
3258 });
3259 let settings = GenerationSettings {
3260 orientation_pattern,
3261 threshold_subtraction: ThresholdSubtractionSettings {
3262 enable_thresholds: true,
3263 assume_positive_external_energies: false,
3264 check_esurface_at_generation: false,
3265 ..Default::default()
3266 },
3267 ..Default::default()
3268 };
3269 graph.generate_cff(&settings).unwrap();
3270 assert_eq!(
3273 graph
3274 .derived_data
3275 .cff_expression
3276 .as_ref()
3277 .unwrap()
3278 .expression
3279 .orientations
3280 .iter()
3281 .filter(|orientation| settings.orientation_pattern.filter(*orientation))
3282 .count(),
3283 1,
3284 );
3285 let raised_data = graph.graph.determine_raised_esurfaces_from_expression(
3286 &graph
3287 .derived_data
3288 .cff_expression
3289 .as_ref()
3290 .unwrap()
3291 .expression,
3292 );
3293 graph.build_lmbs();
3294 let (resolved, _) = graph
3295 .resolve_amplitude_threshold_counterterm_directives(
3296 &raised_data,
3297 &settings,
3298 &(&RuntimeSettings::default()).into(),
3299 &load_generic_model("scalars"),
3300 )
3301 .unwrap();
3302 assert!(resolved.variants.iter().all(|variant| {
3303 variant.name != "dormant"
3304 && variant
3305 .associations
3306 .iter()
3307 .all(|association| association.threshold_edges != dormant_threshold_edges)
3308 }));
3309 }
3310
3311 #[test]
3312 fn amplitude_variant_subspace_resolves_in_the_requested_parent() {
3313 test_initialise().unwrap();
3314 let mut graph: AmplitudeGraph = dot!(
3315 digraph amplitude_variant_subspace {
3316 edge [particle=scalar_1]
3317 node [num=1]
3318 e [style=invis]
3319 e -> A:0 [id=3]
3320 B:1 -> e [id=4]
3321 A -> B [id=0]
3322 A -> B [id=1]
3323 A -> B [id=2]
3324 },
3325 "scalars"
3326 )
3327 .unwrap();
3328 graph.build_lmbs();
3329 let preferred_parent = graph.preferred_amplitude_parent_lmb().unwrap();
3330 let all_lmbs = graph.derived_data.lmbs.as_ref().unwrap();
3331 let parent_edges = all_lmbs[preferred_parent]
3332 .loop_edges
3333 .iter()
3334 .copied()
3335 .collect::<Vec<_>>();
3336 let requested_subspace = vec![parent_edges[0]];
3337 let legacy_subspace = super::SubspaceData::new_with_user_selected_lmb(
3338 graph.graph.no_dummy(),
3339 preferred_parent,
3340 &graph.graph,
3341 all_lmbs,
3342 )
3343 .unwrap();
3344 let resolved = graph
3345 .resolve_amplitude_threshold_variant_subspace(
3346 &ThresholdCountertermVariant {
3347 group_id: None,
3348 name: Some("one_loop".to_string()),
3349 subspace: Some(requested_subspace.clone()),
3350 parent_lmb: Some(parent_edges),
3351 disable: false,
3352 multiplier: None,
3353 },
3354 &legacy_subspace,
3355 "amplitude subspace test",
3356 )
3357 .unwrap();
3358
3359 assert_eq!(resolved.parent_lmb_index(), preferred_parent);
3360 assert_eq!(resolved.loopcount(), 1);
3361 assert_eq!(
3362 resolved.iter_basis_edges(all_lmbs).collect::<Vec<_>>(),
3363 requested_subspace,
3364 );
3365 }
3366
3367 #[test]
3368 fn amplitude_preprocessing_keeps_duplicate_geometric_variants_independent() {
3369 std::thread::Builder::new()
3370 .name("amplitude-threshold-variant-test".to_string())
3371 .stack_size(32 * 1024 * 1024)
3372 .spawn(|| {
3373 test_initialise().unwrap();
3374 let mut graph: AmplitudeGraph = dot!(
3375 digraph duplicate_threshold_variants {
3376 edge [particle=scalar_1]
3377 node [num=1]
3378 e [style=invis]
3379 e -> A:0 [id=3]
3380 B:1 -> e [id=2]
3381 A -> B [id=1]
3382 A -> B [id=0]
3383 },
3384 "scalars"
3385 )
3386 .unwrap();
3387 graph.generate_cff(&GenerationSettings::default()).unwrap();
3388 let threshold_edges = graph
3389 .derived_data
3390 .cff_expression
3391 .as_ref()
3392 .unwrap()
3393 .expression
3394 .surfaces
3395 .esurface_cache
3396 .iter()
3397 .find(|esurface| !esurface.external_shift.is_empty())
3398 .expect("the scalar bubble must contain a physical threshold")
3399 .energies
3400 .iter()
3401 .copied()
3402 .sorted()
3403 .collect::<Vec<_>>();
3404 graph.graph.threshold_counterterms = Autogen::explicit(ThresholdCountertermSpec {
3405 schema_version: 1,
3406 function_map: Default::default(),
3407 cuts: vec![ThresholdCountertermCut {
3408 edges: Vec::new(),
3409 thresholds: vec![ThresholdCountertermThreshold {
3410 edges: threshold_edges.clone(),
3411 counterterms: vec![
3412 ThresholdCountertermVariant {
3413 group_id: None,
3414 name: Some("first".to_string()),
3415 subspace: None,
3416 parent_lmb: Some(
3417 graph
3418 .graph
3419 .loop_momentum_basis
3420 .loop_edges
3421 .iter()
3422 .copied()
3423 .collect(),
3424 ),
3425 disable: false,
3426 multiplier: None,
3427 },
3428 ThresholdCountertermVariant {
3429 group_id: None,
3430 name: Some("disabled".to_string()),
3431 subspace: None,
3432 parent_lmb: Some(
3433 graph
3434 .graph
3435 .loop_momentum_basis
3436 .loop_edges
3437 .iter()
3438 .copied()
3439 .collect(),
3440 ),
3441 disable: true,
3442 multiplier: None,
3443 },
3444 ThresholdCountertermVariant {
3445 group_id: None,
3446 name: Some("second".to_string()),
3447 subspace: None,
3448 parent_lmb: Some(
3449 graph
3450 .graph
3451 .loop_momentum_basis
3452 .loop_edges
3453 .iter()
3454 .copied()
3455 .collect(),
3456 ),
3457 disable: false,
3458 multiplier: None,
3459 },
3460 ],
3461 }],
3462 }],
3463 });
3464
3465 let model = load_generic_model("scalars");
3466 let mut generation_settings = GenerationSettings {
3467 threshold_subtraction: ThresholdSubtractionSettings {
3468 enable_thresholds: true,
3469 assume_positive_external_energies: false,
3470 check_esurface_at_generation: false,
3471 ..Default::default()
3472 },
3473 ..Default::default()
3474 };
3475 graph
3476 .preprocess(
3477 &model,
3478 &generation_settings,
3479 &(&RuntimeSettings::default()).into(),
3480 )
3481 .unwrap();
3482
3483 let resolved = graph
3484 .derived_data
3485 .resolved_threshold_counterterms
3486 .as_ref()
3487 .unwrap();
3488 assert!(!resolved.legacy_equivalent);
3489 let mut names_by_group = std::collections::BTreeMap::<Vec<usize>, Vec<&str>>::new();
3490 for variant in resolved.variants.iter().filter(|variant| {
3491 variant
3492 .associations
3493 .iter()
3494 .any(|association| association.threshold_edges == threshold_edges)
3495 }) {
3496 names_by_group
3497 .entry(
3498 variant
3499 .threshold_esurface_ids
3500 .iter()
3501 .map(|esurface_id| esurface_id.0)
3502 .collect(),
3503 )
3504 .or_default()
3505 .push(&variant.name);
3506 }
3507 assert!(!names_by_group.is_empty());
3508 assert!(
3509 names_by_group
3510 .values()
3511 .all(|names| names == &["first", "second"])
3512 );
3513 assert!(
3514 resolved
3515 .variants
3516 .iter()
3517 .all(|variant| variant.side == ThresholdCountertermSide::Amplitude)
3518 );
3519 assert_eq!(
3520 graph.derived_data.threshold_counterterm_variants.len(),
3521 resolved.variants.len(),
3522 );
3523 assert!(
3524 graph
3525 .derived_data
3526 .threshold_counterterm_variants
3527 .iter()
3528 .all(|variant| variant.atom.is_generated())
3529 );
3530 assert!(graph.derived_data.threshold_counterterms.is_empty());
3531
3532 let (term, _) = AmplitudeGraphTerm::from_amplitude_graph(
3533 &graph,
3534 GraphGroupPosition(0),
3535 TiVec::new(),
3536 &model,
3537 &GlobalSettings {
3538 generation: generation_settings.clone(),
3539 ..Default::default()
3540 },
3541 )
3542 .unwrap();
3543 let registry = term
3544 .threshold_counterterm
3545 .metadata_registry
3546 .as_ref()
3547 .expect("explicit duplicate variants must create amplitude metadata");
3548 assert_eq!(registry.variants.len(), resolved.variants.len());
3549 assert_eq!(registry.components.len(), resolved.variants.len() * 2);
3550 assert!(registry.evaluators.is_empty());
3551 assert!(term.threshold_counterterm.threshold_multipliers.is_none());
3552
3553 let decomposition =
3554 crate::integrands::process::amplitude::amplitude_threshold_event_info(
3555 registry,
3556 Complex::new_re(F(100.0)),
3557 vec![
3558 AmplitudeCountertermComponentEvaluation {
3559 variant_id: super::ThresholdCountertermVariantId(0),
3560 kind: ThresholdCountertermComponentKind::Local,
3561 esurface_id: super::RaisedEsurfaceId(4),
3562 overlap_group: 2,
3563 multiplier_value: F(3.0),
3564 bare: Some(Complex::new_re(F(-2.0))),
3565 weighted: Complex::new_re(F(-6.0)),
3566 evaluation_skipped: false,
3567 },
3568 AmplitudeCountertermComponentEvaluation {
3569 variant_id: super::ThresholdCountertermVariantId(0),
3570 kind: ThresholdCountertermComponentKind::Integrated,
3571 esurface_id: super::RaisedEsurfaceId(4),
3572 overlap_group: 2,
3573 multiplier_value: F(0.0),
3574 bare: None,
3575 weighted: Complex::new_re(F(0.0)),
3576 evaluation_skipped: true,
3577 },
3578 ],
3579 )
3580 .unwrap();
3581 assert_eq!(decomposition.total(), Complex::new_re(F(94.0)));
3582 assert_eq!(decomposition.components[0].component_id, 0);
3583 assert_eq!(decomposition.components[1].component_id, 1);
3584 assert!(decomposition.components[1].evaluation_skipped);
3585
3586 graph.graph.threshold_counterterms =
3587 Autogen::generated(ThresholdCountertermSpec::default());
3588 graph
3589 .preprocess(
3590 &model,
3591 &generation_settings,
3592 &(&RuntimeSettings::default()).into(),
3593 )
3594 .unwrap();
3595 let resolved = graph
3596 .derived_data
3597 .resolved_threshold_counterterms
3598 .as_ref()
3599 .unwrap();
3600 assert!(resolved.legacy_equivalent);
3601 assert_eq!(
3602 graph.derived_data.threshold_counterterms.len(),
3603 graph.derived_data.raised_data.raised_groups.len(),
3604 );
3605 assert_eq!(
3606 graph
3607 .derived_data
3608 .threshold_counterterms
3609 .iter()
3610 .filter(|counterterm| counterterm.is_generated())
3611 .count(),
3612 graph.derived_data.threshold_counterterm_variants.len(),
3613 );
3614
3615 generation_settings.threshold_subtraction.enable_thresholds = false;
3616 graph
3617 .preprocess(
3618 &model,
3619 &generation_settings,
3620 &(&RuntimeSettings::default()).into(),
3621 )
3622 .unwrap();
3623 assert!(graph.derived_data.resolved_threshold_counterterms.is_none());
3624 assert!(graph.derived_data.threshold_counterterms.is_empty());
3625 assert!(graph.derived_data.threshold_counterterm_variants.is_empty());
3626 assert!(graph.derived_data.raised_data.raised_groups.is_empty());
3627 })
3628 .expect("amplitude threshold-variant test thread must start")
3629 .join()
3630 .expect("amplitude threshold-variant test thread must finish successfully");
3631 }
3632
3633 #[test]
3634 fn generalized_amplitude_approach_keeps_duplicate_variant_subspaces_and_complements() {
3635 std::thread::Builder::new()
3636 .name("generalized-amplitude-threshold-approach-test".to_string())
3637 .stack_size(32 * 1024 * 1024)
3638 .spawn(|| {
3639 test_initialise().unwrap();
3640 let model = load_generic_model("scalars");
3641 let mut graph: Graph = dot!(
3642 digraph generalized_threshold_approach {
3643 edge [particle=scalar_1]
3644 node [num=1]
3645 e [style=invis]
3646 e -> A:0 [id=4]
3647 B:1 -> e [id=3]
3648 A -> B [id=2]
3649 A -> B [id=1]
3650 A -> B [id=0]
3651 },
3652 "scalars"
3653 )
3654 .unwrap();
3655 let mut parent_probe = AmplitudeGraph::new(graph.clone());
3656 parent_probe.build_lmbs();
3657 let preferred_parent = parent_probe.preferred_amplitude_parent_lmb().unwrap();
3658 let parent_edges = parent_probe.derived_data.lmbs.as_ref().unwrap()
3659 [preferred_parent]
3660 .loop_edges
3661 .iter()
3662 .copied()
3663 .collect::<Vec<_>>();
3664 assert_eq!(parent_edges.len(), 2);
3665 graph.threshold_counterterms = Autogen::explicit(ThresholdCountertermSpec {
3666 schema_version: 1,
3667 function_map: Default::default(),
3668 cuts: vec![ThresholdCountertermCut {
3669 edges: Vec::new(),
3670 thresholds: vec![ThresholdCountertermThreshold {
3671 edges: vec![
3672 super::EdgeIndex(0),
3673 super::EdgeIndex(1),
3674 super::EdgeIndex(2),
3675 ],
3676 counterterms: vec![
3677 ThresholdCountertermVariant {
3678 group_id: None,
3679 name: Some("one_loop".to_string()),
3680 subspace: Some(vec![parent_edges[0]]),
3681 parent_lmb: Some(parent_edges.clone()),
3682 disable: false,
3683 multiplier: None,
3684 },
3685 ThresholdCountertermVariant {
3686 group_id: None,
3687 name: Some("two_loop".to_string()),
3688 subspace: Some(parent_edges.clone()),
3689 parent_lmb: Some(parent_edges),
3690 disable: false,
3691 multiplier: None,
3692 },
3693 ],
3694 }],
3695 }],
3696 });
3697
3698 let generation_settings = GenerationSettings {
3699 threshold_subtraction: ThresholdSubtractionSettings {
3700 enable_thresholds: true,
3701 assume_positive_external_energies: false,
3702 check_esurface_at_generation: false,
3703 ..Default::default()
3704 },
3705 ..Default::default()
3706 };
3707 let runtime = RuntimeSettings {
3708 kinematics: KinematicsSettings {
3709 e_cm: 6.0,
3710 externals: Externals::Constant {
3711 momenta: vec![
3712 ExternalMomenta::Independent([F(6.0), F(0.0), F(0.0), F(0.0)]),
3713 ExternalMomenta::Dependent(Dep::Dep),
3714 ],
3715 helicities: vec![Helicity::ZERO; 2],
3716 improvement_settings: PhaseSpaceImprovementSettings::default(),
3717 f_64_cache: None,
3718 f_128_cache: None,
3719 arb_cache: Default::default(),
3720 },
3721 },
3722 ..RuntimeSettings::default()
3723 };
3724 let pool = rayon::ThreadPoolBuilder::new()
3725 .num_threads(1)
3726 .build()
3727 .unwrap();
3728 let mut amplitude = super::Amplitude::from_graph_list(
3729 "generalized_threshold_approach",
3730 vec![graph],
3731 )
3732 .unwrap();
3733 amplitude
3734 .preprocess(&model, &generation_settings, &(&runtime).into(), &pool)
3735 .unwrap();
3736 amplitude
3737 .build_integrand(
3738 &model,
3739 "generalized_threshold_approach",
3740 &GlobalSettings {
3741 generation: generation_settings,
3742 ..Default::default()
3743 },
3744 (&runtime).into(),
3745 &pool,
3746 )
3747 .unwrap();
3748 let ProcessIntegrand::Amplitude(integrand) = amplitude.integrand.as_mut().unwrap()
3749 else {
3750 panic!("approach fixture built a non-amplitude integrand")
3751 };
3752 let external_signature =
3753 integrand.data.graph_terms[0].graph.get_external_signature();
3754 let sample = MomentumSample::new(
3755 vec![
3756 ThreeMomentum::new(
3757 F::<ArbPrec>::from_f64(0.4),
3758 F::<ArbPrec>::from_f64(0.1),
3759 F::<ArbPrec>::from_f64(-0.2),
3760 ),
3761 ThreeMomentum::new(
3762 F::<ArbPrec>::from_f64(-0.3),
3763 F::<ArbPrec>::from_f64(0.2),
3764 F::<ArbPrec>::from_f64(0.15),
3765 ),
3766 ]
3767 .into_iter()
3768 .collect(),
3769 0,
3770 &runtime.kinematics.externals,
3771 0,
3772 F::<ArbPrec>::from_f64(1.0),
3773 DependentMomentaConstructor::Amplitude(&external_signature),
3774 None,
3775 )
3776 .unwrap();
3777
3778 let term = &mut integrand.data.graph_terms[0];
3779 let approach = term
3780 .kinematics_for_threshold_approach(&runtime, &model, &sample)
3781 .unwrap();
3782 let variant_ids = approach
3783 .variant_ids
3784 .as_ref()
3785 .expect("generalized approach must expose stable variant IDs");
3786 assert_eq!(variant_ids.len(), approach.existing_esurfaces.len());
3787 assert!(variant_ids.len() >= 2);
3788 assert_eq!(
3789 variant_ids
3790 .iter()
3791 .copied()
3792 .collect::<std::collections::BTreeSet<_>>()
3793 .len(),
3794 variant_ids.len(),
3795 );
3796
3797 let counterterm = &term.threshold_counterterm;
3798 let mut loop_counts_by_raised =
3799 std::collections::BTreeMap::<_, std::collections::BTreeSet<_>>::new();
3800 for &variant_id in variant_ids {
3801 loop_counts_by_raised
3802 .entry(counterterm.variant_raised_esurfaces[variant_id])
3803 .or_default()
3804 .insert(counterterm.variant_subspaces[variant_id].loopcount());
3805 }
3806 assert!(loop_counts_by_raised.values().any(|counts| {
3807 counts
3808 == &[1, 2]
3809 .into_iter()
3810 .collect::<std::collections::BTreeSet<_>>()
3811 }));
3812
3813 let mut referenced_variants = std::collections::BTreeSet::new();
3814 for group in &approach.overlap_groups_with_kinematics {
3815 let centers = group
3816 .approach_centers_at_esurface
3817 .as_ref()
3818 .expect("generalized overlap groups need per-variant centers");
3819 assert_eq!(centers.len(), group.overlap_group.existing_esurfaces.len());
3820 assert_eq!(
3821 group.loop_momenta_at_esurface.len(),
3822 group.overlap_group.existing_esurfaces.len()
3823 );
3824 for (position, &existing_esurface_id) in
3825 group.overlap_group.existing_esurfaces.iter().enumerate()
3826 {
3827 let variant_id = variant_ids[existing_esurface_id];
3828 referenced_variants.insert(variant_id);
3829 let local_id = ExistingEsurfaceId::from(position);
3830 let root = group.loop_momenta_at_esurface[local_id]
3831 .as_ref()
3832 .expect("each generalized threshold needs an r_star sample");
3833 let center = centers[local_id]
3834 .as_ref()
3835 .expect("each generalized threshold needs a full center");
3836 for momentum in root.loop_moms().iter().chain(center.iter()) {
3837 for component in [&momentum.px, &momentum.py, &momentum.pz] {
3838 assert!(component.into_f64().is_finite());
3839 }
3840 }
3841
3842 let subspace = &counterterm.variant_subspaces[variant_id];
3843 let parent_lmb = subspace.get_lmb(&counterterm.lmbs);
3844 let base_in_parent =
3845 sample.lmb_transform(&term.graph.loop_momentum_basis, parent_lmb);
3846 let root_in_parent =
3847 root.lmb_transform(&term.graph.loop_momentum_basis, parent_lmb);
3848 let mut center_sample = root.clone();
3849 center_sample.sample.loop_moms = center.clone();
3850 let center_in_parent = center_sample
3851 .lmb_transform(&term.graph.loop_momentum_basis, parent_lmb);
3852 for loop_index in (0..term.graph.get_loop_number()).map(LoopIndex::from) {
3853 if subspace.contains_loop_index(loop_index) {
3854 continue;
3855 }
3856 for (actual, expected) in [
3857 (
3858 &root_in_parent.loop_moms()[loop_index],
3859 &base_in_parent.loop_moms()[loop_index],
3860 ),
3861 (
3862 ¢er_in_parent.loop_moms()[loop_index],
3863 &base_in_parent.loop_moms()[loop_index],
3864 ),
3865 ] {
3866 for (actual, expected) in [
3867 (&actual.px, &expected.px),
3868 (&actual.py, &expected.py),
3869 (&actual.pz, &expected.pz),
3870 ] {
3871 assert!((actual - expected).abs().into_f64() < 1.0e-20);
3872 }
3873 }
3874 }
3875 }
3876 }
3877 assert_eq!(
3878 referenced_variants,
3879 variant_ids
3880 .iter()
3881 .copied()
3882 .collect::<std::collections::BTreeSet<_>>(),
3883 "overlap groups must retain every semantic duplicate independently",
3884 );
3885 })
3886 .expect("generalized amplitude approach test thread must start")
3887 .join()
3888 .expect("generalized amplitude approach test thread must finish successfully");
3889 }
3890
3891 #[test]
3892 fn amplitude_dot_export_materializes_resolved_defaults_and_round_trips() {
3893 std::thread::Builder::new()
3894 .name("amplitude-threshold-dot-export-test".to_string())
3895 .stack_size(32 * 1024 * 1024)
3896 .spawn(|| {
3897 test_initialise().unwrap();
3898 let mut graph: AmplitudeGraph = dot!(
3899 digraph amplitude_threshold_dot_export {
3900 edge [particle=scalar_0]
3903 node [num=1]
3904 e [style=invis]
3905 e -> A:0 [id=3 particle=scalar_2]
3906 B:1 -> e [id=2 particle=scalar_2]
3907 A -> B [id=1]
3908 A -> B [id=0]
3909 },
3910 "scalars"
3911 )
3912 .unwrap();
3913 let model = load_generic_model("scalars");
3914 let generation_settings = GenerationSettings {
3915 threshold_subtraction: ThresholdSubtractionSettings {
3916 enable_thresholds: true,
3917 assume_positive_external_energies: false,
3918 check_esurface_at_generation: false,
3919 ..Default::default()
3920 },
3921 ..Default::default()
3922 };
3923 graph
3924 .preprocess(
3925 &model,
3926 &generation_settings,
3927 &(&RuntimeSettings::default()).into(),
3928 )
3929 .unwrap();
3930
3931 assert!(graph.graph.threshold_counterterms.autogenerated);
3932 assert!(graph.graph.threshold_counterterms.cuts.is_empty());
3933 let resolved = graph
3934 .derived_data
3935 .resolved_threshold_counterterms
3936 .as_ref()
3937 .unwrap();
3938 assert!(resolved.legacy_equivalent);
3939 let expected_materialized = resolved.materialized_spec(
3940 &graph.graph.threshold_counterterms,
3941 graph.derived_data.lmbs.as_ref().unwrap(),
3942 );
3943 assert!(!expected_materialized.cuts.is_empty());
3944 assert!(
3945 expected_materialized
3946 .cuts
3947 .iter()
3948 .all(|cut| cut.edges.is_empty())
3949 );
3950
3951 let mut ordinary = String::new();
3952 graph
3953 .write_dot_fmt(&mut ordinary, &DotExportSettings::default())
3954 .unwrap();
3955 assert!(!ordinary.contains("threshold_counterterms"));
3956 let ordinary_import = Graph::from_string(&ordinary, &model).unwrap().remove(0);
3957 assert!(ordinary_import.threshold_counterterms.autogenerated);
3958 assert!(ordinary_import.threshold_counterterms.cuts.is_empty());
3959
3960 let mut normalized = String::new();
3961 graph
3962 .write_dot_fmt(
3963 &mut normalized,
3964 &DotExportSettings {
3965 include_autogenerated_fields: true,
3966 ..DotExportSettings::default()
3967 },
3968 )
3969 .unwrap();
3970 let normalized_import = Graph::from_string(&normalized, &model).unwrap().remove(0);
3971 assert!(!normalized_import.threshold_counterterms.autogenerated);
3972 assert_eq!(
3973 *normalized_import.threshold_counterterms,
3974 expected_materialized
3975 );
3976 assert!(graph.graph.threshold_counterterms.autogenerated);
3977 assert!(graph.graph.threshold_counterterms.cuts.is_empty());
3978
3979 let mut round_tripped = AmplitudeGraph::new(normalized_import);
3980 round_tripped
3981 .preprocess(
3982 &model,
3983 &generation_settings,
3984 &(&RuntimeSettings::default()).into(),
3985 )
3986 .unwrap();
3987 assert!(
3988 round_tripped
3989 .derived_data
3990 .resolved_threshold_counterterms
3991 .as_ref()
3992 .unwrap()
3993 .legacy_equivalent
3994 );
3995 let (term, _) = AmplitudeGraphTerm::from_amplitude_graph(
3996 &round_tripped,
3997 GraphGroupPosition(0),
3998 TiVec::new(),
3999 &model,
4000 &GlobalSettings {
4001 generation: generation_settings.clone(),
4002 ..Default::default()
4003 },
4004 )
4005 .unwrap();
4006 assert!(term.threshold_counterterm.legacy_equivalent);
4007 assert!(term.threshold_counterterm.variant_evaluators.is_empty());
4008 assert!(term.threshold_counterterm.variant_subspaces.is_empty());
4009 assert!(term.threshold_counterterm.metadata_registry.is_some());
4010 assert_eq!(
4011 term.threshold_counterterm.helper_evaluators.len(),
4012 round_tripped
4013 .derived_data
4014 .raised_data
4015 .pass_two_evaluator
4016 .as_ref()
4017 .unwrap()
4018 .len(),
4019 );
4020
4021 let normalized_graph = Graph::from_string(&normalized, &model).unwrap().remove(0);
4022 let loop_count = normalized_graph.get_loop_number();
4023 let mut runtime = RuntimeSettings {
4027 kinematics: KinematicsSettings {
4028 e_cm: 4.0,
4029 externals: Externals::Constant {
4030 momenta: vec![
4031 ExternalMomenta::Independent([F(4.0), F(0.0), F(0.0), F(0.0)]),
4032 ExternalMomenta::Dependent(Dep::Dep),
4033 ],
4034 helicities: vec![Helicity::ZERO; 2],
4035 improvement_settings: PhaseSpaceImprovementSettings::default(),
4036 f_64_cache: None,
4037 f_128_cache: None,
4038 arb_cache: Default::default(),
4039 },
4040 },
4041 ..RuntimeSettings::default()
4042 };
4043 runtime.general.generate_events = true;
4044 runtime.general.store_additional_weights_in_event = true;
4045 let mut amplitude = super::Amplitude::from_graph_list(
4046 "amplitude_threshold_event_roundtrip",
4047 vec![normalized_graph],
4048 )
4049 .unwrap();
4050 let pool = rayon::ThreadPoolBuilder::new()
4051 .num_threads(1)
4052 .build()
4053 .unwrap();
4054 amplitude
4055 .preprocess(&model, &generation_settings, &(&runtime).into(), &pool)
4056 .unwrap();
4057 amplitude
4058 .build_integrand(
4059 &model,
4060 "amplitude_threshold_event_roundtrip",
4061 &GlobalSettings {
4062 generation: generation_settings.clone(),
4063 ..Default::default()
4064 },
4065 (&runtime).into(),
4066 &pool,
4067 )
4068 .unwrap();
4069 let integrand = amplitude.integrand.as_mut().unwrap();
4070 integrand.warm_up(&model).unwrap();
4071 let result = integrand
4072 .evaluate_momentum_configuration(
4073 &model,
4074 &crate::integrands::process::MomentumSpaceEvaluationInput {
4075 loop_momenta: (0..loop_count)
4076 .map(|index| {
4077 crate::momentum::ThreeMomentum::new(
4078 F(0.35 + index as f64 * 0.1),
4079 F(-0.2),
4080 F(0.45),
4081 )
4082 })
4083 .collect(),
4084 integrator_weight: F(1.0),
4085 graph_id: Some(0),
4086 group_id: None,
4087 orientation: None,
4088 channel_id: None,
4089 },
4090 false,
4091 )
4092 .unwrap();
4093 let events = result
4094 .event_groups
4095 .iter()
4096 .flat_map(|group| group.iter())
4097 .collect::<Vec<_>>();
4098 assert_eq!(events.len(), 1);
4099 let event = events[0];
4100 let decomposition = event
4101 .additional_weights
4102 .threshold_counterterms
4103 .as_ref()
4104 .expect("explicit normalized amplitude must record threshold components");
4105 assert!(!decomposition.components.is_empty());
4106 assert_eq!(decomposition.total(), event.weight);
4107 assert!(decomposition.components.iter().all(|component| {
4108 component.multiplier_values.as_slice() == [F(1.0)]
4109 && component.bare.is_some()
4110 && component.bare == Some(component.weighted)
4111 && !component.evaluation_skipped
4112 }));
4113 assert!(event.additional_weights.weights.keys().any(|key| matches!(
4114 key,
4115 crate::observables::AdditionalWeightKey::AmplitudeThresholdCounterterm { .. }
4116 )));
4117 })
4118 .expect("amplitude threshold DOT-export test thread must start")
4119 .join()
4120 .expect("amplitude threshold DOT-export test thread must finish successfully");
4121 }
4122
4123 #[test]
4124 fn grouped_amplitude_threshold_directives_remain_member_local_through_runtime_and_save_load() {
4125 std::thread::Builder::new()
4126 .name("grouped-amplitude-threshold-ownership-test".to_string())
4127 .stack_size(32 * 1024 * 1024)
4128 .spawn(|| {
4129 test_initialise().unwrap();
4130 let model = load_generic_model("scalars");
4131 let mut member_a: Graph = dot!(
4132 digraph grouped_threshold_member_a {
4133 graph [group_id=0 is_group_master=true]
4134 edge [particle=scalar_0]
4135 node [num=1]
4136 e [style=invis]
4137 e -> A:0 [id=3 particle=scalar_2]
4138 B:1 -> e [id=2 particle=scalar_2]
4139 A -> B [id=1]
4140 A -> B [id=0 lmb_id=0]
4141 },
4142 "scalars"
4143 )
4144 .unwrap();
4145 let mut member_b: Graph = dot!(
4146 digraph grouped_threshold_member_b {
4147 graph [group_id=0]
4148 edge [particle=scalar_0]
4149 node [num=1]
4150 e [style=invis]
4151 e -> A:0 [id=3 particle=scalar_2]
4152 B:1 -> e [id=2 particle=scalar_2]
4153 A -> B [id=1 lmb_id=0]
4154 A -> B [id=0]
4155 },
4156 "scalars"
4157 )
4158 .unwrap();
4159 for (graph, name, subspace_edge, expression) in [
4160 (&mut member_a, "member_a_one_loop", 0, "1"),
4161 (&mut member_b, "member_b_one_loop", 1, "2"),
4162 ] {
4163 graph.threshold_counterterms = Autogen::explicit(ThresholdCountertermSpec {
4164 schema_version: 1,
4165 function_map: Default::default(),
4166 cuts: vec![ThresholdCountertermCut {
4167 edges: Vec::new(),
4168 thresholds: vec![ThresholdCountertermThreshold {
4169 edges: vec![super::EdgeIndex(0), super::EdgeIndex(1)],
4170 counterterms: vec![ThresholdCountertermVariant {
4171 group_id: None,
4172 name: Some(name.to_string()),
4173 subspace: Some(vec![super::EdgeIndex(subspace_edge)]),
4174 parent_lmb: Some(vec![super::EdgeIndex(subspace_edge)]),
4175 disable: false,
4176 multiplier: Some(ThresholdCountertermMultiplier {
4177 expression: expression.to_string(),
4178 function_map: Default::default(),
4179 symmetrize: false,
4180 opaque_derivatives: true,
4181 }),
4182 }],
4183 }],
4184 }],
4185 });
4186 }
4187
4188 let generation_settings = GenerationSettings {
4189 threshold_subtraction: ThresholdSubtractionSettings {
4190 enable_thresholds: true,
4191 assume_positive_external_energies: false,
4192 check_esurface_at_generation: false,
4193 ..Default::default()
4194 },
4195 ..Default::default()
4196 };
4197 let mut runtime = RuntimeSettings {
4200 kinematics: KinematicsSettings {
4201 e_cm: 4.0,
4202 externals: Externals::Constant {
4203 momenta: vec![
4204 ExternalMomenta::Independent([F(4.0), F(0.0), F(0.0), F(0.0)]),
4205 ExternalMomenta::Dependent(Dep::Dep),
4206 ],
4207 helicities: vec![Helicity::ZERO; 2],
4208 improvement_settings: PhaseSpaceImprovementSettings::default(),
4209 f_64_cache: None,
4210 f_128_cache: None,
4211 arb_cache: Default::default(),
4212 },
4213 },
4214 ..RuntimeSettings::default()
4215 };
4216 runtime.general.generate_events = true;
4217 runtime.general.store_additional_weights_in_event = true;
4218 let pool = rayon::ThreadPoolBuilder::new()
4219 .num_threads(1)
4220 .build()
4221 .unwrap();
4222 let mut amplitude = super::Amplitude::from_graph_list(
4223 "grouped_threshold_ownership",
4224 vec![member_a, member_b],
4225 )
4226 .unwrap();
4227 assert_eq!(amplitude.graph_group_structure.len(), 1);
4228 assert_eq!(
4229 (&litude.graph_group_structure[super::GroupId(0)])
4230 .into_iter()
4231 .count(),
4232 2
4233 );
4234 amplitude
4235 .preprocess(&model, &generation_settings, &(&runtime).into(), &pool)
4236 .unwrap();
4237
4238 let expected = [
4239 ("grouped_threshold_member_a", "member_a_one_loop", 0, "1"),
4240 ("grouped_threshold_member_b", "member_b_one_loop", 1, "2"),
4241 ];
4242 for (graph, (_, name, subspace_edge, expression)) in
4243 amplitude.graphs.iter().zip(expected)
4244 {
4245 let resolved = graph
4246 .derived_data
4247 .resolved_threshold_counterterms
4248 .as_ref()
4249 .unwrap();
4250 assert!(!resolved.legacy_equivalent);
4251 assert_eq!(resolved.variants.len(), 2);
4255 assert_eq!(
4256 resolved
4257 .variants
4258 .keys()
4259 .map(|variant_id| variant_id.0)
4260 .collect::<Vec<_>>(),
4261 [0, 1]
4262 );
4263 for variant in &resolved.variants {
4264 assert_eq!(variant.name, name);
4265 assert_eq!(
4266 variant.requested_subspace,
4267 Some(vec![super::EdgeIndex(subspace_edge)])
4268 );
4269 assert_eq!(
4270 variant
4271 .subspace
4272 .iter_basis_edges(graph.derived_data.lmbs.as_ref().unwrap())
4273 .collect::<Vec<_>>(),
4274 vec![super::EdgeIndex(subspace_edge)]
4275 );
4276 assert_eq!(variant.multiplier.as_ref().unwrap().expression, expression);
4277 }
4278 }
4279
4280 for include_autogenerated_fields in [false, true] {
4281 let mut exported = String::new();
4282 amplitude
4283 .write_dot_fmt(
4284 &mut exported,
4285 &DotExportSettings {
4286 include_autogenerated_fields,
4287 ..DotExportSettings::default()
4288 },
4289 )
4290 .unwrap();
4291 let exported_graphs = Graph::from_string(&exported, &model).unwrap();
4292 assert_eq!(exported_graphs.len(), 2);
4293 for (graph, (graph_name, name, subspace_edge, expression)) in
4294 exported_graphs.iter().zip(expected)
4295 {
4296 assert_eq!(graph.name, graph_name);
4297 let spec = &graph.threshold_counterterms;
4298 assert!(!spec.autogenerated);
4299 assert_eq!(spec.cuts.len(), 1);
4300 assert!(spec.cuts[0].edges.is_empty());
4301 let variants = &spec.cuts[0].thresholds[0].counterterms;
4302 assert_eq!(variants.len(), 1);
4303 assert_eq!(variants[0].name.as_deref(), Some(name));
4304 assert_eq!(
4305 variants[0].subspace,
4306 Some(vec![super::EdgeIndex(subspace_edge)])
4307 );
4308 assert_eq!(
4309 variants[0].multiplier.as_ref().unwrap().expression,
4310 expression
4311 );
4312 }
4313 }
4314
4315 amplitude
4316 .build_integrand(
4317 &model,
4318 "grouped_threshold_ownership",
4319 &GlobalSettings {
4320 generation: generation_settings.clone(),
4321 ..Default::default()
4322 },
4323 (&runtime).into(),
4324 &pool,
4325 )
4326 .unwrap();
4327 let integrand = amplitude.integrand.as_mut().unwrap();
4328 integrand.warm_up(&model).unwrap();
4329
4330 let inspect_ownership = |integrand: &ProcessIntegrand| {
4331 let ProcessIntegrand::Amplitude(integrand) = integrand else {
4332 panic!("grouped amplitude fixture built a non-amplitude integrand")
4333 };
4334 assert_eq!(integrand.data.graph_group_structure.len(), 1);
4335 assert_eq!(
4336 (&integrand.data.graph_group_structure[super::GroupId(0)])
4337 .into_iter()
4338 .count(),
4339 2
4340 );
4341 for (term, (graph_name, name, subspace_edge, expression)) in
4342 integrand.data.graph_terms.iter().zip(expected)
4343 {
4344 assert_eq!(term.graph.name, graph_name);
4345 let counterterm = &term.threshold_counterterm;
4346 assert!(!counterterm.legacy_equivalent);
4347 assert_eq!(counterterm.variant_metadata.len(), 2);
4348 for (variant_id, variant) in counterterm.variant_metadata.iter_enumerated()
4349 {
4350 assert!(variant_id.0 < 2);
4351 assert_eq!(variant.name, name);
4352 assert_eq!(
4353 variant.requested_subspace,
4354 Some(vec![super::EdgeIndex(subspace_edge)])
4355 );
4356 }
4357 let multipliers = counterterm.threshold_multipliers.as_ref().unwrap();
4358 assert_eq!(multipliers.left_variants().len(), 2);
4359 for (variant_id, reference) in
4360 multipliers.left_variants().iter().enumerate()
4361 {
4362 assert_eq!(reference.variant_id.0, variant_id);
4363 assert_eq!(reference.evaluator_id.unwrap().0, 0);
4364 }
4365 let registry = counterterm.metadata_registry.as_ref().unwrap();
4366 assert_eq!(registry.graph_name, graph_name);
4367 assert_eq!(registry.variants.len(), 2);
4368 for (variant_id, variant) in registry.variants.iter().enumerate() {
4369 assert_eq!(variant.variant_id, variant_id);
4370 assert_eq!(variant.name, name);
4371 assert_eq!(variant.resolved_subspace, [subspace_edge]);
4372 }
4373 assert_eq!(registry.evaluators.len(), 1);
4374 assert_eq!(registry.evaluators[0].evaluator_id, 0);
4375 assert_eq!(registry.evaluators[0].variant_ids, [0, 1]);
4376 assert_eq!(registry.evaluators[0].expression, expression);
4377 assert!(registry.components.iter().all(|component| {
4378 component.variant_ids.len() == 1
4379 && component.variant_ids[0] < 2
4380 && component.evaluator_ids == [Some(0)]
4381 }));
4382 }
4383 };
4384 inspect_ownership(integrand);
4385
4386 let evaluate_members = |integrand: &mut ProcessIntegrand| {
4387 (0..2)
4388 .map(|graph_id| {
4389 let registry = match integrand {
4390 ProcessIntegrand::Amplitude(amplitude_integrand) => {
4391 amplitude_integrand.data.graph_terms[graph_id]
4392 .threshold_counterterm
4393 .metadata_registry
4394 .clone()
4395 .unwrap()
4396 }
4397 ProcessIntegrand::CrossSection(_) => unreachable!(),
4398 };
4399 let result = integrand
4400 .evaluate_momentum_configuration(
4401 &model,
4402 &MomentumSpaceEvaluationInput {
4403 loop_momenta: vec![crate::momentum::ThreeMomentum::new(
4404 F(0.35),
4405 F(-0.2),
4406 F(0.45),
4407 )],
4408 integrator_weight: F(1.0),
4409 graph_id: Some(graph_id),
4410 group_id: None,
4411 orientation: None,
4412 channel_id: None,
4413 },
4414 false,
4415 )
4416 .unwrap();
4417 let events = result
4418 .event_groups
4419 .iter()
4420 .flat_map(|group| group.iter())
4421 .collect::<Vec<_>>();
4422 assert_eq!(events.len(), 1);
4423 let event = events[0];
4424 let decomposition = event
4425 .additional_weights
4426 .threshold_counterterms
4427 .as_ref()
4428 .expect("each grouped member must record its own decomposition");
4429 assert_eq!(decomposition.total(), event.weight);
4430 assert!(!decomposition.components.is_empty());
4431 let expected_multiplier = F(if graph_id == 0 { 1.0 } else { 2.0 });
4432 for component in &decomposition.components {
4433 let metadata = ®istry.components[component.component_id];
4434 assert_eq!(metadata.variant_ids.len(), 1);
4435 assert!(metadata.variant_ids[0] < 2);
4436 assert_eq!(metadata.evaluator_ids, [Some(0)]);
4437 assert_eq!(
4438 component.multiplier_values.as_slice(),
4439 [expected_multiplier]
4440 );
4441 assert_eq!(component.effective_multiplier, expected_multiplier);
4442 }
4443 (
4444 event.weight,
4445 decomposition
4446 .components
4447 .iter()
4448 .map(|component| {
4449 (
4450 component.component_id,
4451 component.multiplier_values.clone(),
4452 component.effective_multiplier,
4453 component.bare,
4454 component.weighted,
4455 component.evaluation_skipped,
4456 )
4457 })
4458 .collect::<Vec<_>>(),
4459 )
4460 })
4461 .collect::<Vec<_>>()
4462 };
4463 let before_save = evaluate_members(integrand);
4464
4465 let unique = SystemTime::now()
4466 .duration_since(UNIX_EPOCH)
4467 .unwrap()
4468 .as_nanos();
4469 let save_root = std::env::temp_dir().join(format!(
4470 "gammalooprs-grouped-amplitude-threshold-{}-{unique}",
4471 std::process::id(),
4472 ));
4473 amplitude.save(&save_root, true).unwrap();
4474 let mut state_bytes = Vec::new();
4475 State::export(&mut state_bytes).unwrap();
4476 let state_map = State::import(&mut Cursor::new(state_bytes), None).unwrap();
4477 let context = GammaLoopContextContainer {
4478 model: &model,
4479 state_map: &state_map,
4480 };
4481 let mut loaded =
4482 super::Amplitude::load(save_root.join("grouped_threshold_ownership"), context)
4483 .unwrap();
4484 let loaded_integrand = loaded.integrand.as_mut().unwrap();
4485 loaded_integrand.warm_up(&model).unwrap();
4486 inspect_ownership(loaded_integrand);
4487 let after_load = evaluate_members(loaded_integrand);
4488 assert_eq!(after_load, before_save);
4489 fs::remove_dir_all(&save_root).unwrap();
4490 })
4491 .expect("grouped-amplitude threshold ownership test thread must start")
4492 .join()
4493 .expect("grouped-amplitude threshold ownership test must finish successfully");
4494 }
4495
4496 #[test]
4497 fn raised_amplitude_constituents_reject_different_group_ids() {
4498 test_initialise().unwrap();
4499 let model = crate::model::Model::from_file(
4500 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
4501 .join("../../assets/models/json/scalars/scalars_2p_3p.json"),
4502 )
4503 .unwrap();
4504 let mut graph = AmplitudeGraph::new(
4505 Graph::from_string(
4506 include_str!("../../../../tests/resources/graphs/dotted_bubble_amp.dot"),
4507 &model,
4508 )
4509 .unwrap()
4510 .remove(0),
4511 );
4512 graph.graph.threshold_counterterms = Autogen::explicit(ThresholdCountertermSpec {
4516 schema_version: 1,
4517 function_map: Default::default(),
4518 cuts: vec![ThresholdCountertermCut {
4519 edges: Vec::new(),
4520 thresholds: [1, 2]
4521 .into_iter()
4522 .enumerate()
4523 .map(|(group_id, edge)| ThresholdCountertermThreshold {
4524 edges: vec![super::EdgeIndex(edge), super::EdgeIndex(3)],
4525 counterterms: vec![ThresholdCountertermVariant {
4526 name: Some("shared_raised_variant".to_string()),
4527 group_id: Some(group_id),
4528 subspace: Some(vec![super::EdgeIndex(3)]),
4529 parent_lmb: Some(vec![super::EdgeIndex(3)]),
4530 disable: false,
4531 multiplier: None,
4532 }],
4533 })
4534 .collect(),
4535 }],
4536 });
4537 let settings = GenerationSettings {
4538 threshold_subtraction: ThresholdSubtractionSettings {
4539 enable_thresholds: true,
4540 assume_positive_external_energies: false,
4541 check_esurface_at_generation: false,
4542 ..Default::default()
4543 },
4544 ..Default::default()
4545 };
4546 graph.generate_cff(&settings).unwrap();
4547 graph.build_lmbs();
4548 let raised_data = graph.graph.determine_raised_esurfaces_from_expression(
4549 &graph
4550 .derived_data
4551 .cff_expression
4552 .as_ref()
4553 .unwrap()
4554 .expression,
4555 );
4556 let error = graph
4557 .resolve_amplitude_threshold_counterterm_directives(
4558 &raised_data,
4559 &settings,
4560 &(&RuntimeSettings::default()).into(),
4561 &model,
4562 )
4563 .unwrap_err()
4564 .to_string();
4565 for detail in [
4566 "Constituents of one raised residue",
4567 "[EdgeIndex(1), EdgeIndex(3)]",
4568 "[EdgeIndex(2), EdgeIndex(3)]",
4569 "group_id=Some(0)",
4570 "group_id=Some(1)",
4571 "shared_raised_variant",
4572 ] {
4573 assert!(error.contains(detail), "missing {detail}: {error}");
4574 }
4575 }
4576
4577 #[test]
4578 fn generalized_amplitude_directive_preserves_raised_threshold_order_and_components() {
4579 std::thread::Builder::new()
4580 .name("generalized-raised-amplitude-threshold-test".to_string())
4581 .stack_size(32 * 1024 * 1024)
4582 .spawn(|| {
4583 test_initialise().unwrap();
4584 let model = crate::model::Model::from_file(
4585 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
4586 .join("../../assets/models/json/scalars/scalars_2p_3p.json"),
4587 )
4588 .unwrap();
4589 let mut graph = Graph::from_string(
4590 include_str!("../../../../tests/resources/graphs/dotted_bubble_amp.dot"),
4591 &model,
4592 )
4593 .unwrap()
4594 .remove(0);
4595 let mut generation_settings = GenerationSettings {
4596 threshold_subtraction: ThresholdSubtractionSettings {
4597 enable_thresholds: true,
4598 assume_positive_external_energies: false,
4599 check_esurface_at_generation: false,
4600 ..Default::default()
4601 },
4602 ..Default::default()
4603 };
4604 generation_settings
4605 .tropical_subgraph_table
4606 .disable_tropical_generation = true;
4607
4608 let mut legacy_graph = AmplitudeGraph::new(graph.clone());
4609 legacy_graph
4610 .preprocess(
4611 &model,
4612 &generation_settings,
4613 &(&RuntimeSettings::default()).into(),
4614 )
4615 .unwrap();
4616 let expected_thresholds = [
4617 vec![super::EdgeIndex(1), super::EdgeIndex(3)],
4618 vec![super::EdgeIndex(2), super::EdgeIndex(3)],
4619 ];
4620 let legacy_resolved = legacy_graph
4621 .derived_data
4622 .resolved_threshold_counterterms
4623 .as_ref()
4624 .unwrap();
4625 assert_eq!(legacy_resolved.variants.len(), 2);
4626 for variant in &legacy_resolved.variants {
4627 assert_eq!(variant.raised_esurface_group.max_occurence, 2);
4628 assert_eq!(
4629 variant
4630 .associations
4631 .iter()
4632 .map(|association| association.threshold_edges.clone())
4633 .collect::<Vec<_>>(),
4634 expected_thresholds
4635 );
4636 assert_eq!(variant.subspace_loop_count, 1);
4637 assert_eq!(
4638 variant
4639 .subspace
4640 .iter_basis_edges(legacy_graph.derived_data.lmbs.as_ref().unwrap())
4641 .collect::<Vec<_>>(),
4642 [super::EdgeIndex(3)]
4643 );
4644 }
4645
4646 let explicit_variant = ThresholdCountertermVariant {
4647 group_id: None,
4648 name: Some("raised_one_loop".to_string()),
4649 subspace: Some(vec![super::EdgeIndex(3)]),
4650 parent_lmb: Some(vec![super::EdgeIndex(3)]),
4651 disable: false,
4652 multiplier: Some(ThresholdCountertermMultiplier {
4653 expression: "2".to_string(),
4654 function_map: Default::default(),
4655 symmetrize: false,
4656 opaque_derivatives: true,
4657 }),
4658 };
4659 graph.threshold_counterterms = Autogen::explicit(ThresholdCountertermSpec {
4660 schema_version: 1,
4661 function_map: Default::default(),
4662 cuts: vec![ThresholdCountertermCut {
4663 edges: Vec::new(),
4664 thresholds: expected_thresholds
4665 .iter()
4666 .map(|edges| ThresholdCountertermThreshold {
4667 edges: edges.clone(),
4668 counterterms: vec![explicit_variant.clone()],
4669 })
4670 .collect(),
4671 }],
4672 });
4673 let mut runtime = RuntimeSettings {
4674 kinematics: KinematicsSettings {
4675 e_cm: 4.0,
4676 externals: Externals::Constant {
4677 momenta: vec![
4678 ExternalMomenta::Independent([
4679 F(4.0),
4680 F(0.0),
4681 F(0.0),
4682 F(0.0),
4683 ]),
4684 ExternalMomenta::Dependent(Dep::Dep),
4685 ],
4686 helicities: vec![Helicity::ZERO; 2],
4687 improvement_settings: PhaseSpaceImprovementSettings::default(),
4688 f_64_cache: None,
4689 f_128_cache: None,
4690 arb_cache: Default::default(),
4691 },
4692 },
4693 ..RuntimeSettings::default()
4694 };
4695 runtime.general.generate_events = true;
4696 runtime.general.store_additional_weights_in_event = true;
4697 let pool = rayon::ThreadPoolBuilder::new()
4698 .num_threads(1)
4699 .build()
4700 .unwrap();
4701 let mut amplitude = super::Amplitude::from_graph_list(
4702 "generalized_raised_amplitude_threshold",
4703 vec![graph],
4704 )
4705 .unwrap();
4706 amplitude
4707 .preprocess(&model, &generation_settings, &(&runtime).into(), &pool)
4708 .unwrap();
4709 let resolved = amplitude.graphs[0]
4710 .derived_data
4711 .resolved_threshold_counterterms
4712 .as_ref()
4713 .unwrap();
4714 assert!(!resolved.legacy_equivalent);
4715 assert_eq!(resolved.variants.len(), 2);
4716 for variant in &resolved.variants {
4717 assert_eq!(variant.name, "raised_one_loop");
4718 assert_eq!(variant.raised_esurface_group.max_occurence, 2);
4719 assert_eq!(variant.subspace_loop_count, 1);
4720 assert_eq!(variant.multiplier.as_ref().unwrap().expression, "2");
4721 }
4722 for symbolic in &litude.graphs[0]
4723 .derived_data
4724 .threshold_counterterm_variants
4725 {
4726 let orders = symbolic
4727 .atom
4728 .parametric
4729 .iter()
4730 .filter_map(|(index, _)| index.left_threshold_order)
4731 .collect::<std::collections::BTreeSet<_>>();
4732 assert_eq!(orders, [1, 2].into_iter().collect::<std::collections::BTreeSet<_>>());
4733 }
4734
4735 amplitude
4736 .build_integrand(
4737 &model,
4738 "generalized_raised_amplitude_threshold",
4739 &GlobalSettings {
4740 generation: generation_settings.clone(),
4741 ..Default::default()
4742 },
4743 (&runtime).into(),
4744 &pool,
4745 )
4746 .unwrap();
4747 let inspect_raised_runtime = |integrand: &ProcessIntegrand| {
4748 let ProcessIntegrand::Amplitude(integrand) = integrand else {
4749 panic!("raised amplitude fixture built a non-amplitude integrand")
4750 };
4751 let counterterm = &integrand.data.graph_terms[0].threshold_counterterm;
4752 assert!(!counterterm.legacy_equivalent);
4753 assert_eq!(counterterm.variant_evaluators.len(), 2);
4754 for (variant_id, evaluator) in
4755 counterterm.variant_evaluators.iter_enumerated()
4756 {
4757 assert_eq!(
4758 evaluator
4759 .evaluator_stacks
4760 .keys()
4761 .filter_map(|index| index.left_threshold_order)
4762 .collect::<std::collections::BTreeSet<_>>(),
4763 [1, 2].into_iter().collect::<std::collections::BTreeSet<_>>()
4764 );
4765 let helpers = &counterterm.variant_helper_evaluators[variant_id];
4766 assert_eq!(helpers.len(), 2);
4767 assert!(helpers.iter().all(|helper| helper.exprs_len == 2));
4768 }
4769
4770 let registry = counterterm.metadata_registry.as_ref().unwrap();
4771 assert_eq!(registry.variants.len(), 2);
4772 assert!(registry.variants.iter().all(|variant| {
4773 variant.name == "raised_one_loop"
4774 && variant.subspace_loop_count == 1
4775 && variant.resolved_subspace == [3]
4776 }));
4777 assert_eq!(registry.evaluators.len(), 1);
4778 assert_eq!(registry.evaluators[0].variant_ids, [0, 1]);
4779 assert_eq!(registry.evaluators[0].expression, "2");
4780 assert_eq!(registry.components.len(), 4);
4781 for variant_id in 0..2 {
4782 assert_eq!(
4783 registry
4784 .components
4785 .iter()
4786 .filter(|component| component.variant_ids == [variant_id])
4787 .map(|component| component.kind)
4788 .collect::<std::collections::BTreeSet<_>>(),
4789 [
4790 ThresholdCountertermComponentKind::Local,
4791 ThresholdCountertermComponentKind::Integrated,
4792 ]
4793 .into_iter()
4794 .collect::<std::collections::BTreeSet<_>>()
4795 );
4796 }
4797 registry.clone()
4798 };
4799 let registry_before_save =
4800 inspect_raised_runtime(amplitude.integrand.as_ref().unwrap());
4801
4802 let evaluate_raised = |integrand: &mut ProcessIntegrand| {
4803 let result = integrand
4804 .evaluate_momentum_configuration(
4805 &model,
4806 &MomentumSpaceEvaluationInput {
4807 loop_momenta: vec![crate::momentum::ThreeMomentum::new(
4808 F(0.35),
4809 F(-0.2),
4810 F(0.45),
4811 )],
4812 integrator_weight: F(1.0),
4813 graph_id: Some(0),
4814 group_id: None,
4815 orientation: None,
4816 channel_id: None,
4817 },
4818 false,
4819 )
4820 .unwrap();
4821 let events = result
4822 .event_groups
4823 .iter()
4824 .flat_map(|group| group.iter())
4825 .collect::<Vec<_>>();
4826 assert_eq!(events.len(), 1);
4827 let event = events[0];
4828 let decomposition = event
4829 .additional_weights
4830 .threshold_counterterms
4831 .as_ref()
4832 .expect("raised generalized threshold must record completed L/I pieces");
4833 assert_eq!(decomposition.total(), event.weight);
4834 assert_eq!(decomposition.components.len(), 2);
4835 assert_eq!(
4836 decomposition
4837 .components
4838 .iter()
4839 .map(|component| {
4840 registry_before_save.components[component.component_id].kind
4841 })
4842 .collect::<std::collections::BTreeSet<_>>(),
4843 [
4844 ThresholdCountertermComponentKind::Local,
4845 ThresholdCountertermComponentKind::Integrated,
4846 ]
4847 .into_iter()
4848 .collect::<std::collections::BTreeSet<_>>()
4849 );
4850 for component in &decomposition.components {
4851 let metadata = ®istry_before_save.components[component.component_id];
4852 assert_eq!(metadata.variant_ids.len(), 1);
4853 assert!(metadata.variant_ids[0] < 2);
4854 assert_eq!(metadata.evaluator_ids, [Some(0)]);
4855 assert_eq!(component.multiplier_values.as_slice(), [F(2.0)]);
4856 assert_eq!(component.effective_multiplier, F(2.0));
4857 assert!(component.bare.is_some());
4858 assert!(!component.evaluation_skipped);
4859 let crate::observables::ThresholdCountertermComponentOccurrence::Amplitude {
4860 raised_esurface_id,
4861 overlap_group,
4862 } = &component.occurrence
4863 else {
4864 panic!("amplitude fixture recorded a non-amplitude occurrence")
4865 };
4866 assert!(event.additional_weights.weights.contains_key(
4867 &crate::observables::AdditionalWeightKey::AmplitudeThresholdCountertermVariant {
4868 variant_id: metadata.variant_ids[0],
4869 esurface_id: *raised_esurface_id,
4870 overlap_group: *overlap_group,
4871 },
4872 ));
4873 }
4874 (
4875 event.weight,
4876 decomposition.original,
4877 decomposition
4878 .components
4879 .iter()
4880 .map(|component| {
4881 (
4882 component.component_id,
4883 component.occurrence.clone(),
4884 component.multiplier_values.clone(),
4885 component.effective_multiplier,
4886 component.bare,
4887 component.weighted,
4888 component.evaluation_skipped,
4889 )
4890 })
4891 .collect::<Vec<_>>(),
4892 )
4893 };
4894 let integrand = amplitude.integrand.as_mut().unwrap();
4895 integrand.warm_up(&model).unwrap();
4896 let before_save = evaluate_raised(integrand);
4897
4898 let unique = SystemTime::now()
4899 .duration_since(UNIX_EPOCH)
4900 .unwrap()
4901 .as_nanos();
4902 let save_root = std::env::temp_dir().join(format!(
4903 "gammalooprs-raised-amplitude-threshold-{}-{unique}",
4904 std::process::id(),
4905 ));
4906 amplitude.save(&save_root, true).unwrap();
4907 let mut state_bytes = Vec::new();
4908 State::export(&mut state_bytes).unwrap();
4909 let state_map = State::import(&mut Cursor::new(state_bytes), None).unwrap();
4910 let context = GammaLoopContextContainer {
4911 model: &model,
4912 state_map: &state_map,
4913 };
4914 let mut loaded = super::Amplitude::load(
4915 save_root.join("generalized_raised_amplitude_threshold"),
4916 context,
4917 )
4918 .unwrap();
4919 let loaded_integrand = loaded.integrand.as_mut().unwrap();
4920 assert_eq!(
4921 inspect_raised_runtime(loaded_integrand),
4922 registry_before_save
4923 );
4924 loaded_integrand.warm_up(&model).unwrap();
4925 assert_eq!(evaluate_raised(loaded_integrand), before_save);
4926 fs::remove_dir_all(&save_root).unwrap();
4927 })
4928 .unwrap()
4929 .join()
4930 .unwrap();
4931 }
4932
4933 #[test]
4934 fn amplitude_tree() {
4935 test_initialise().unwrap();
4936 let mut graph: AmplitudeGraph = dot!(digraph qqx_aaa_tree_1 {
4937 num="spenso::g(spenso::dind(spenso::cof(3, hedge(1))), spenso::cof(3, hedge(2)))/3"
4938 ext [style=invis]
4939 ext -> v1:1 [particle="d" id=1];
4940 ext -> v3:2 [particle="d~" id=2];
4941 v1:3 -> ext [particle="a" id=3];
4942 v2:4 -> ext [particle="a" id=4];
4943 v3:0 -> ext [particle="a" id=0];
4944 v1 -> v2 [particle="d" id=5];
4945 v2 -> v3 [particle="d" id=6];
4946 })
4947 .unwrap();
4948
4949 let _model = load_generic_model("sm");
4950
4951 graph.generate_cff(&GenerationSettings::default()).unwrap();
4952 let param_builder = &graph.graph.param_builder;
4955 println!("{param_builder}");
4956
4957 }
4967
4968 #[test]
4969 fn uncut_amplitude_generation_has_one_residue_for_both_uv_backends() {
4970 test_initialise().unwrap();
4971 let mut amplitude_graph: AmplitudeGraph = dot!(
4972 digraph bub {
4973 edge [particle=scalar_1]
4974 node [num=1]
4975 e [style=invis]
4976 e -> A:0 [id=3]
4977 B:1 -> e [id=2]
4978 A -> B [id=1]
4979 A -> B [id=0]
4980 },
4981 "scalars"
4982 )
4983 .unwrap();
4984 let generation_settings = GenerationSettings::default();
4985 amplitude_graph.generate_cff(&generation_settings).unwrap();
4986 let production = amplitude_graph
4987 .derived_data
4988 .cff_expression
4989 .as_ref()
4990 .unwrap();
4991 let options = amplitude_graph
4992 .graph
4993 .production_cff_3d_expression_options(&generation_settings)
4994 .unwrap();
4995
4996 for orchestrator in [UVOrchestrator::LegacyDagForest, UVOrchestrator::HedgePoset] {
4997 let mut graph = amplitude_graph.graph.clone();
4998 let cut_structure = CutStructure::empty(&graph);
4999 assert_eq!(cut_structure.cuts.len(), 1);
5000 let expected_cut = cut_structure.cuts[0].clone();
5001 let settings = GenerationSettings {
5002 uv: UVgenerationSettings {
5003 generate_integrated: false,
5004 softct: false,
5005 orchestrator,
5006 ..Default::default()
5007 },
5008 ..Default::default()
5009 };
5010 let generated = orchestrator
5011 .parametric_integrands(
5012 &mut graph,
5013 cut_structure,
5014 crate::utils::vakint().unwrap(),
5015 OrientationProjection::exact_expression(
5016 production,
5017 &options,
5018 &generation_settings.orientation_pattern,
5019 generation_settings.explicit_orientation_sum_only,
5020 ),
5021 &settings,
5022 )
5023 .unwrap();
5024
5025 assert_eq!(generated.len(), 1, "{orchestrator}");
5026 assert_eq!(generated[0].cuts, expected_cut, "{orchestrator}");
5027 let residues = generated[0].integrands.iter().collect::<Vec<_>>();
5028 assert_eq!(residues.len(), 1, "{orchestrator}");
5029 assert_eq!(*residues[0].0, CutCFFIndex::new_all_none());
5030 }
5031 }
5032
5033 #[test]
5034 fn generation_orientation_pattern_filters_evaluator_orientations() {
5035 test_initialise().unwrap();
5036 let mut graph: AmplitudeGraph = dot!(
5037 digraph bub {
5038 edge [particle=scalar_1]
5039 node [num=1]
5040 e [style=invis]
5041 e -> A:0 [id=3]
5042 B:1 -> e [id=2]
5043 A -> B [id=1]
5044 A -> B [id=0]
5045 },
5046 "scalars"
5047 )
5048 .unwrap();
5049
5050 let model = load_generic_model("scalars");
5051 let generation_settings = GenerationSettings {
5052 threshold_subtraction: ThresholdSubtractionSettings {
5053 enable_thresholds: false,
5054 ..Default::default()
5055 },
5056 ..Default::default()
5057 };
5058 let runtime_settings = RuntimeSettings::default();
5059 graph
5060 .preprocess(&model, &generation_settings, &(&runtime_settings).into())
5061 .unwrap();
5062
5063 assert!(
5064 graph
5065 .derived_data
5066 .cff_expression
5067 .as_ref()
5068 .unwrap()
5069 .expression
5070 .orientations
5071 .len()
5072 > 1
5073 );
5074
5075 let global_settings = GlobalSettings {
5076 generation: GenerationSettings {
5077 orientation_pattern: OrientationPattern::from_orientation(
5078 &graph
5079 .derived_data
5080 .cff_expression
5081 .as_ref()
5082 .unwrap()
5083 .expression
5084 .orientations[OrientationID(0)],
5085 ),
5086 threshold_subtraction: ThresholdSubtractionSettings {
5087 enable_thresholds: false,
5088 ..Default::default()
5089 },
5090 ..Default::default()
5091 },
5092 ..Default::default()
5093 };
5094
5095 let (term, _stats) = AmplitudeGraphTerm::from_amplitude_graph(
5096 &graph,
5097 GraphGroupPosition(0),
5098 TiVec::new(),
5099 &model,
5100 &global_settings,
5101 )
5102 .unwrap();
5103
5104 assert_eq!(term.orientations.len(), 1);
5105 assert_eq!(
5106 term.orientations[OrientationID(0)],
5107 graph
5108 .derived_data
5109 .cff_expression
5110 .as_ref()
5111 .unwrap()
5112 .expression
5113 .orientations[OrientationID(0)]
5114 .data
5115 .orientation
5116 );
5117 }
5118}