1use std::{
2 collections::{BTreeMap, HashMap},
3 fmt,
4 fs::{self, File},
5 io::Write,
6 iter,
7 path::Path,
8};
9
10use ahash::AHashSet;
11use bincode_trait_derive::{Decode, Encode};
13use color_eyre::Result;
14use momtrop::SampleGenerator;
15
16use idenso::dirac::GammaSimplifier;
17use rayon::{
18 ThreadPool,
19 iter::{IndexedParallelIterator, IntoParallelRefMutIterator, ParallelIterator},
20};
21use spenso::algebra::complex::Complex;
22use tracing::{info_span, instrument};
23use tracing_indicatif::{span_ext::IndicatifSpanExt, style::ProgressStyle};
24use vakint::{EvaluationMethod, NumericalEvaluationResult, Vakint, vakint_symbol};
25
26use crate::{
27 GammaLoopContext, GammaLoopContextContainer,
28 cff::{
29 esurface::{GroupEsurfaceId, RaisedEsurfaceData, RaisedEsurfaceId},
30 expression::{CFFExpression, OrientationID},
31 },
32 graph::{
33 GraphGroup, GraphGroupPosition, GroupId, LMBext, LmbIndex, LoopMomentumBasis,
34 cuts::{CutSet, ResidueSelector},
35 },
36 integrands::process::{
37 GenericEvaluator, LmbMultiChannelingSetup,
38 amplitude::{AmplitudeGraphTerm, AmplitudeIntegrand, AmplitudeIntegrandData},
39 graph_to_group_id_for_group_structure,
40 },
41 model::ArcParticle,
42 momentum::{sample::ExternalIndex, signature::SignatureLike},
43 processes::{
44 DotExportSettings, EvaluatorSettings, GraphGenerationStats, GraphGroupSelectionPlan,
45 GraphGroupSelectionSpec, NamedGraphGenerationReport, StandaloneExportSettings,
46 build_derivative_structure_atom, params_for_derivative_order,
47 },
48 settings::{
49 GlobalSettings, RuntimeSettings, global::OrientationPattern, runtime::LockedRuntimeSettings,
50 },
51 subtraction::amplitude_counterterm::AmplitudeCountertermAtom,
52 utils::{F, GS, Length, W_},
53 uv::{
54 RenormalizationPart, UVgenerationSettings, UltravioletGraph,
55 approx::{CutStructure, OrientationProjection, integrated::to_vakint_integrand},
56 settings::VakintSettings,
57 },
58};
59use eyre::{Context, eyre};
60use itertools::Itertools;
61use linnet::{
62 half_edge::{
63 involution::{EdgeVec, Flow, HedgePair},
64 subgraph::{ModifySubSet, SuBitGraph, SubGraphLike, SubSetOps},
65 },
66 parser::DotGraph,
67};
68use spenso::shadowing::symbolica_utils::LogPrint;
69use symbolica::{atom::Var, prelude::*};
70use tracing::{debug, info};
71use typed_index_collections::{TiVec, ti_vec};
72
73use super::generation_progress::{self, GenerationProcessKind, GenerationProgressPhase};
74
75use crate::{
76 cff::esurface::EsurfaceID,
77 graph::{FeynmanGraph, Graph},
78 integrands::process::ProcessIntegrand,
79 model::Model,
80 settings::global::GenerationSettings,
81};
82
83use crate::graph::parse::complete_group_parsing;
84
85#[derive(Clone, Encode, Decode)]
86#[trait_decode(trait = GammaLoopContext)]
87pub struct Amplitude {
88 pub name: String,
89 pub integrand: Option<ProcessIntegrand>,
90 pub graphs: Vec<AmplitudeGraph>,
91 pub graph_group_structure: TiVec<GroupId, GraphGroup>,
92 pub external_particles: Vec<ArcParticle>,
93 pub external_signature: SignatureLike<ExternalIndex>,
94 pub group_derived_data: TiVec<GroupId, GroupDerivedData>,
95}
96
97#[derive(Clone, Encode, Decode)]
98#[trait_decode(trait = GammaLoopContext)]
99pub struct GroupDerivedData {
100 pub esurface_map: TiVec<GroupEsurfaceId, TiVec<GraphGroupPosition, Option<RaisedEsurfaceId>>>,
101 pub esurface_atoms: TiVec<GroupEsurfaceId, Atom>,
102}
103
104impl Amplitude {
105 pub fn plan_graph_group_selection(
106 &self,
107 spec: &GraphGroupSelectionSpec,
108 ) -> Result<GraphGroupSelectionPlan> {
109 spec.plan(&self.graph_group_structure, |graph_id| {
110 self.graphs.get(graph_id).map(|graph| &graph.graph)
111 })
112 }
113
114 pub fn validate_graph_group_selection_plan(
115 &self,
116 plan: &GraphGroupSelectionPlan,
117 ) -> Result<()> {
118 if !self.group_derived_data.is_empty()
119 && self.group_derived_data.len() != self.graph_group_structure.len()
120 {
121 return Err(eyre!(
122 "Amplitude '{}' has {} group-derived-data entries for {} graph groups.",
123 self.name,
124 self.group_derived_data.len(),
125 self.graph_group_structure.len()
126 ));
127 }
128
129 for &old_group_id in plan.retained_group_ids() {
130 plan.new_group_id_for_old(old_group_id).ok_or_else(|| {
131 eyre!(
132 "Selection plan is missing compact group id for old group {}.",
133 old_group_id.0
134 )
135 })?;
136 if old_group_id.0 >= self.graph_group_structure.len() {
137 return Err(eyre!(
138 "Selection plan refers to missing graph group {}.",
139 old_group_id.0
140 ));
141 }
142 let group = &self.graph_group_structure[old_group_id];
143 for old_graph_id in group {
144 if old_graph_id >= self.graphs.len() {
145 return Err(eyre!(
146 "Graph group {} refers to missing graph id {}.",
147 old_group_id.0,
148 old_graph_id
149 ));
150 }
151 }
152 let master = group.master();
153 if master >= self.graphs.len() {
154 return Err(eyre!(
155 "Graph group {} refers to missing master graph id {}.",
156 old_group_id.0,
157 master
158 ));
159 }
160 }
161
162 Ok(())
163 }
164
165 pub fn apply_graph_group_selection(&mut self, plan: &GraphGroupSelectionPlan) -> Result<()> {
166 self.validate_graph_group_selection_plan(plan)?;
167
168 let mut old_graph_to_new_group = vec![None; self.graphs.len()];
169 let mut old_graph_is_master = vec![false; self.graphs.len()];
170 for &old_group_id in plan.retained_group_ids() {
171 let new_group_id = plan.new_group_id_for_old(old_group_id).ok_or_else(|| {
172 eyre!(
173 "Selection plan is missing compact group id for old group {}.",
174 old_group_id.0
175 )
176 })?;
177 let group = &self.graph_group_structure[old_group_id];
178 for old_graph_id in group {
179 old_graph_to_new_group[old_graph_id] = Some(new_group_id);
180 }
181 old_graph_is_master[group.master()] = true;
182 }
183
184 let mut new_graphs = self
185 .graphs
186 .iter()
187 .cloned()
188 .enumerate()
189 .filter_map(|(old_graph_id, mut graph)| {
190 let new_group_id = old_graph_to_new_group[old_graph_id]?;
191 graph.graph.group_id = Some(new_group_id);
192 graph.graph.is_group_master = old_graph_is_master[old_graph_id];
193 if let Some(multi_channeling_setup) = &mut graph.derived_data.multi_channeling_setup
194 {
195 multi_channeling_setup.graph.group_id = Some(new_group_id);
196 multi_channeling_setup.graph.is_group_master =
197 old_graph_is_master[old_graph_id];
198 }
199 Some(graph)
200 })
201 .collect::<Vec<_>>();
202
203 let mut parsed_graphs = new_graphs
204 .iter()
205 .map(|graph| graph.graph.clone())
206 .collect::<Vec<_>>();
207 let new_graph_group_structure = complete_group_parsing(&mut parsed_graphs)?;
208 for (graph, parsed_graph) in new_graphs.iter_mut().zip(parsed_graphs) {
209 graph.graph.group_id = parsed_graph.group_id;
210 graph.graph.is_group_master = parsed_graph.is_group_master;
211 if let Some(multi_channeling_setup) = &mut graph.derived_data.multi_channeling_setup {
212 multi_channeling_setup.graph.group_id = graph.graph.group_id;
213 multi_channeling_setup.graph.is_group_master = graph.graph.is_group_master;
214 }
215 }
216
217 let new_group_derived_data = if self.group_derived_data.is_empty() {
218 TiVec::new()
219 } else {
220 plan.retained_group_ids()
221 .iter()
222 .map(|&old_group_id| self.group_derived_data[old_group_id].clone())
223 .collect::<TiVec<GroupId, _>>()
224 };
225
226 self.graphs = new_graphs;
227 self.graph_group_structure = new_graph_group_structure;
228 self.group_derived_data = new_group_derived_data;
229 self.integrand = None;
230 Ok(())
231 }
232
233 pub fn export_standalone(
234 &self,
235 path: impl AsRef<Path>,
236 settings: &StandaloneExportSettings,
237 ) -> Result<()> {
238 if let Some(integrand) = &self.integrand {
239 integrand.export_standalone(path, settings)?
240 } else {
241 return Err(eyre!(
242 "Cannot warm up amplitude {} without integrand",
243 self.name
244 ));
245 }
246
247 Ok(())
248 }
249
250 #[instrument(
251 skip_all,
252 fields(
253 amplitude.name = %self.name,
254 )
255 )]
256 pub(crate) fn warm_up(&mut self, model: &Model) -> Result<()> {
257 if let Some(integrand) = &mut self.integrand {
258 integrand.warm_up(model)
259 } else {
260 Err(eyre!(
261 "Cannot warm up amplitude {} without integrand",
262 self.name
263 ))
264 }
265 }
266
267 #[instrument(
268 skip_all,
269 fields(
270 path = %path.as_ref().display(),
271 )
272 )]
273 pub(crate) fn load(path: impl AsRef<Path>, context: GammaLoopContextContainer) -> Result<Self> {
274 let binary = fs::read(path.as_ref().join("amp.bin"))?;
275 let (mut amp, _): (Self, _) =
276 bincode::decode_from_slice_with_context(&binary, bincode::config::standard(), context)?;
277
278 if path.as_ref().join("integrand").exists() {
279 let integrand = AmplitudeIntegrand::load(path.as_ref().join("integrand"), context)?;
280 amp.integrand = Some(ProcessIntegrand::Amplitude(integrand));
281 }
282
283 Ok(amp)
284 }
285
286 #[instrument(
287 skip_all,
288 fields(
289 amplitude.name = %self.name,
290 )
291 )]
292 pub fn compile(
293 &mut self,
294 path: impl AsRef<Path>,
295 override_existing: bool,
296 thread_pool: &ThreadPool,
297 ) -> Result<Vec<NamedGraphGenerationReport>> {
298 info!("Compiling amplitude {}", self.name);
299 let p = path.as_ref().join(&self.name);
300
301 let r = fs::create_dir_all(&p).with_context(|| {
302 format!(
303 "Trying to create directory to save amplitude {}",
304 p.display()
305 )
306 });
307 if override_existing {
308 r?;
309 }
310 if let Some(integrand) = &mut self.integrand {
311 let compile_times = integrand.compile(&p, override_existing, thread_pool)?;
312 return Ok(compile_times
313 .into_iter()
314 .map(|(graph_name, duration)| NamedGraphGenerationReport {
315 integrand_name: self.name.clone(),
316 graph_name,
317 stats: GraphGenerationStats {
318 total_time: duration,
319 evaluator_compile_time: duration,
320 ..GraphGenerationStats::default()
321 },
322 })
323 .collect());
324 };
325 Ok(Vec::new())
326 }
327
328 #[instrument(
329 skip_all,
330 fields(
331 amplitude.name = %self.name,
332 path = %path.as_ref().display(),
333 )
334 )]
335 pub fn save(&mut self, path: impl AsRef<Path>, override_existing: bool) -> Result<()> {
336 let p = path.as_ref().join(&self.name);
337
338 let r = fs::create_dir_all(&p).with_context(|| {
339 format!(
340 "Trying to create directory to save amplitude {}",
341 p.display()
342 )
343 });
344 if override_existing {
345 r?;
346 }
347
348 let integrand = self.integrand.take();
349 if let Some(integrand) = &integrand {
350 integrand.save(&p, override_existing)?;
351 };
352
353 let binary = bincode::encode_to_vec(&(*self), bincode::config::standard())?;
354 if override_existing {
355 fs::write(p.join("amp.bin"), binary)?;
356 } else {
357 let mut file = File::create_new(p.join("amp.bin"))?;
358 file.write_all(&binary)?;
359 }
360
361 self.integrand = integrand;
362 Ok(())
363 }
364
365 #[instrument(
366 skip_all,
367 fields(
368 amplitude.name = %self.name,
369 )
370 )]
371 pub fn preprocess(
372 &mut self,
373 model: &Model,
374 settings: &GenerationSettings,
375 locked_runtime_settings: &LockedRuntimeSettings,
376 thread_pool: &ThreadPool,
377 ) -> Result<Vec<NamedGraphGenerationReport>> {
378 let integrand_name = self.name.clone();
380
381 let preprocess_span = if generation_progress::detailed_progress_enabled() {
382 let span = info_span!("Preprocessing graphs", indicatif.pb_show = true);
383 span.pb_set_style(&ProgressStyle::with_template(
384 "{wide_bar} {pos}/{len} {msg}",
385 )?);
386 span.pb_set_length(self.graphs.len() as u64);
387 span.pb_set_message("Preprocessing graphs");
388 Some(span)
389 } else {
390 None
391 };
392 let preprocess_span_enter = preprocess_span.as_ref().map(|span| span.enter());
393
394 let preprocess_reports = thread_pool.install(|| {
395 let parent = preprocess_span.clone();
396 self.graphs
397 .par_iter_mut()
398 .map(|amplitude_graph| {
399 if crate::is_interrupted() {
400 return Err(eyre!("Generation interrupted by user"));
401 }
402 let graph_name = amplitude_graph.graph.name.clone();
403 generation_progress::graph_started(
404 GenerationProcessKind::Amplitude,
405 &integrand_name,
406 &graph_name,
407 None,
408 );
409 let _guard = parent.as_ref().map(|span| span.enter());
410 let stats =
411 amplitude_graph.preprocess(model, settings, locked_runtime_settings);
412 if let Some(span) = &parent {
413 span.pb_inc(1);
414 }
415
416 let stats = stats?;
417 if crate::is_interrupted() {
418 return Err(eyre!("Generation interrupted by user"));
419 }
420 generation_progress::graph_finished(
421 GenerationProcessKind::Amplitude,
422 &integrand_name,
423 &graph_name,
424 &stats,
425 None,
426 );
427
428 Ok(NamedGraphGenerationReport {
429 integrand_name: integrand_name.clone(),
430 graph_name,
431 stats,
432 })
433 })
434 .collect::<Result<Vec<_>>>()
435 })?;
436
437 drop(preprocess_span_enter);
438 drop(preprocess_span);
439
440 self.generate_grouped_derived_data()?;
441
442 Ok(preprocess_reports)
443 }
444
445 #[instrument(
446 skip_all,
447 fields(
448 amplitude.name = %self.name,
449 )
450 )]
451 pub fn build_integrand(
452 &mut self,
453 model: &Model,
454 process_name: &str,
455 global_settings: &GlobalSettings,
456 runtime_default: LockedRuntimeSettings,
457 thread_pool: &ThreadPool,
458 ) -> Result<Vec<NamedGraphGenerationReport>> {
459 let started = std::time::Instant::now();
460 crate::debug_tags!(#generation, #profile, #graph, #summary;
461 stage = "amplitude_build_integrand_start",
462 integrand = %self.name,
463 graph_count = self.graphs.len(),
464 "Generation timing milestone"
465 );
466 if crate::is_interrupted() {
467 return Err(eyre!("Generation interrupted by user"));
468 }
469 let integrand_name = self.name.clone();
470 generation_progress::begin_phase(
471 GenerationProgressPhase::GraphGeneration,
472 GenerationProcessKind::Amplitude,
473 process_name,
474 &integrand_name,
475 self.graphs.len(),
476 None,
477 );
478 let mut graph_reports = Vec::new();
479 let terms: Vec<_> = thread_pool.install(|| {
480 self.graphs
481 .par_iter_mut()
482 .enumerate()
483 .map(|(graph_id, graph)| {
484 if crate::is_interrupted() {
485 return Err(eyre!("Generation interrupted by user"));
486 }
487 let graph_started = std::time::Instant::now();
488 let group_id = graph.graph.group_id.unwrap(); let esurface_map = &self.group_derived_data[group_id].esurface_map;
490 let group_pos = self.graph_group_structure[group_id]
491 .find_position(graph_id)
492 .unwrap();
493
494 crate::debug_tags!(#generation, #profile, #graph, #summary;
495 stage = "amplitude_generate_term_for_graph_start",
496 integrand = %integrand_name,
497 graph = %graph.graph.name,
498 graph_id,
499 group_id = %group_id.0,
500 "Generation timing milestone"
501 );
502 generation_progress::graph_started(
503 GenerationProcessKind::Amplitude,
504 &integrand_name,
505 &graph.graph.name,
506 None,
507 );
508 let _progress_context_guard =
509 generation_progress::enter_progress_context(graph.graph.name.clone());
510 let (term, mut stats) = graph.generate_term_for_graph(
511 model,
512 group_pos,
513 esurface_map.clone(),
514 global_settings,
515 )?;
516 if crate::is_interrupted() {
517 return Err(eyre!("Generation interrupted by user"));
518 }
519 stats.evaluator_count = term.generic_evaluator_count();
520 stats.total_time += graph_started.elapsed();
521 crate::debug_tags!(#generation, #profile, #graph, #summary;
522 stage = "amplitude_generate_term_for_graph_done",
523 integrand = %integrand_name,
524 graph = %graph.graph.name,
525 graph_id,
526 group_id = %group_id.0,
527 elapsed_ms = graph_started.elapsed().as_secs_f64() * 1000.0,
528 "Generation timing milestone"
529 );
530 generation_progress::graph_finished(
531 GenerationProcessKind::Amplitude,
532 &integrand_name,
533 &graph.graph.name,
534 &stats,
535 None,
536 );
537 Ok((
538 term,
539 NamedGraphGenerationReport {
540 integrand_name: integrand_name.clone(),
541 graph_name: graph.graph.name.clone(),
542 stats,
543 },
544 ))
545 })
546 .collect::<Result<Vec<_>>>()
547 })?;
548 if crate::is_interrupted() {
549 return Err(eyre!("Generation interrupted by user"));
550 }
551 for (_, report) in &terms {
552 graph_reports.push(report.clone());
553 }
554 let mut terms = terms.into_iter().map(|(term, _)| term).collect::<Vec<_>>();
555
556 for group in self.graph_group_structure.iter() {
557 let master_graph_id = group.master();
558 let mc_of_master = self.graphs[master_graph_id]
559 .derived_data
560 .multi_channeling_setup
561 .as_ref()
562 .unwrap();
563 let master_graph = &self.graphs[master_graph_id].graph;
564 let master_external_signature = master_graph.get_external_signature();
565 let master_external_pdgs = master_graph
566 .get_external_partcles()
567 .into_iter()
568 .map(|particle| particle.pdg_code)
569 .collect_vec();
570
571 for graph_id in group.into_iter() {
572 terms[graph_id].multi_channeling_setup = mc_of_master.clone();
573 terms[graph_id].master_external_signature = master_external_signature.clone();
574 terms[graph_id].master_external_pdgs = master_external_pdgs.clone();
575 }
576 }
577
578 let backend_started = std::time::Instant::now();
579 let graph_count = terms.len();
580 crate::debug_tags!(#generation, #profile, #compile, #graph, #summary;
581 stage = "amplitude_prepare_runtime_backends_start",
582 integrand = %self.name,
583 graph_count,
584 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
585 "Generation timing milestone"
586 );
587 generation_progress::backend_started(
588 GenerationProcessKind::Amplitude,
589 &self.name,
590 graph_count,
591 );
592 let mut amplitude_integrand = AmplitudeIntegrand {
593 settings: runtime_default.into_with_modified_kinematics(
594 &self.external_signature,
595 &self.graphs[0].graph.get_external_masses(model),
596 )?,
597 data: AmplitudeIntegrandData {
598 name: self.name.clone(),
599 compilation: global_settings
600 .generation
601 .compile
602 .frozen_mode(&global_settings.generation.evaluator),
603 rotations: None,
604 loop_cache_id: 0,
605 external_cache_id: 0,
606 base_external_cache_id: 0,
607 graph_terms: terms,
608 external_signature: self.external_signature.clone(),
609 graph_group_structure: self.graph_group_structure.clone(),
610 graph_to_group_id: graph_to_group_id_for_group_structure(
611 &self.graph_group_structure,
612 ),
613 group_derived_data: self.group_derived_data.clone(),
614 },
615 event_processing_runtime: Default::default(),
616 active_f64_backend: Default::default(),
617 };
618 let compile_times =
619 amplitude_integrand.prepare_runtime_backends_after_generation_with_compile_times()?;
620 crate::debug_tags!(#generation, #profile, #compile, #graph, #summary;
621 stage = "amplitude_prepare_runtime_backends_done",
622 integrand = %self.name,
623 graph_count,
624 elapsed_ms = backend_started.elapsed().as_secs_f64() * 1000.0,
625 total_elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
626 "Generation timing milestone"
627 );
628 generation_progress::backend_finished(
629 GenerationProcessKind::Amplitude,
630 &self.name,
631 backend_started.elapsed(),
632 );
633 for (report, compile_time) in graph_reports.iter_mut().zip(compile_times) {
634 report.stats.evaluator_compile_time += compile_time;
635 report.stats.total_time += compile_time;
636 }
637 self.integrand = Some(ProcessIntegrand::Amplitude(amplitude_integrand));
638 crate::debug_tags!(#generation, #profile, #graph, #summary;
639 stage = "amplitude_build_integrand_done",
640 integrand = %self.name,
641 graph_count = self.graphs.len(),
642 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
643 "Generation timing milestone"
644 );
645 Ok(graph_reports)
646 }
647
648 #[instrument(
649 skip_all,
650 fields(
651 amplitude.name = %self.name,
652 )
653 )]
654 #[allow(dead_code)]
655 pub(crate) fn write_dot<W: std::io::Write>(
656 &self,
657 writer: &mut W,
658 settings: &DotExportSettings,
659 ) -> Result<(), std::io::Error> {
660 for graph in &self.graphs {
661 graph.write_dot(writer, settings)?;
662 writeln!(writer)?;
663 }
664 Ok(())
665 }
666
667 #[instrument(
668 skip_all,
669 fields(
670 amplitude.name = %self.name,
671 )
672 )]
673 pub fn write_dot_fmt<W: fmt::Write>(
674 &self,
675 writer: &mut W,
676 settings: &DotExportSettings,
677 ) -> Result<(), std::fmt::Error> {
678 for graph in &self.graphs {
679 graph.write_dot_fmt(writer, settings)?;
680 writeln!(writer)?;
681 }
682 Ok(())
683 }
684
685 pub fn generate_grouped_derived_data(&mut self) -> Result<()> {
686 let group_derived_data = self
689 .graph_group_structure
690 .iter()
691 .map(|group| {
692 let mut group_esurface_structure =
693 BTreeMap::<Atom, TiVec<GraphGroupPosition, Option<RaisedEsurfaceId>>>::default(
694 );
695
696 for (graph_group_position, graph_id) in group.iter_enumerated() {
697 let amplitude_graph = &self.graphs[graph_id];
698 let lmb_reps = amplitude_graph.graph.integrand_replacement(
699 &litude_graph.graph.full_filter(),
700 &litude_graph.graph.loop_momentum_basis,
701 &[W_.x___],
702 );
703
704 let esurfaces = &litude_graph.graph.surface_cache.esurface_cache;
705
706 for (raised_esurface_id, raised_group) in amplitude_graph
707 .derived_data
708 .raised_data
709 .raised_groups
710 .iter_enumerated()
711 {
712 let esurface = &esurfaces[raised_group.esurface_ids[0]];
713 let esurface_atom = esurface.lmb_atom(&litude_graph.graph, &lmb_reps);
714
715 group_esurface_structure
716 .entry(esurface_atom)
717 .or_insert(ti_vec![None; group.len()])[graph_group_position] =
718 Some(raised_esurface_id);
719 }
720 }
721
722 let (surface_atoms, esurface_map) = group_esurface_structure.into_iter().unzip();
723
724 GroupDerivedData {
725 esurface_map,
726 esurface_atoms: surface_atoms,
727 }
728 })
729 .collect::<TiVec<GroupId, _>>();
730
731 let _: () = self.group_derived_data = group_derived_data;
732 Ok(())
733 }
734}
735
736#[derive(Clone, Encode, Decode)]
737#[trait_decode(trait= GammaLoopContext)]
738pub struct AmplitudeGraph {
739 pub graph: Graph,
740 pub derived_data: AmplitudeDerivedData,
741}
742
743pub struct AnalyticalEvaluationConfig<'a> {
744 pub model: &'a Model,
745 pub refresh_model_values: bool,
746 pub evaluate_numerically: bool,
747 pub vakint: &'a Vakint,
748 pub true_settings: &'a vakint::VakintSettings,
749 pub settings: &'a VakintSettings,
750 pub run_time_settings: &'a RuntimeSettings,
751 pub include_global_numerator: bool,
752}
753
754impl AmplitudeGraph {
755 pub(crate) fn new(graph: Graph) -> Self {
756 AmplitudeGraph {
757 graph,
758 derived_data: AmplitudeDerivedData {
759 all_mighty_integrand: Atom::Zero,
760 cff_expression: None,
761
762 lmbs: None,
763 tropical_sampler: None,
764 multi_channeling_setup: None,
765 threshold_counterterms: TiVec::new(),
766 raised_data: RaisedEsurfaceData {
767 raised_groups: TiVec::new(),
768 pass_two_evaluator: None,
769 },
770 raised_esurface_ids: TiVec::new(),
771 },
772 }
773 }
774}
775
776impl AmplitudeGraph {
777 pub fn renormalization_part(
778 &mut self,
779 settings: &UVgenerationSettings,
780 ) -> Result<RenormalizationPart> {
781 if self.derived_data.cff_expression.is_none() {
782 self.generate_cff(&OrientationPattern::default())?;
783 }
784 let valid_orientations: Vec<_> = self
785 .derived_data
786 .cff_expression
787 .as_ref()
788 .expect("cff_expression should have been created")
789 .orientations
790 .iter()
791 .map(|orientation| orientation.data.orientation.clone())
792 .collect();
793
794 settings.orchestrator.renormalization_part(
795 &mut self.graph,
796 OrientationProjection::new(&valid_orientations, &OrientationPattern::default()),
797 settings,
798 )
799 }
800
801 #[allow(dead_code)]
802 pub(crate) fn write_dot<W: std::io::Write>(
803 &self,
804 writer: &mut W,
805 settings: &DotExportSettings,
806 ) -> Result<(), std::io::Error> {
807 self.graph.dot_serialize_io(writer, settings)
808 }
809
810 pub(crate) fn write_dot_fmt<W: fmt::Write>(
811 &self,
812 writer: &mut W,
813 settings: &DotExportSettings,
814 ) -> Result<(), std::fmt::Error> {
815 self.graph.dot_serialize_fmt(writer, settings)
816 }
817
818 #[instrument(skip_all, err)]
819 pub(crate) fn generate_cff(&mut self, orientation_pattern: &OrientationPattern) -> Result<()> {
820 let _progress_guard = generation_progress::enter_detailed_progress_span("Generating CFF");
821 let shift_rewrite = self
822 .graph
823 .get_esurface_canonization(&self.graph.loop_momentum_basis);
824
825 let contract_edges = self
826 .graph
827 .iter_edges_of(
828 &self
829 .graph
830 .tree_edges
831 .subtract(&self.graph.initial_state_cut)
832 .subtract(&self.graph.external_filter::<SuBitGraph>()),
833 )
834 .map(|x| x.1)
835 .collect_vec();
836
837 let cff_expression =
838 self.graph
839 .generate_cff(&contract_edges, &shift_rewrite, orientation_pattern)?;
840
841 self.derived_data.cff_expression = Some(cff_expression);
842
843 Ok(())
844 }
845
846 #[instrument(skip_all, err)]
847 pub(crate) fn preprocess(
848 &mut self,
849 model: &Model,
850 settings: &GenerationSettings,
851 locked_runtime_settings: &LockedRuntimeSettings,
852 ) -> Result<GraphGenerationStats> {
853 let _progress_guard = generation_progress::enter_detailed_progress_span("preprocessing");
854 let preprocess_started = std::time::Instant::now();
855 let vk = crate::utils::vakint()?;
856
857 self.generate_cff(&settings.orientation_pattern)?;
858
859 let raised_data = settings.threshold_subtraction.enable_thresholds.then(|| {
862 self.graph.determine_raised_esurfaces_from_expression(
863 self.derived_data
864 .cff_expression
865 .as_ref()
866 .expect("cff_expression should have been created"),
867 )
868 });
869
870 self.build_integrands(settings, vk)?;
871
872 if self.graph.is_group_master {
873 self.build_tropical_sampler(settings)?;
874 }
875
876 self.build_lmbs();
877
878 if self.graph.is_group_master {
879 self.build_multi_channeling_channels(settings.override_lmb_heuristics);
880 }
881
882 if let Some(mut raised_data) = raised_data {
883 let max_order = raised_data
884 .raised_groups
885 .iter()
886 .map(|raised_group| raised_group.max_occurence)
887 .max()
888 .unwrap_or(0);
889 if max_order > 1 {
890 self.graph.param_builder.initialize_duals(max_order);
891 }
892 raised_data.pass_two_evaluator = Some(
893 (1..=max_order)
894 .map(|order| {
895 threshold_counterterm_helper(
896 order as u8,
897 self.graph.get_loop_number(),
898 &settings.evaluator,
899 )
900 })
901 .collect(),
902 );
903 self.derived_data.raised_data = raised_data;
904
905 let (threshold_counterterms, raised_esurface_ids) = self
906 .build_threshold_counterterm_parametric_integrand(
907 settings,
908 vk,
909 locked_runtime_settings,
910 model,
911 )?;
912 self.derived_data.threshold_counterterms = threshold_counterterms;
913 self.derived_data.raised_esurface_ids = raised_esurface_ids;
914 }
915
916 Ok(GraphGenerationStats {
917 total_time: preprocess_started.elapsed(),
918 ..GraphGenerationStats::default()
919 })
920 }
921
922 #[instrument(skip_all)]
923 fn build_multi_channeling_channels(&mut self, override_lmb_heuristics: bool) {
924 let _progress_guard =
925 generation_progress::enter_detailed_progress_span("Building Multi-Channeling Channels");
926 let channels = self.graph.build_multi_channeling_channels(
927 self.derived_data.lmbs.as_ref().unwrap(),
928 override_lmb_heuristics,
929 );
930
931 self.derived_data.multi_channeling_setup = Some(channels)
932 }
933
934 pub fn to_numerical(
987 numerical_result: AtomView,
988 true_settings: &vakint::VakintSettings,
989 ) -> Result<NumericalEvaluationResult> {
990 Ok(NumericalEvaluationResult::from_atom(
991 numerical_result,
992 vakint_symbol!(&true_settings.epsilon_symbol),
993 true_settings,
994 )?)
995 }
996
997 pub fn analytical_evaluation<S: SubGraphLike<Base = SuBitGraph> + SubSetOps>(
998 &self,
999 component: &S,
1000 config: AnalyticalEvaluationConfig<'_>,
1001 ) -> Result<Atom> {
1002 let mut true_settings = config.true_settings.clone();
1003 true_settings.number_of_terms_in_epsilon_expansion =
1004 self.graph.n_loops(&self.graph.no_dummy()) as i64 + 1;
1005 let pysec_dec_enabled_in_vakint = true_settings.evaluation_order.0.iter().find_map(|o| {
1006 if let EvaluationMethod::PySecDec(opts) = o {
1007 Some(opts)
1008 } else {
1009 None
1010 }
1011 });
1012
1013 let complex_params_vakint =
1014 if config.evaluate_numerically || pysec_dec_enabled_in_vakint.is_some() {
1015 let mut param_builder = self.graph.param_builder.clone(); if config.refresh_model_values {
1017 param_builder.update_model_values(config.model);
1018 }
1019 param_builder.m_uv_value(Complex::new_re(F(config.run_time_settings.general.m_uv)));
1020 param_builder.renormalization_localization_scale_value(Complex::new_re(F(config
1021 .run_time_settings
1022 .general
1023 .renormalization_localization_scale)));
1024 param_builder.mu_r_sq_value(Complex::new_re(F(config
1025 .run_time_settings
1026 .general
1027 .mu_r_sq())));
1028
1029 let mut complex_params: HashMap<String, symbolica::domains::float::Complex<f64>> =
1032 HashMap::default();
1033 for params in param_builder.pairs.into_iter() {
1034 let ps: crate::integrands::process::ParamValuePairs = params;
1035 for (p_name, p_value) in
1036 ps.params
1037 .iter()
1038 .zip(ps.value_range)
1039 .map(|(a, value_index)| {
1040 (
1041 a.to_canonical_string(),
1042 param_builder.values[0][value_index],
1043 )
1044 })
1045 {
1046 complex_params.insert(
1047 p_name,
1048 symbolica::domains::float::Complex::new(
1049 p_value.re.into(),
1050 p_value.im.into(),
1051 ),
1052 );
1053 }
1054 }
1055 for atom in &[
1057 Atom::Var(Var::new(Symbol::PI)),
1058 Atom::Var(Var::new(vakint_symbol!("EulerGamma"))),
1059 function!(Symbol::LOG, Atom::num(2)),
1060 ] {
1061 _ = complex_params.remove(&atom.to_string());
1062 }
1063
1064 config
1066 .vakint
1067 .params_from_complex_f64(&true_settings, &complex_params)
1068 } else {
1069 HashMap::default()
1070 };
1071
1072 if let Some(pysec_dec_opts) = pysec_dec_enabled_in_vakint {
1073 true_settings.evaluation_order.adjust(
1074 None,
1075 pysec_dec_opts.relative_precision,
1076 &HashMap::default(),
1077 &complex_params_vakint,
1078 &HashMap::default(),
1079 );
1080 }
1081
1082 let mut num = self
1083 .graph
1084 .numerator(component, &self.graph.empty_subgraph());
1085 if config.include_global_numerator {
1086 num.state.expr *= &self.graph.global_prefactor.num;
1087 }
1088
1089 let before_gamma = num.to_d_dim(GS.dim).get_single_atom().unwrap();
1090 let before_gamma_plain = before_gamma.to_plain_string();
1091 let four_dimensional_numerator = before_gamma.simplify_gamma();
1092 let after_gamma_plain = four_dimensional_numerator.to_plain_string();
1093 crate::debug_tags!(#uv, #integrated, #vakint, #profile, #trace;
1094 stage = "amplitude_to_vakint_after_simplify_gamma",
1095 changed = before_gamma_plain != after_gamma_plain,
1096 before_gamma_count = %before_gamma_plain.matches("spenso::gamma").count(),
1097 after_gamma_count = %after_gamma_plain.matches("spenso::gamma").count(),
1098 before_chain_count = %before_gamma_plain.matches("spenso::chain").count(),
1099 after_chain_count = %after_gamma_plain.matches("spenso::chain").count(),
1100 log.before_gamma = before_gamma,
1101 log.after_gamma = four_dimensional_numerator,
1102 "Gamma simplification before Vakint"
1103 );
1104
1105 let mut four_dimensional_integrand = four_dimensional_numerator
1106 / self
1107 .graph
1108 .denominator(component, |e| e.extra_data.vakint_edge_power.unwrap_or(1));
1109
1110 let component_lmb = self.graph.lmb_of(component);
1113 let mom_reps = self.graph.uv_wrapped_replacement(
1114 &self.graph.full_filter(),
1115 &component_lmb,
1116 &[W_.x___],
1117 );
1118
1119 four_dimensional_integrand = four_dimensional_integrand.replace_multiple(&mom_reps);
1126
1127 let mut vakint_integrand = to_vakint_integrand(
1142 &four_dimensional_integrand,
1143 &self.graph,
1144 &self.graph.full_filter(),
1145 &self.graph.empty_subgraph::<SuBitGraph>(),
1146 config.settings,
1147 false,
1148 )?;
1149
1150 vakint_integrand.canonicalize(&true_settings, &config.vakint.topologies, false)?;
1151 vakint_integrand.tensor_reduce(config.vakint, &true_settings)?;
1153 vakint_integrand.evaluate_integral(config.vakint, &true_settings)?;
1155 let analytical_evaluation: Atom = vakint_integrand.into();
1157 if !config.evaluate_numerically {
1162 Ok(analytical_evaluation)
1163 } else {
1164 let (numerical_evaluation, _error) = config
1165 .vakint
1166 .numerical_evaluation(
1167 &true_settings,
1168 analytical_evaluation.as_view(),
1169 &HashMap::default(),
1170 &complex_params_vakint,
1171 None,
1172 )
1173 .unwrap();
1174
1175 let numerical_evaluation_atom =
1178 numerical_evaluation.to_atom(vakint_symbol!(true_settings.epsilon_symbol.clone()));
1179
1180 Ok(numerical_evaluation_atom)
1181 }
1182 }
1183
1184 #[instrument(skip_all, err)]
1185 pub(crate) fn build_integrands(
1186 &mut self,
1187 settings: &GenerationSettings,
1188 vakint: &Vakint,
1189 ) -> Result<()> {
1190 let _progress_guard =
1191 generation_progress::enter_detailed_progress_span("Building Parametric Integrand");
1192 let started = std::time::Instant::now();
1193 crate::debug_tags!(#generation, #profile, #uv, #graph, #summary;
1194 stage = "amplitude_graph_build_integrands_start",
1195 graph = %self.graph.name,
1196 subtract_uv = settings.uv.subtract_uv,
1197 generate_integrated = settings.uv.generate_integrated,
1198 only = %settings.uv.final_integrand,
1199 "Generation timing milestone"
1200 );
1201 let valid_orientations: Vec<_> = self
1202 .derived_data
1203 .cff_expression
1204 .as_ref()
1205 .expect("cff_expression should have been created")
1206 .orientations
1207 .iter()
1208 .map(|orientation| orientation.data.orientation.clone())
1209 .collect();
1210 crate::debug_tags!(#generation, #profile, #graph, #orientation, #summary;
1211 stage = "amplitude_graph_valid_orientations_done",
1212 graph = %self.graph.name,
1213 orientation_count = valid_orientations.len(),
1214 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1215 "Generation timing milestone"
1216 );
1217 let cutstructure = CutStructure::empty(&self.graph);
1218 let orchestration_started = std::time::Instant::now();
1219 let parametric_exprs = settings.uv.orchestrator.parametric_integrands(
1220 &mut self.graph,
1221 cutstructure,
1222 vakint,
1223 OrientationProjection::new(&valid_orientations, &settings.orientation_pattern),
1224 &settings.uv,
1225 )?;
1226 crate::debug_tags!(#generation, #profile, #uv, #graph, #summary;
1227 stage = "amplitude_graph_parametric_orchestration_done",
1228 graph = %self.graph.name,
1229 parametric_integrand_count = parametric_exprs.len(),
1230 elapsed_ms = orchestration_started.elapsed().as_secs_f64() * 1000.0,
1231 total_elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1232 "Generation timing milestone"
1233 );
1234
1235 let normalization_started = std::time::Instant::now();
1236 let exprs: Vec<_> = parametric_exprs.into_iter().collect();
1237 crate::debug_tags!(#generation, #profile, #graph, #summary;
1238 stage = "amplitude_graph_cff_normalization_done",
1239 graph = %self.graph.name,
1240 expr_count = exprs.len(),
1241 elapsed_ms = normalization_started.elapsed().as_secs_f64() * 1000.0,
1242 total_elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1243 "Generation timing milestone"
1244 );
1245
1246 let assign_started = std::time::Instant::now();
1247 let integrands = exprs.into_iter().next().unwrap().integrands;
1248 self.derived_data.all_mighty_integrand = integrands.iter().next().unwrap().1.clone(); crate::debug_tags!(#generation, #profile, #graph, #summary;
1250 stage = "amplitude_graph_build_integrands_done",
1251 graph = %self.graph.name,
1252 assign_elapsed_ms = assign_started.elapsed().as_secs_f64() * 1000.0,
1253 elapsed_ms = started.elapsed().as_secs_f64() * 1000.0,
1254 "Generation timing milestone"
1255 );
1256
1257 Ok(())
1258 }
1259
1260 #[instrument(skip_all, err)]
1261 fn build_threshold_counterterm_parametric_integrand(
1262 &mut self,
1263 settings: &GenerationSettings,
1264 vakint: &Vakint,
1265 locked_runtime_settings: &LockedRuntimeSettings,
1266 model: &Model,
1267 ) -> Result<(
1268 TiVec<RaisedEsurfaceId, AmplitudeCountertermAtom>,
1269 TiVec<EsurfaceID, RaisedEsurfaceId>,
1270 )> {
1271 let _progress_guard =
1272 generation_progress::enter_detailed_progress_span("Building Threshold Counterterms");
1273 let valid_orientations: Vec<_> = self
1274 .derived_data
1275 .cff_expression
1276 .as_ref()
1277 .expect("cff_expression should have been created")
1278 .orientations
1279 .iter()
1280 .map(|orientation| orientation.data.orientation.clone())
1281 .collect();
1282
1283 let global_cff = self
1284 .derived_data
1285 .cff_expression
1286 .as_ref()
1287 .expect("cff_expression should have been created");
1288 let esurface_raising = &self.derived_data.raised_data;
1289 let mut counterterms: TiVec<RaisedEsurfaceId, AmplitudeCountertermAtom> = ti_vec![
1290 AmplitudeCountertermAtom::new();
1291 esurface_raising.raised_groups.len()
1292 ];
1293 let mut raised_esurface_ids: TiVec<EsurfaceID, Option<RaisedEsurfaceId>> =
1294 ti_vec![None; global_cff.surfaces.esurface_cache.len()];
1295
1296 for (raised_esurface_id, raised_group) in esurface_raising.raised_groups.iter_enumerated() {
1297 for &esurface_id in &raised_group.esurface_ids {
1298 raised_esurface_ids[esurface_id] = Some(raised_esurface_id);
1299 }
1300 }
1301 let raised_esurface_ids: TiVec<EsurfaceID, RaisedEsurfaceId> = raised_esurface_ids
1302 .into_iter()
1303 .map(|raised_esurface_id| {
1304 raised_esurface_id
1305 .expect("every esurface should belong to exactly one raised-esurface group")
1306 })
1307 .collect();
1308
1309 let mut cuts = vec![];
1310
1311 let external_filter: SuBitGraph = self.graph.external_filter();
1312 let mut incoming_externals = vec![];
1313 let mut outgoing_externals = vec![];
1314
1315 for (edge, edge_id, _) in self.graph.iter_edges_of(&external_filter) {
1316 match edge {
1317 HedgePair::Unpaired {
1318 flow: Flow::Sink, ..
1319 } => incoming_externals.push(edge_id),
1320 HedgePair::Unpaired {
1321 flow: Flow::Source, ..
1322 } => outgoing_externals.push(edge_id),
1323 _ => unreachable!("the external filter must contain only unpaired edges"),
1324 }
1325 }
1326
1327 for raised_data in esurface_raising.raised_groups.iter().cloned() {
1328 let esurface_id = raised_data.esurface_ids[0];
1329 let esurface = &global_cff.surfaces.esurface_cache[esurface_id];
1330
1331 if esurface.external_shift.is_empty() {
1332 continue;
1333 }
1334
1335 let is_known_existing_at_generation =
1336 settings.threshold_subtraction.check_esurface_at_generation;
1337 if is_known_existing_at_generation {
1338 let masses: EdgeVec<F<f64>> = self.graph.get_real_mass_vector(model);
1339 let lmb = &self.graph.loop_momentum_basis;
1340 if !locked_runtime_settings.existence_check(
1341 esurface,
1342 &masses,
1343 &self.graph.get_external_signature(),
1344 lmb,
1345 settings.threshold_subtraction.esurface_existence_threshold,
1346 ) {
1347 continue;
1348 }
1349 }
1350
1351 if settings
1352 .threshold_subtraction
1353 .assume_positive_external_energies
1354 && !is_known_existing_at_generation
1355 && !esurface.external_shift_is_strictly_negative_for_positive_energies(
1356 &incoming_externals,
1357 &outgoing_externals,
1358 )
1359 {
1360 continue;
1361 }
1362
1363 let mut cut_union: SuBitGraph = self.graph.empty_subgraph();
1364
1365 for energy in esurface.energies.iter() {
1366 let (_, hedge_pair) = self.graph[energy];
1367 match hedge_pair {
1368 HedgePair::Paired { source, sink } => {
1369 cut_union.add(source);
1370 cut_union.add(sink);
1371 }
1372 _ => unreachable!(),
1373 }
1374 }
1375
1376 let cutset = CutSet {
1377 residue_selector: ResidueSelector {
1378 lu_cut: None,
1379 left_th_cut: Some(raised_data.clone()),
1380 right_th_cut: None,
1381 },
1382 union: cut_union,
1383 canonicalize_external_shifts: false,
1384 };
1385
1386 cuts.push(cutset);
1387 }
1388
1389 let cut_structure = CutStructure { cuts };
1390
1391 let exprs: Vec<_> = settings.uv.orchestrator.parametric_integrands(
1392 &mut self.graph,
1393 cut_structure,
1394 vakint,
1395 OrientationProjection::new(&valid_orientations, &settings.orientation_pattern),
1396 &settings.uv,
1397 )?;
1398
1399 for expr in exprs.into_iter() {
1400 let loop_number = self.graph.n_loops(&self.graph.underlying.full_filter());
1401 let jacobian_factor = Atom::var(GS.radius_star_left).pow(loop_number as i32 * 3 - 1);
1402
1403 let expr = expr.map(|integrand| integrand * &jacobian_factor);
1404 let counterterm_atom = AmplitudeCountertermAtom {
1405 parametric: expr.integrands,
1406 };
1407 let raised_group = expr.cuts.residue_selector.left_th_cut.unwrap();
1408 let raised_esurface_id = raised_esurface_ids[raised_group.esurface_ids[0]];
1409 debug!("raised_esurface_id: {}", raised_esurface_id.0);
1410
1411 for (_, integrand) in counterterm_atom.parametric.iter() {
1412 debug!("counterterm integrand: {}", integrand.log_print(Some(100)));
1413 }
1414
1415 counterterms[raised_esurface_id] = counterterm_atom;
1416 }
1417
1418 Ok((counterterms, raised_esurface_ids))
1419 }
1420
1421 #[instrument(skip_all)]
1422 fn build_lmbs(&mut self) {
1423 let _progress_guard =
1424 generation_progress::enter_detailed_progress_span("Building Loop Momentum Bases");
1425 let lmbs = self
1426 .graph
1427 .generate_loop_momentum_bases_of(&self.graph.no_dummy());
1428
1429 self.derived_data.lmbs = Some(lmbs)
1430 }
1431
1432 #[instrument(skip_all, err)]
1433 fn build_tropical_sampler(&mut self, process_settings: &GenerationSettings) -> Result<()> {
1434 let _progress_guard =
1435 generation_progress::enter_detailed_progress_span("Building Tropical Sampler");
1436 if process_settings
1437 .tropical_subgraph_table
1438 .disable_tropical_generation
1439 {
1440 debug!("Tropical subgraph table generation is disabled.");
1441 return Ok(());
1442 }
1443 let num_virtual_loop_edges = self.graph.iter_loop_edges().count();
1444
1445 if num_virtual_loop_edges == 0 {
1446 debug!("Graph has no loop edges, skipping tropical sampler generation.");
1447 return Ok(());
1448 }
1449
1450 let num_loops = self.graph.loop_momentum_basis.loop_edges.len();
1451 let target_omega = process_settings.tropical_subgraph_table.target_omega;
1452
1453 let weight = (target_omega + (3 * num_loops) as f64 / 2.) / num_virtual_loop_edges as f64;
1454
1455 debug!(
1456 "Building tropical subgraph table with all edge weights set to: {}",
1457 weight
1458 );
1459
1460 let tropical_edges = self
1461 .graph
1462 .iter_loop_edges()
1463 .map(|(pair, _edge_id, edge)| {
1464 let is_massive = edge.data.particle.is_massive();
1465
1466 let vertices = match pair {
1467 HedgePair::Paired { source, sink } => (
1468 self.graph.underlying.node_id(source).0 as u8,
1469 self.graph.underlying.node_id(sink).0 as u8,
1470 ),
1471 _ => unreachable!(),
1472 };
1473
1474 momtrop::Edge {
1475 is_massive,
1476 weight,
1477 vertices,
1478 }
1479 })
1480 .collect_vec();
1481
1482 let mut external_vertices_pool = AHashSet::new();
1483
1484 for (pair, _, _) in self.graph.iter_non_loop_edges() {
1485 match pair {
1486 HedgePair::Paired { source, sink } => {
1487 let source_id = self.graph.underlying.node_id(source).0 as u8;
1488 let sink_id = self.graph.underlying.node_id(sink).0 as u8;
1489
1490 external_vertices_pool.insert(source_id);
1491 external_vertices_pool.insert(sink_id);
1492 }
1493 HedgePair::Unpaired { hedge, .. } => {
1494 let id = self.graph.underlying.node_id(hedge).0 as u8;
1495 external_vertices_pool.insert(id);
1496 }
1497 _ => unreachable!(),
1498 }
1499 }
1500
1501 let mut external_vertices = vec![];
1502
1503 for tropical_edge in &tropical_edges {
1504 if external_vertices_pool.contains(&tropical_edge.vertices.0) {
1505 external_vertices.push(tropical_edge.vertices.0);
1506 }
1507
1508 if external_vertices_pool.contains(&tropical_edge.vertices.1) {
1509 external_vertices.push(tropical_edge.vertices.1);
1510 }
1511 }
1512
1513 let tropical_graph = momtrop::Graph {
1514 edges: tropical_edges,
1515 externals: external_vertices,
1516 };
1517
1518 let loop_part = self
1519 .graph
1520 .iter_loop_edges()
1521 .map(|(_, edge_id, _edge)| {
1522 self.graph.loop_momentum_basis.edge_signatures[edge_id]
1523 .internal
1524 .clone()
1525 .to_momtrop_format()
1526 })
1527 .collect_vec();
1528
1529 let sampler = tropical_graph
1530 .build_sampler(loop_part)
1531 .map_err(|e| eyre!(e))?;
1532
1533 let _: () = self.derived_data.tropical_sampler = Some(sampler);
1534 Ok(())
1535 }
1536
1537 #[instrument(
1539 name = "generate_term_for_graph",
1540 level = "info",
1541 skip(self, model, global_settings),
1542 fields(
1543 graph.name = %self.graph.name
1544
1545 ),
1546 err
1547 )]
1548 fn generate_term_for_graph(
1549 &self,
1550 model: &Model,
1551 own_group_position: GraphGroupPosition,
1552 esurface_map: TiVec<GroupEsurfaceId, TiVec<GraphGroupPosition, Option<RaisedEsurfaceId>>>,
1553 global_settings: &GlobalSettings,
1554 ) -> Result<(AmplitudeGraphTerm, GraphGenerationStats)> {
1555 let _progress_guard = generation_progress::enter_detailed_progress_span(&format!(
1556 "Generating Evaluators for {}",
1557 self.graph.name
1558 ));
1559 AmplitudeGraphTerm::from_amplitude_graph(
1560 self,
1561 own_group_position,
1562 esurface_map,
1563 model,
1564 global_settings,
1565 )
1566 }
1567}
1568
1569#[derive(Clone, Encode, Decode)]
1570#[trait_decode(trait = GammaLoopContext)]
1571pub struct AmplitudeDerivedData {
1572 pub all_mighty_integrand: Atom,
1573 pub threshold_counterterms: TiVec<RaisedEsurfaceId, AmplitudeCountertermAtom>,
1574 pub raised_data: RaisedEsurfaceData,
1575 pub raised_esurface_ids: TiVec<EsurfaceID, RaisedEsurfaceId>,
1576 pub multi_channeling_setup: Option<LmbMultiChannelingSetup>,
1577 pub lmbs: Option<TiVec<LmbIndex, LoopMomentumBasis>>,
1578 pub tropical_sampler: Option<SampleGenerator<3>>,
1579 pub cff_expression: Option<CFFExpression<OrientationID>>,
1580}
1581
1582pub trait AmplitudeState:
1583 Clone + std::fmt::Debug + bincode::Encode + for<'a> bincode::Decode<GammaLoopContextContainer<'a>>
1584{
1585}
1586impl AmplitudeState for () {}
1587
1588#[derive(Clone, Encode, Decode, Debug)]
1589pub struct Processed {}
1590impl AmplitudeState for Processed {}
1591
1592impl Amplitude {
1597 pub fn from_dot_string<Str: AsRef<str>>(s: Str, name: String, model: &Model) -> Result<Self> {
1598 let graphs = Graph::from_string(s, model)?;
1599
1600 let mut amp = Amplitude::new(name);
1601 for g in graphs {
1602 amp.add_graph(g)?;
1603 }
1604 Ok(amp)
1605 }
1606
1607 pub fn from_dot_file<P>(p: P, name: String, model: &Model) -> Result<Self>
1608 where
1609 P: AsRef<Path>,
1610 {
1611 let graphs = Graph::from_file(p, model)?;
1612
1613 let mut amp = Amplitude::new(name);
1614 for g in graphs {
1615 amp.add_graph(g)?;
1616 }
1617 Ok(amp)
1618 }
1619
1620 pub fn from_graph_list(name: impl ToString, mut graphs: Vec<Graph>) -> Result<Self> {
1621 let mut amplitude: Amplitude = Amplitude::new(name);
1622 amplitude.graph_group_structure = complete_group_parsing(&mut graphs)?;
1623
1624 for amplitude_graph in graphs {
1625 amplitude.add_graph(amplitude_graph)?;
1626 }
1627 Ok(amplitude)
1628 }
1629
1630 pub(crate) fn new(name: impl ToString) -> Self {
1631 Self {
1632 integrand: None,
1633 name: name.to_string(),
1634 graphs: vec![],
1635 graph_group_structure: TiVec::new(),
1636 external_particles: vec![],
1637 external_signature: SignatureLike::from_iter(iter::empty::<i8>()),
1638 group_derived_data: TiVec::new(),
1639 }
1640 }
1641
1642 fn add_graph(&mut self, graph: Graph) -> Result<()> {
1643 let new_external_particels = graph.get_external_partcles();
1644 let new_external_signature = graph.get_external_signature();
1645
1646 if !self.graphs.is_empty() {
1647 if self.external_particles != new_external_particels {
1648 return Err(eyre!("amplitude graph has different externals")).with_context(|| {
1649 format!(
1650 "Found {} externals, expected {} for the graph {}",
1651 new_external_particels.len(),
1652 self.external_particles.len(),
1653 DotGraph::from(&graph).debug_dot()
1654 )
1655 });
1656 }
1657
1658 if self.external_signature != new_external_signature {
1659 return Err(eyre!("wrong external signature"));
1660 }
1661 } else {
1662 self.external_particles = new_external_particels;
1663 self.external_signature = new_external_signature;
1664 }
1665
1666 self.graphs.push(AmplitudeGraph::new(graph));
1667
1668 Ok(())
1670 }
1671}
1672
1673pub(crate) fn threshold_counterterm_helper_atom(order: u8, loop_number: usize) -> Atom {
1674 let loop_3 = loop_number as i64 * 3;
1675
1676 let laurent_coeff_indices = (1..=order).map(|i| -(i as i8));
1677
1678 let mut laurent_coeffs = laurent_coeff_indices.map(|laurent_coeff_index| {
1679 build_derivative_structure_atom(order, laurent_coeff_index)
1680 .replace(GS.rescale_star)
1681 .with(GS.radius_star_left)
1682 });
1683
1684 let i = Atom::i();
1685
1686 let radius = Atom::var(GS.radius_left);
1687 let radius_star = Atom::var(GS.radius_star_left);
1688 let uv_damp_plus = Atom::var(GS.uv_damp_plus_left);
1689 let uv_damp_minus = Atom::var(GS.uv_damp_minus_left);
1690 let hfunction = Atom::var(GS.hfunction_left_th);
1691
1692 let delta_r_plus = &radius - &radius_star;
1693 let delta_r_minus = -&radius - &radius_star;
1694
1695 let jacobian_ratio = (Atom::one() / &radius).pow(loop_3 - 1);
1696
1697 let local_prefactor =
1698 &jacobian_ratio * (uv_damp_plus / &delta_r_plus + uv_damp_minus / &delta_r_minus);
1699
1700 let integrated_prefactor = -i * Atom::var(GS.pi) * &jacobian_ratio * hfunction;
1701
1702 let mut result = (local_prefactor + integrated_prefactor) * laurent_coeffs.next().unwrap();
1703
1704 for pow in 2..=order {
1705 result += laurent_coeffs.next().unwrap()
1706 * &jacobian_ratio
1707 * (Atom::one() / delta_r_plus.pow(pow as i64)
1708 + Atom::one() / delta_r_minus.pow(pow as i64));
1709 }
1710
1711 debug!(
1712 "Threshold counterterm helper atom for order {} and loop number {}: {}",
1713 order, loop_number, result
1714 );
1715 result
1716}
1717
1718pub(crate) fn threshold_counterterm_helper(
1719 order: u8,
1720 loop_number: usize,
1721 evaluator_settings: &EvaluatorSettings,
1722) -> GenericEvaluator {
1723 let atom = threshold_counterterm_helper_atom(order, loop_number);
1724 let mut fn_map = FunctionMap::default();
1725 fn_map
1726 .add_aliases([(
1727 GS.pi.into(),
1728 Atom::num(Rational::try_from(std::f64::consts::PI).unwrap()),
1729 )])
1730 .unwrap();
1731
1732 let mut params = params_for_derivative_order(order)
1733 .into_iter()
1734 .map(|param| param.replace(GS.rescale_star).with(GS.radius_star_left))
1735 .collect_vec();
1736
1737 let radius = Atom::var(GS.radius_left);
1738 let radius_star = Atom::var(GS.radius_star_left);
1739 let uv_damp_plus = Atom::var(GS.uv_damp_plus_left);
1740 let uv_damp_minus = Atom::var(GS.uv_damp_minus_left);
1741 let hfunction = Atom::var(GS.hfunction_left_th);
1742
1743 params.push(radius);
1744 params.push(radius_star);
1745 params.push(uv_damp_plus);
1746 params.push(uv_damp_minus);
1747 params.push(hfunction);
1748
1749 GenericEvaluator::new_from_raw_params(
1750 [atom],
1751 ¶ms,
1752 &fn_map,
1753 vec![],
1754 evaluator_settings.optimization_settings(),
1755 None,
1756 evaluator_settings,
1757 )
1758 .unwrap()
1759 .into_eager_only()
1760}
1761
1762#[cfg(test)]
1763pub mod test {
1764
1765 use crate::{
1766 cff::expression::OrientationID,
1767 dot,
1768 graph::{GraphGroupPosition, parse::IntoGraph},
1769 initialisation::test_initialise,
1770 integrands::process::amplitude::AmplitudeGraphTerm,
1771 processes::AmplitudeGraph,
1772 settings::{
1773 GlobalSettings, RuntimeSettings,
1774 global::{GenerationSettings, OrientationPattern, ThresholdSubtractionSettings},
1775 },
1776 utils::load_generic_model,
1777 };
1778 use typed_index_collections::TiVec;
1779
1780 #[test]
1781 fn amplitude_tree() {
1782 test_initialise().unwrap();
1783 let mut graph: AmplitudeGraph = dot!(digraph qqx_aaa_tree_1 {
1784 num="spenso::g(spenso::dind(spenso::cof(3, hedge(1))), spenso::cof(3, hedge(2)))/3"
1785 ext [style=invis]
1786 ext -> v1:1 [particle="d" id=1];
1787 ext -> v3:2 [particle="d~" id=2];
1788 v1:3 -> ext [particle="a" id=3];
1789 v2:4 -> ext [particle="a" id=4];
1790 v3:0 -> ext [particle="a" id=0];
1791 v1 -> v2 [particle="d" id=5];
1792 v2 -> v3 [particle="d" id=6];
1793 })
1794 .unwrap();
1795
1796 let _model = load_generic_model("sm");
1797
1798 graph.generate_cff(&OrientationPattern::default()).unwrap();
1799 let param_builder = &graph.graph.param_builder;
1802 println!("{param_builder}");
1803
1804 }
1814
1815 #[test]
1816 fn generation_orientation_pattern_filters_evaluator_orientations() {
1817 test_initialise().unwrap();
1818 let mut graph: AmplitudeGraph = dot!(
1819 digraph bub {
1820 edge [particle=scalar_1]
1821 node [num=1]
1822 e [style=invis]
1823 e -> A:0 [id=3]
1824 B:1 -> e [id=2]
1825 A -> B [id=1]
1826 A -> B [id=0]
1827 },
1828 "scalars"
1829 )
1830 .unwrap();
1831
1832 let model = load_generic_model("scalars");
1833 let generation_settings = GenerationSettings {
1834 threshold_subtraction: ThresholdSubtractionSettings {
1835 enable_thresholds: false,
1836 ..Default::default()
1837 },
1838 ..Default::default()
1839 };
1840 let runtime_settings = RuntimeSettings::default();
1841 graph
1842 .preprocess(&model, &generation_settings, &(&runtime_settings).into())
1843 .unwrap();
1844
1845 assert!(
1846 graph
1847 .derived_data
1848 .cff_expression
1849 .as_ref()
1850 .unwrap()
1851 .orientations
1852 .len()
1853 > 1
1854 );
1855
1856 let global_settings = GlobalSettings {
1857 generation: GenerationSettings {
1858 orientation_pattern: OrientationPattern::from_orientation(
1859 &graph
1860 .derived_data
1861 .cff_expression
1862 .as_ref()
1863 .unwrap()
1864 .orientations[OrientationID(0)],
1865 ),
1866 threshold_subtraction: ThresholdSubtractionSettings {
1867 enable_thresholds: false,
1868 ..Default::default()
1869 },
1870 ..Default::default()
1871 },
1872 ..Default::default()
1873 };
1874
1875 let (term, _stats) = AmplitudeGraphTerm::from_amplitude_graph(
1876 &graph,
1877 GraphGroupPosition(0),
1878 TiVec::new(),
1879 &model,
1880 &global_settings,
1881 )
1882 .unwrap();
1883
1884 assert_eq!(term.orientations.len(), 1);
1885 assert_eq!(
1886 term.orientations[OrientationID(0)],
1887 graph
1888 .derived_data
1889 .cff_expression
1890 .as_ref()
1891 .unwrap()
1892 .orientations[OrientationID(0)]
1893 .data
1894 .orientation
1895 );
1896 }
1897}