Skip to main content

gammalooprs/processes/
amplitude.rs

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;
11// use bincode::{Decode, Encode};
12use 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        // preprocess each graph individually
379        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(); // should always be set
489                    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        // for each group we must collect all inequivalent esurfaces.
687
688        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                        &amplitude_graph.graph.full_filter(),
700                        &amplitude_graph.graph.loop_momentum_basis,
701                        &[W_.x___],
702                    );
703
704                    let esurfaces = &amplitude_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(&amplitude_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        // UV orchestration can extend the graph surface cache, while raised IDs
860        // belong to the CFF expression generated above.
861        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 ct_params(&self, model: &Model) -> ParamBuilder<f64> {
935    //      let mut param_builder = self.param_builder_core(model);
936    //      param_builder.uv_damp_atom(vec![
937    //          Atom::var(GS.uv_damp_plus),
938    //          Atom::var(GS.uv_damp_minus),
939    //      ]);
940    //      param_builder.derivative_at_tstar_atom(Atom::var(GS.deta));
941    //      param_builder.radius_atom(Atom::var(GS.radius));
942    //      param_builder.radius_star_atom(Atom::var(GS.radius_star));
943    //      param_builder.h_function_atom(Atom::var(GS.hfunction));
944    //      param_builder
945    //  }
946
947    //  pub(crate) fn param_builder_core(&self, model: &Model) -> ParamBuilder<f64> {
948    //      // the float type does not matter here
949    //      let mut param_builder = ParamBuilder::<f64>::new_empty();
950
951    //      // this is wrong if we allow for vacuum graphs
952    //      param_builder.external_energies_atom(&self.graph);
953    //      param_builder.orientation_params(&self.graph);
954
955    //      // param_builder.polarizations(&self.graph);
956    //      // param_builder.polar
957
958    //      // spatial components of external momenta
959    //      param_builder.external_spatial_atom(&self.graph);
960    //      param_builder.polarization_params(&self.graph);
961    //      // spatial EMR
962    //      param_builder.emr_spatial_atom(&self.graph);
963
964    //      param_builder.model_parameters_atom(model);
965
966    //      param_builder.m_uv_atom(Atom::var(GS.m_uv_vacuum));
967
968    //      param_builder.mu_r_sq_atom(Atom::var(GS.mu_r_sq));
969
970    //      self.add_function_map(&mut param_builder);
971
972    //      param_builder
973    //  }
974
975    // pub fn fill_in_params(
976    //     &self,
977    //     param_builder: &mut ParamBuilder,
978    //     model: &Model,
979    //     settings: &Settings,
980    // ) {
981    //     param_builder.external_energies_value(momentum_sample);
982    // }
983
984    // fn get_eager_const_map(&self)->HashM
985
986    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(); //ParamBuilder::<f64>::new(&self.graph, model);
1016                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                // println!("\nParamBuilder parameters:\n{}", param_builder);
1030
1031                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                // Make sure to remove entries already supported by vakint, as they may not match required precision
1056                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                // Make sure to properly do the upcasting to required precision in vakint settings
1065                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        // println!("Four-dimensional integrand: {}", four_dimensional_integrand);
1111
1112        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        // println!("Reps:");
1120        // for r in &mom_reps {
1121        //     println!("{r}");
1122        // }
1123
1124        // rewrite the inner_t as well
1125        four_dimensional_integrand = four_dimensional_integrand.replace_multiple(&mom_reps);
1126
1127        // println!("LMB: {}", lmb);
1128        // let vk_mom = vakint_symbol!("k");
1129        // for (i, l) in lmb.loop_edges.iter().enumerate() {
1130        //     four_dimensional_integrand = four_dimensional_integrand
1131        //         .replace(function!(GS.emr_mom, usize::from(*l) as i64))
1132        //         .with(function!(vk_mom, i as i64 + 1))
1133        //         .replace(function!(
1134        //             GS.emr_mom,
1135        //             function!(vk_mom, i as i64 + 1),
1136        //             W_.x___
1137        //         ))
1138        //         .with(function!(vk_mom, i as i64 + 1, W_.x___));
1139        // }
1140
1141        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        // println!("Canonized: {}", vakint_integrand);
1152        vakint_integrand.tensor_reduce(config.vakint, &true_settings)?;
1153        // println!("Tensor Reduced {}", vakint_integrand);
1154        vakint_integrand.evaluate_integral(config.vakint, &true_settings)?;
1155        // println!("Evaluated {}", vakint_integrand);
1156        let analytical_evaluation: Atom = vakint_integrand.into();
1157        // println!(
1158        //     "\nVakint analytical evaluation:\n{:#}",
1159        //     analytical_evaluation
1160        // );
1161        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            // println!("\nVakint numerical evaluation:\n{:#}", numerical_evaluation);
1176
1177            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(); // should be exactly one expression
1249        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    // Expects cff_expression, esurface_data,
1538    #[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
1592// #[derive(Clone, Encode, Decode, Debug)]
1593// pub struct ReadyForTerm {}
1594// impl AmplitudeState for ReadyForTerm {}
1595
1596impl 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        //  TODO: validate that the graph is compatible
1669        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        &params,
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        // graph.build_parametric_integrand(&GenerationSettings::default());
1800
1801        let param_builder = &graph.graph.param_builder;
1802        println!("{param_builder}");
1803
1804        // GenericEvaluator::new_from_builder(
1805        //     [GS.orientation_delta(&EdgeVec::from_iter(vec![Orientation::Default; 7]))],
1806        //     param_builder,
1807        //     None,
1808        //     OptimizationSettings::default(),
1809        //     true,
1810        // )
1811        // .unwrap();
1812        // println!(" {}", a);
1813    }
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}