1#![allow(clippy::too_many_arguments)]
3
4use ahash::HashMap;
5use color_eyre::owo_colors::OwoColorize;
6use tracing::info;
7
8use std::collections::{BTreeMap, BTreeSet};
9use std::ffi::OsStr;
10use std::fs;
11use std::num::ParseIntError;
12use std::ops::RangeInclusive;
13use std::path::Path;
14use std::str::FromStr;
15
16use clap::{Args, Parser, Subcommand, ValueEnum};
17use color_eyre::Result;
18use regex::Regex;
19use schemars::JsonSchema;
20use serde::{Deserialize, Serialize};
21use symbolica::parse;
22use tabled::{
23 builder::Builder,
24 settings::{style::HorizontalLine, themes::Theme, Style},
25};
26use thiserror::Error;
27use walkdir::WalkDir;
28
29use eyre::{eyre, Context};
30use gammalooprs::feyngen::{
31 FeynGenFilter, FeynGenFilters, GenerationType, GraphGroupingOptions,
32 NumeratorAwareGraphGroupingOption, SelfEnergyFilterOptions, SewedFilterOptions,
33 SnailFilterOptions, TadpolesFilterOptions,
34};
35use gammalooprs::model::Model;
36use gammalooprs::numerator::GlobalPrefactor;
37use gammalooprs::processes::amplitude::Amplitude;
38use gammalooprs::processes::{
39 merge_generated_graph_reports, CrossSection, GeneratedGraphReport, GraphGenerationStats,
40 Process, ProcessDefinition, ProcessList,
41};
42use gammalooprs::settings::{GlobalSettings, RuntimeSettings};
43
44use crate::commands::set::KvPair;
45use crate::completion::CompletionArgExt;
46use crate::state::{GenerationResourceSummary, ProcessRef, State};
47
48#[derive(Debug, Parser, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
51pub struct Generate {
53 #[arg(long = "keep-sources", default_value_t = false, global = true)]
55 #[serde(default)]
56 pub keep_sources: bool,
57 #[command(subcommand)]
60 pub mode: Option<GenerateCmd>,
61}
62
63#[derive(Debug, Subcommand, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
64pub enum GenerateCmd {
66 Xs(SpecArgs),
68
69 #[command(alias = "amplitude")]
71 Amp(SpecArgs),
72
73 Existing(ProcessArgs),
75}
76
77#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, ValueEnum)]
78pub enum GroupingChoice {
79 #[clap(name = "no_grouping")]
80 NoGrouping,
81 #[clap(name = "only_detect_zeroes")]
82 OnlyDetectZeroes,
83 #[clap(name = "group_identical_graphs_up_to_sign")]
84 GroupIdenticalGraphsUpToSign,
85 #[clap(name = "group_identical_graphs_up_to_scalar_rescaling")]
86 GroupIdenticalGraphsUpToScalarRescaling,
87}
88
89impl GroupingChoice {
90 fn to_strategy(
91 self,
92 seed: Option<u16>,
93 num_samples: Option<usize>,
94 differentiate_particle_masses_only: Option<bool>,
95 fully_numerical_substitution_when_comparing_numerators: Option<bool>,
96 test_canonized_numerator: Option<bool>,
97 symmetric_polarizations: Option<bool>,
98 ) -> NumeratorAwareGraphGroupingOption {
99 let mut graph_grouping_options = GraphGroupingOptions::default();
100 if let Some(seed) = seed {
101 graph_grouping_options.numerical_sample_seed = seed;
102 }
103 if let Some(num_samples) = num_samples {
104 graph_grouping_options.number_of_numerical_samples = num_samples;
105 }
106 if let Some(differentiate_particle_masses_only) = differentiate_particle_masses_only {
107 graph_grouping_options.differentiate_particle_masses_only =
108 differentiate_particle_masses_only;
109 }
110 if let Some(test_canonized_numerator) = test_canonized_numerator {
111 graph_grouping_options.test_canonized_numerator = test_canonized_numerator;
112 }
113 if let Some(fully_numerical_substitution_when_comparing_numerators) =
114 fully_numerical_substitution_when_comparing_numerators
115 {
116 graph_grouping_options.fully_numerical_substitution_when_comparing_numerators =
117 fully_numerical_substitution_when_comparing_numerators;
118 }
119 if let Some(symmetric_polarizations) = symmetric_polarizations {
120 graph_grouping_options.symmetric_polarizations = symmetric_polarizations;
121 }
122 match self {
123 GroupingChoice::GroupIdenticalGraphsUpToScalarRescaling => {
124 NumeratorAwareGraphGroupingOption::GroupIdenticalGraphUpToScalarRescaling(
125 graph_grouping_options,
126 )
127 }
128 GroupingChoice::GroupIdenticalGraphsUpToSign => {
129 NumeratorAwareGraphGroupingOption::GroupIdenticalGraphUpToSign(
130 graph_grouping_options,
131 )
132 }
133 GroupingChoice::NoGrouping => NumeratorAwareGraphGroupingOption::NoGrouping,
134 GroupingChoice::OnlyDetectZeroes => NumeratorAwareGraphGroupingOption::OnlyDetectZeroes,
135 }
136 }
137}
138
139#[derive(Args, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
140pub struct SpecArgs {
141 #[arg(value_name = "TOKENS", num_args = 1..)]
154 pub tokens: Vec<String>,
155
156 #[arg(short = 'a', default_value_t = false)]
162 pub append: bool,
163
164 #[arg(
166 long = "clear-existing-processes",
167 short = 'c',
168 alias = "clear",
169 default_value_t = false
170 )]
171 pub clear_existing_processes: bool,
172
173 #[arg(long = "process-name", short = 'p')]
175 pub process_name: Option<String>,
176
177 #[arg(
179 long = "integrand-name",
180 short = 'i',
181 completion_disable_special_value()
182 )]
183 pub integrand_name: Option<String>,
184
185 #[arg(long = "only-diagrams", short = 'o', default_value_t = false)]
187 pub only_diagrams: bool,
188
189 #[arg(long = "filter-selfenergies")]
191 pub filter_selfenergies: Option<bool>,
192 #[arg(long = "filter-snails")]
194 pub filter_snails: Option<bool>,
195 #[arg(long = "filter-tadpoles")]
197 pub filter_tadpoles: Option<bool>,
198
199 #[arg(long = "veto-vertex-interactions", num_args = 0..)]
201 pub veto_vertex_interactions: Option<Vec<String>>,
202
203 #[arg(long = "allowed-vertex-interactions", num_args = 0..)]
205 pub allowed_vertex_interactions: Option<Vec<String>>,
206
207 #[arg(long = "filter-cross-section-tadpoles")]
209 pub filter_cross_section_tadpoles: Option<bool>,
210
211 #[arg(long = "veto-tadpoles-attached-to-massive-lines")]
213 pub veto_tadpoles_attached_to_massive_lines: Option<bool>,
214 #[arg(long = "veto-tadpoles-attached-to-massless-lines")]
216 pub veto_tadpoles_attached_to_massless_lines: Option<bool>,
217 #[arg(long = "veto-only-scaleless-tadpoles")]
219 pub veto_only_scaleless_tadpoles: Option<bool>,
220
221 #[arg(long = "veto-snails-attached-to-massive-lines")]
223 pub veto_snails_attached_to_massive_lines: Option<bool>,
224 #[arg(long = "veto-snails-attached-to-massless-lines")]
226 pub veto_snails_attached_to_massless_lines: Option<bool>,
227 #[arg(long = "veto-only-scaleless-snails")]
229 pub veto_only_scaleless_snails: Option<bool>,
230
231 #[arg(long = "veto-self-energy-of-massive-lines")]
233 pub veto_self_energy_of_massive_lines: Option<bool>,
234 #[arg(long = "veto-self-energy-of-massless-lines")]
236 pub veto_self_energy_of_massless_lines: Option<bool>,
237 #[arg(long = "veto-only-scaleless-self-energy")]
239 pub veto_only_scaleless_self_energy: Option<bool>,
240
241 #[arg(long = "max-n-bridges", short = 'b', allow_negative_numbers = true)]
243 pub max_n_bridges: Option<i32>,
244 #[arg(
246 long = "number-of-factorized-loop-subtopologies",
247 short = 'f',
248 alias = "nfactl",
249 num_args = 2,
250 allow_negative_numbers = true
251 )]
252 pub number_of_factorized_loop_subtopologies: Option<Vec<i32>>,
253 #[arg(
255 long = "number-of-fermion-loops",
256 short = 'L',
257 num_args = 2,
258 allow_negative_numbers = true
259 )]
260 pub number_of_fermion_loops: Option<Vec<i32>>,
261
262 #[arg(long = "n-cut-blobs", short = 'B', num_args = 2)]
264 pub n_cut_blobs: Option<Vec<usize>>,
265 #[arg(long = "n-cut-spectators", short = 'S', num_args = 2)]
267 pub n_cut_spectators: Option<Vec<usize>>,
268
269 #[arg(
271 long = "allow-symmetrization-of-external-fermions-in-amplitudes",
272 alias = "symferm"
273 )]
274 pub allow_symmetrization_of_external_fermions_in_amplitudes: Option<bool>,
275 #[arg(long = "symmetrize-initial-states")]
277 pub symmetrize_initial_states: Option<bool>,
278 #[arg(long = "symmetrize-final-states")]
280 pub symmetrize_final_states: Option<bool>,
281 #[arg(long = "symmetrize-left-right-states")]
283 pub symmetrize_left_right_states: Option<bool>,
284
285 #[arg(long = "numerator-grouping", short = 'G', value_enum)]
287 pub numerator_aware_isomorphism_grouping: Option<GroupingChoice>,
288
289 #[arg(long = "numerical-samples-seed")]
291 pub numerical_samples_seed: Option<u16>,
292 #[arg(long = "number-of-samples-for-numerator-comparisons")]
294 pub number_of_samples_for_numerator_comparisons: Option<usize>,
295 #[arg(long = "consider-internal-masses-only-in-numerator-isomorphisms")]
297 pub consider_internal_masses_only_in_numerator_isomorphisms: Option<bool>,
298 #[arg(long = "fully-numerical-substitution-when-comparing-numerators")]
300 pub fully_numerical_substitution_when_comparing_numerators: Option<bool>,
301 #[arg(long = "compare-canonized-numerator")]
303 pub compare_canonized_numerator: Option<bool>,
304 #[arg(long = "symmetric-left-right-polarizations")]
306 pub symmetric_left_right_polarizations: Option<bool>,
307
308 #[arg(long = "loop-momentum-bases", value_name = "KEY=VALUE", num_args = 0.., value_parser = KvPair::from_str)]
315 pub loop_momentum_bases: Option<Vec<KvPair>>,
316 #[arg(long = "select-graphs", num_args = 0..)]
318 pub select_graphs: Option<Vec<String>>,
319 #[arg(long = "veto-graphs", num_args = 0..)]
321 pub veto_graphs: Option<Vec<String>>,
322 #[arg(long = "graph-prefix", short = 'g')]
324 pub graph_prefix: Option<String>,
325
326 #[arg(
328 long = "global-prefactor-projector",
329 value_name = "ATOM",
330 allow_hyphen_values = true
331 )]
332 pub global_prefactor_projector: Option<String>,
333
334 #[arg(
336 long = "global-prefactor-num",
337 value_name = "ATOM",
338 allow_hyphen_values = true
339 )]
340 pub global_prefactor_num: Option<String>,
341
342 #[arg(
344 long = "max-multiplicity-for-fast-cut-filter",
345 short = 'M',
346 default_value_t = 6usize
347 )]
348 pub max_multiplicity_for_fast_cut_filter: usize,
349
350 #[arg(long = "filter-self-loop")]
352 pub filter_self_loop: Option<bool>,
353
354 #[arg(long = "filter-zero-flow-edges")]
356 pub filter_zero_flow_edges: Option<bool>,
357}
358
359impl SpecArgs {
360 pub(crate) fn from_process_spec_string(spec: impl Into<String>) -> Self {
361 Self {
362 tokens: vec![spec.into()],
363 ..Self::default()
364 }
365 }
366}
367
368impl Default for SpecArgs {
369 fn default() -> Self {
370 Self {
371 tokens: Vec::new(),
372 append: false,
373 clear_existing_processes: false,
374 process_name: None,
375 integrand_name: None,
376 only_diagrams: false,
377 filter_selfenergies: None,
378 filter_snails: None,
379 filter_tadpoles: None,
380 veto_vertex_interactions: None,
381 allowed_vertex_interactions: None,
382 filter_cross_section_tadpoles: None,
383 veto_tadpoles_attached_to_massive_lines: None,
384 veto_tadpoles_attached_to_massless_lines: None,
385 veto_only_scaleless_tadpoles: None,
386 veto_snails_attached_to_massive_lines: None,
387 veto_snails_attached_to_massless_lines: None,
388 veto_only_scaleless_snails: None,
389 veto_self_energy_of_massive_lines: None,
390 veto_self_energy_of_massless_lines: None,
391 veto_only_scaleless_self_energy: None,
392 max_n_bridges: None,
393 number_of_factorized_loop_subtopologies: None,
394 number_of_fermion_loops: None,
395 n_cut_blobs: None,
396 n_cut_spectators: None,
397 allow_symmetrization_of_external_fermions_in_amplitudes: None,
398 symmetrize_initial_states: None,
399 symmetrize_final_states: None,
400 symmetrize_left_right_states: None,
401 numerator_aware_isomorphism_grouping: None,
402 numerical_samples_seed: None,
403 number_of_samples_for_numerator_comparisons: None,
404 consider_internal_masses_only_in_numerator_isomorphisms: None,
405 fully_numerical_substitution_when_comparing_numerators: None,
406 compare_canonized_numerator: None,
407 symmetric_left_right_polarizations: None,
408 loop_momentum_bases: None,
409 select_graphs: None,
410 veto_graphs: None,
411 graph_prefix: None,
412 global_prefactor_projector: None,
413 global_prefactor_num: None,
414 max_multiplicity_for_fast_cut_filter: 6,
415 filter_self_loop: None,
416 filter_zero_flow_edges: None,
417 }
418 }
419}
420
421fn format_generation_duration(duration: std::time::Duration) -> String {
424 if duration.as_secs() >= 60 {
425 let minutes = duration.as_secs() / 60;
426 let seconds = duration.as_secs_f64() - (minutes * 60) as f64;
427 format!("{minutes}m {seconds:.1}s")
428 } else if duration.as_secs_f64() >= 1.0 {
429 format!("{:.2}s", duration.as_secs_f64())
430 } else {
431 format!("{}ms", duration.as_millis())
432 }
433}
434
435fn format_generation_fraction(
436 numerator: std::time::Duration,
437 total: std::time::Duration,
438) -> String {
439 let percent = if total.is_zero() {
440 0.0
441 } else {
442 (numerator.as_secs_f64() / total.as_secs_f64()) * 100.0
443 };
444
445 if percent >= 10.0 {
446 format!("{percent:.0}%")
447 } else if percent >= 1.0 {
448 format!("{percent:.1}%")
449 } else {
450 format!("{percent:.2}%")
451 }
452}
453
454fn format_generation_memory(bytes: u64) -> String {
455 const KIB: f64 = 1024.0;
456 const MIB: f64 = KIB * 1024.0;
457 const GIB: f64 = MIB * 1024.0;
458 const TIB: f64 = GIB * 1024.0;
459
460 let value = bytes as f64;
461 if value < KIB {
462 format!("{bytes} B")
463 } else if value < MIB {
464 format!("{:.2} KiB", value / KIB)
465 } else if value < GIB {
466 format!("{:.2} MiB", value / MIB)
467 } else if value < TIB {
468 format!("{:.2} GiB", value / GIB)
469 } else {
470 format!("{:.2} TiB", value / TIB)
471 }
472}
473
474pub(crate) fn render_generation_summary(
475 reports: &[GeneratedGraphReport],
476 peak_ram_bytes: u64,
477 generation_cores: Option<usize>,
478 title: Option<&str>,
479) -> Option<String> {
480 if reports.is_empty() {
481 return None;
482 }
483
484 let mut sorted_reports = reports.to_vec();
485 sorted_reports.sort_by(|left, right| {
486 (
487 left.process_id,
488 left.integrand_name.as_str(),
489 left.graph_name.as_str(),
490 )
491 .cmp(&(
492 right.process_id,
493 right.integrand_name.as_str(),
494 right.graph_name.as_str(),
495 ))
496 });
497
498 let mut builder = Builder::new();
499 builder.push_record([
500 "integrand".bold().blue().to_string(),
501 "graph".bold().blue().to_string(),
502 "# evals".bold().blue().to_string(),
503 "expr build".bold().blue().to_string(),
504 "spenso".bold().blue().to_string(),
505 "symbolica eval".bold().blue().to_string(),
506 "compile".bold().blue().to_string(),
507 ]);
508
509 let mut total_stats = GraphGenerationStats::default();
510 for report in &sorted_reports {
511 total_stats.merge_in_place(&report.stats);
512 let expr_time = report.stats.expression_build_time();
513 let total_time = report.stats.total_time;
514 let expr_value = format!(
515 "{} ({})",
516 format_generation_duration(expr_time).magenta(),
517 format_generation_fraction(expr_time, total_time).cyan()
518 );
519 let spenso_value = format!(
520 "{} ({})",
521 format_generation_duration(report.stats.evaluator_spenso_time).magenta(),
522 format_generation_fraction(report.stats.evaluator_spenso_time, total_time).cyan()
523 );
524 let symbolica_value = format!(
525 "{} ({})",
526 format_generation_duration(report.stats.evaluator_symbolica_time).magenta(),
527 format_generation_fraction(report.stats.evaluator_symbolica_time, total_time).cyan()
528 );
529 let compile_value = format!(
530 "{} ({})",
531 format_generation_duration(report.stats.evaluator_compile_time).magenta(),
532 format_generation_fraction(report.stats.evaluator_compile_time, total_time).cyan()
533 );
534
535 builder.push_record([
536 report.integrand_name.yellow().to_string(),
537 report.graph_name.yellow().to_string(),
538 report
539 .stats
540 .evaluator_count
541 .to_string()
542 .yellow()
543 .to_string(),
544 expr_value,
545 spenso_value,
546 symbolica_value,
547 compile_value,
548 ]);
549 }
550
551 let total_time = total_stats.total_time;
552 let total_expr_time = total_stats.expression_build_time();
553 let total_expr_value = format!(
554 "{} ({})",
555 format_generation_duration(total_expr_time).magenta(),
556 format_generation_fraction(total_expr_time, total_time).cyan()
557 );
558 let total_spenso_value = format!(
559 "{} ({})",
560 format_generation_duration(total_stats.evaluator_spenso_time).magenta(),
561 format_generation_fraction(total_stats.evaluator_spenso_time, total_time).cyan()
562 );
563 let total_symbolica_value = format!(
564 "{} ({})",
565 format_generation_duration(total_stats.evaluator_symbolica_time).magenta(),
566 format_generation_fraction(total_stats.evaluator_symbolica_time, total_time).cyan()
567 );
568 let total_compile_value = format!(
569 "{} ({})",
570 format_generation_duration(total_stats.evaluator_compile_time).magenta(),
571 format_generation_fraction(total_stats.evaluator_compile_time, total_time).cyan()
572 );
573 builder.push_record([
574 "Total".bold().yellow().to_string(),
575 String::new(),
576 total_stats
577 .evaluator_count
578 .to_string()
579 .bold()
580 .yellow()
581 .to_string(),
582 total_expr_value,
583 total_spenso_value,
584 total_symbolica_value,
585 total_compile_value,
586 ]);
587
588 let mut table = builder.build();
589 let mut style = Theme::from_style(Style::rounded());
590 style.insert_horizontal_line(
591 sorted_reports.len() + 1,
592 HorizontalLine::inherit(Style::modern()),
593 );
594 table.with(style);
595 let mut sections = Vec::new();
596 if let Some(title) = title {
597 sections.push(title.bold().blue().to_string());
598 }
599 let resources = match generation_cores {
600 Some(cores) => {
601 format!(
602 "{} {} | {} {}",
603 "peak RAM".bold().blue(),
604 format_generation_memory(peak_ram_bytes).yellow(),
605 "cores".bold().blue(),
606 cores.to_string().yellow(),
607 )
608 }
609 None => format!(
610 "{} {}",
611 "peak RAM".bold().blue(),
612 format_generation_memory(peak_ram_bytes).yellow(),
613 ),
614 };
615 sections.push(resources);
616 sections.push(table.to_string());
617 Some(sections.join("\n"))
618}
619
620fn log_generation_summary_table(
621 reports: &[GeneratedGraphReport],
622 resources: GenerationResourceSummary,
623) {
624 if let Some(summary) = render_generation_summary(
625 reports,
626 resources.peak_ram_bytes,
627 Some(resources.generation_cores),
628 Some("Integrand generation summary"),
629 ) {
630 info!("\n{summary}");
631 }
632}
633
634fn remove_compiled_cpp_sources(root: &Path) -> Result<usize> {
635 let mut removed_count = 0usize;
636 for entry in WalkDir::new(root) {
637 let entry = entry?;
638 if !entry.file_type().is_file() {
639 continue;
640 }
641
642 let source_path = entry.path();
643 if source_path.extension() != Some(OsStr::new("cpp")) {
644 continue;
645 }
646
647 let library_path = source_path.with_extension("so");
648 if !library_path.is_file() {
649 continue;
650 }
651
652 fs::remove_file(source_path).with_context(|| {
653 format!(
654 "Trying to remove generated C++ source {} after compiling {}",
655 source_path.display(),
656 library_path.display()
657 )
658 })?;
659 removed_count += 1;
660 }
661
662 Ok(removed_count)
663}
664
665fn compile_integrands_for_generation(
666 state: &mut State,
667 compile_folder: &Path,
668 override_existing_compiled: bool,
669 global_settings: &GlobalSettings,
670 process_id: Option<usize>,
671 integrand_name: Option<String>,
672 keep_sources: bool,
673) -> Result<Vec<GeneratedGraphReport>> {
674 let reports = state.compile_integrands(
675 compile_folder,
676 override_existing_compiled,
677 global_settings,
678 process_id,
679 integrand_name,
680 )?;
681
682 if !keep_sources {
683 let removed_count = remove_compiled_cpp_sources(compile_folder)?;
684 if removed_count > 0 {
685 info!(
686 "Removed {} generated C++ source file(s). Use '{}' to keep them.",
687 removed_count,
688 "--keep-sources".green()
689 );
690 }
691 }
692
693 Ok(reports)
694}
695
696fn finish_generation(
697 state: &mut State,
698 reports: &[GeneratedGraphReport],
699 resources: GenerationResourceSummary,
700) {
701 state.record_generation_summary(reports, resources);
702 log_generation_summary_table(reports, resources);
703}
704
705impl Generate {
706 pub fn run(
707 &self,
708 state: &mut State,
709 compile_folder: impl AsRef<Path>,
710 override_existing_compiled: bool,
711 global_settings: &GlobalSettings,
712 runtime_settings: &RuntimeSettings,
713 ) -> Result<()> {
714 let compile_folder = compile_folder.as_ref();
715 let generation_mode = match &self.mode {
716 Some(GenerateCmd::Xs(a)) => Some((GenerationType::CrossSection, a)),
717 Some(GenerateCmd::Amp(a)) => Some((GenerationType::Amplitude, a)),
718 _ => None,
719 };
720 if let Some((generation_type, args)) = generation_mode.as_ref() {
721 global_settings
722 .generation
723 .validate_for_process(*generation_type)?;
724 if !state.process_list.processes.is_empty() && args.clear_existing_processes {
725 info!(
726 "Clearing all {} existing processes as requested.",
727 state.process_list.processes.len()
728 );
729 state.process_list = ProcessList::default();
730 }
731 }
732 let generation_info = if let Some((gen_mode, args)) = generation_mode {
733 let mut spec = parse_spec_with_model(args, gen_mode, &state.model)?;
734 spec.process_definition.process_id = state.process_list.processes.len();
735
736 let mut existing_process = None;
737 if let Some(ep) = state
738 .process_list
739 .processes
740 .iter_mut()
741 .find(|p| p.definition.folder_name == spec.process_definition.folder_name)
742 {
743 if ep.definition != spec.process_definition {
744 if !args.append {
745 return Err(eyre!(
746 "Process with name '{}' already exists.\n> Use 'existing' subcommand to continue generation of this process.\n> Use '--clear-existing-processes' to remove all existing ones.\n> Or specify a different process name with '--process-name <chosen_process_name>'.",
747 spec.process_definition.folder_name
748 ));
749 }
750 } else {
751 info!(
752 "Identical process definition, with name '{}', already exists. Gammaloop will recycle it.",
753 spec.process_definition.folder_name
754 );
755 return Ok(());
756 }
757 spec.process_definition.process_id = ep.definition.process_id;
758 existing_process = Some(ep);
759 }
760 Some((spec, existing_process))
761 } else {
762 None
763 };
764 match &self.mode {
765 Some(GenerateCmd::Amp(args)) | Some(GenerateCmd::Xs(args)) => {
766 let generation_type = generation_mode.as_ref().unwrap().0;
767 let model: &Model = &state.model;
768 let (spec, existing_process) = generation_info.unwrap();
769 let this_process_id = spec.process_definition.process_id;
770 let graphs = spec.process_definition.generate(model, global_settings)?;
772 info!(
773 "Generated {} {} graphs.",
774 if matches!(self.mode, Some(GenerateCmd::Amp(_))) {
775 "amplitude"
776 } else {
777 "cross-section"
778 },
779 graphs.len()
780 );
781 let integrand_base_name = matches!(self.mode, Some(GenerateCmd::Amp(_)))
783 .then(|| args.integrand_name.clone().unwrap_or("default".to_string()))
784 .unwrap_or_else(|| {
785 args.integrand_name.clone().unwrap_or("default".to_string())
786 });
787 let generated_integrand_name = if let Some(p) = existing_process {
788 let existing_names = p.collection.get_integrand_names();
789 let integrand_name = if existing_names.contains(&integrand_base_name.as_str()) {
790 let mut integrand_i = 0;
791 while existing_names
792 .iter()
793 .any(|ce| *ce == format!("{}_{}", integrand_base_name, integrand_i))
794 {
795 integrand_i += 1;
796 }
797 format!("{}_{}", integrand_base_name, integrand_i)
798 } else {
799 integrand_base_name
800 };
801 match &self.mode {
802 Some(GenerateCmd::Amp(_)) => {
803 p.collection.add_amplitude(Amplitude::from_graph_list(
804 integrand_name.clone(),
805 graphs,
806 )?);
807 }
808 Some(GenerateCmd::Xs(_)) => {
809 p.collection
810 .add_cross_section(CrossSection::from_graph_list(
811 integrand_name.clone(),
812 graphs,
813 model,
814 )?);
815 }
816 _ => unreachable!(),
817 }
818 integrand_name
819 } else {
820 let process = Process::from_graph_list(
821 spec.process_definition.folder_name.clone(),
822 integrand_base_name.clone(),
823 graphs,
824 generation_type,
825 Some(spec.process_definition),
826 None,
827 model,
828 )?;
829 state.process_list.add_process(process);
830 integrand_base_name
831 };
832 if !args.only_diagrams {
833 let generated_integrand_name_for_compile = generated_integrand_name.clone();
834 let generation = state.generate_integrand(
835 global_settings,
836 runtime_settings.into(),
837 this_process_id,
838 Some(generated_integrand_name),
839 )?;
840 let mut reports = generation.reports;
841 if global_settings.generation.evaluator.compile
842 && global_settings
843 .generation
844 .compile
845 .requires_external_compilation()
846 {
847 merge_generated_graph_reports(
848 &mut reports,
849 compile_integrands_for_generation(
850 state,
851 compile_folder,
852 override_existing_compiled,
853 global_settings,
854 Some(this_process_id),
855 Some(generated_integrand_name_for_compile),
856 self.keep_sources,
857 )?,
858 );
859 }
860 finish_generation(state, &reports, generation.resources);
861 Ok(())
862 } else {
863 info!(
864 "Only diagram generation was requested, skipping integrand generation. You can generate integrands later using the '{}' command.",
865 "generate existing <options>".green()
866 );
867 Ok(())
868 }
869 }
870 Some(GenerateCmd::Existing(process_args)) => {
871 let process_id = state.resolve_process_ref(process_args.process.as_ref())?;
872
873 let generation = state.generate_integrand(
874 global_settings,
875 runtime_settings.into(),
876 process_id,
877 process_args.integrand_name.clone(),
878 )?;
879 let mut reports = generation.reports;
880 if global_settings.generation.evaluator.compile
881 && global_settings
882 .generation
883 .compile
884 .requires_external_compilation()
885 {
886 merge_generated_graph_reports(
887 &mut reports,
888 compile_integrands_for_generation(
889 state,
890 compile_folder,
891 override_existing_compiled,
892 global_settings,
893 Some(process_id),
894 process_args.integrand_name.clone(),
895 self.keep_sources,
896 )?,
897 );
898 }
899 finish_generation(state, &reports, generation.resources);
900 Ok(())
901 }
902 None => {
903 let generation =
904 state.generate_integrands(global_settings, runtime_settings.into())?;
905 let mut reports = generation.reports;
906 if global_settings.generation.evaluator.compile
907 && global_settings
908 .generation
909 .compile
910 .requires_external_compilation()
911 {
912 merge_generated_graph_reports(
913 &mut reports,
914 compile_integrands_for_generation(
915 state,
916 compile_folder,
917 override_existing_compiled,
918 global_settings,
919 None,
920 None,
921 self.keep_sources,
922 )?,
923 );
924 }
925 finish_generation(state, &reports, generation.resources);
926 Ok(())
927 }
928 }
929 }
930}
931
932#[derive(Args, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
933pub struct ProcessArgs {
934 #[arg(
936 long = "process",
937 short = 'p',
938 value_name = "PROCESS",
939 completion_process_selector(crate::completion::SelectorKind::Any)
940 )]
941 pub process: Option<ProcessRef>,
942
943 #[arg(
945 long = "integrand-name",
946 short = 'i',
947 completion_integrand_selector(crate::completion::SelectorKind::Any)
948 )]
949 pub integrand_name: Option<String>,
950}
951
952#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
955pub struct OrderRange {
956 pub eq: Option<u32>,
957 pub min: Option<u32>,
958 pub max: Option<u32>,
959}
960impl OrderRange {
961 fn set(&mut self, op: &str, v: u32) {
962 match op {
963 "==" => self.eq = Some(v),
964 ">=" => self.min = Some(v),
965 "<=" => self.max = Some(v),
966 _ => {}
967 }
968 }
969}
970
971#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord)]
972pub struct CouplingKey {
973 pub name: String,
974 pub power: u32, }
976
977#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
978pub struct Perturbative {
979 pub loops_sum_amp_or_sum: Option<u32>,
981 pub loops_forward_graph: Option<u32>,
983 pub orders: BTreeMap<String, u32>,
985}
986
987#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
988pub struct ProcessSpec {
989 pub initial: Vec<String>,
990 pub final_: Vec<String>,
991 pub empty_initial: bool,
992 pub empty_final: bool,
993
994 pub veto: BTreeSet<String>,
996 pub only: Option<BTreeSet<String>>,
998
999 pub amp_couplings: BTreeMap<String, OrderRange>,
1001 pub xs_couplings: BTreeMap<CouplingKey, OrderRange>,
1003
1004 pub pert: Perturbative,
1006
1007 pub final_sets: Vec<Vec<String>>,
1009
1010 pub process_definition: ProcessDefinition,
1012
1013 pub numerator_grouping: NumeratorAwareGraphGroupingOption,
1015}
1016
1017impl ProcessSpec {
1018 pub fn process_shell_name(&self, short: bool) -> String {
1020 let sanitize_piece = |s: &str| s.replace('~', "x").replace('+', "p").replace('-', "m");
1022 let lower = |s: &str| s.to_ascii_lowercase();
1023
1024 let init_slug = if self.initial.is_empty() {
1026 "empty".to_string()
1027 } else {
1028 self.initial.iter().map(|p| lower(p)).collect::<String>()
1029 };
1030
1031 let finals_lists: Vec<Vec<String>> = if self.final_sets.is_empty() {
1032 vec![self.final_.clone()]
1033 } else {
1034 self.final_sets.clone()
1035 };
1036 let finals_slugs: Vec<String> = finals_lists
1037 .iter()
1038 .map(|lst| {
1039 if lst.is_empty() {
1040 "empty".to_string()
1041 } else {
1042 lst.iter().map(|p| lower(p)).collect::<String>()
1043 }
1044 })
1045 .collect();
1046
1047 let mut pieces: Vec<String> = vec![sanitize_piece(&format!(
1048 "{}_{}",
1049 init_slug,
1050 finals_slugs.join("_or_")
1051 ))];
1052
1053 if !short {
1054 if !self.veto.is_empty() {
1056 let mut veto_list: Vec<String> = self.veto.iter().map(|v| lower(v)).collect();
1057 veto_list.sort();
1058 pieces.push(sanitize_piece(&format!("no__{}", veto_list.join("_"))));
1059 }
1060
1061 if !self.amp_couplings.is_empty() {
1063 let mut keys: Vec<&String> = self.amp_couplings.keys().collect();
1064 keys.sort();
1065 let mut parts: Vec<String> = Vec::new();
1066 for k in keys {
1067 let r = &self.amp_couplings[k];
1068 if let Some(eq) = r.eq {
1069 parts.push(format!("{}_eq_{}", k, eq));
1070 } else {
1071 if let Some(min) = r.min {
1072 if min > 0 {
1073 parts.push(format!("{}_ge_{}", k, min));
1074 }
1075 }
1076 if let Some(max) = r.max {
1077 parts.push(format!("{}_le_{}", k, max));
1078 }
1079 }
1080 }
1081 if !parts.is_empty() {
1082 pieces.push(parts.join("__"));
1083 }
1084 }
1085
1086 if !self.xs_couplings.is_empty() {
1088 let mut entries: Vec<(&CouplingKey, &OrderRange)> =
1089 self.xs_couplings.iter().collect();
1090 entries.sort_by(|(ka, _), (kb, _)| {
1091 use std::cmp::Ordering;
1092 match ka.name.cmp(&kb.name) {
1093 Ordering::Equal => ka.power.cmp(&kb.power),
1094 other => other,
1095 }
1096 });
1097 let mut parts: Vec<String> = Vec::new();
1098 for (k, r) in entries {
1099 let base = format!("{}sq", k.name);
1100 if let Some(eq) = r.eq {
1101 parts.push(format!("{}_eq_{}", base, eq));
1102 } else {
1103 if let Some(min) = r.min {
1104 if min > 0 {
1105 parts.push(format!("{}_ge_{}", base, min));
1106 }
1107 }
1108 if let Some(max) = r.max {
1109 parts.push(format!("{}_le_{}", base, max));
1110 }
1111 }
1112 }
1113 if !parts.is_empty() {
1114 pieces.push(parts.join("__"));
1115 }
1116 }
1117
1118 if !self.pert.orders.is_empty() {
1120 let mut ords: Vec<(&String, &u32)> = self.pert.orders.iter().collect();
1121 ords.sort_by(|a, b| a.0.cmp(b.0));
1122 let parts: Vec<String> = ords
1123 .into_iter()
1124 .map(|(k, v)| format!("{}loop_eq_{}", k, v))
1125 .collect();
1126 if !parts.is_empty() {
1127 pieces.push(parts.join("__"));
1128 }
1129 }
1130 }
1131
1132 pieces.join("-")
1133 }
1134
1135 pub fn repr_str(&self) -> String {
1137 let lower = |s: &str| s.to_ascii_lowercase();
1138
1139 let mut out: Vec<String> = Vec::new();
1140
1141 if self.initial.is_empty() {
1143 out.push("{}".to_string());
1144 } else {
1145 for p in &self.initial {
1146 out.push(lower(p));
1147 }
1148 }
1149
1150 out.push(">".to_string());
1152
1153 if self.final_sets.is_empty() {
1155 if self.final_.is_empty() {
1156 out.push("{}".to_string());
1157 } else {
1158 for p in &self.final_ {
1159 out.push(lower(p));
1160 }
1161 }
1162 } else {
1163 let sets_str = self
1165 .final_sets
1166 .iter()
1167 .map(|set| {
1168 if set.is_empty() {
1169 "empty".to_string()
1170 } else {
1171 set.iter().map(|p| lower(p)).collect::<Vec<_>>().join(" ")
1172 }
1173 })
1174 .collect::<Vec<_>>()
1175 .join(", ");
1176 out.push(format!("{{ {} }}", sets_str));
1177 }
1178
1179 if !self.veto.is_empty() {
1181 out.push("/".to_string());
1182 let mut veto_list: Vec<String> = self.veto.iter().map(|v| lower(v)).collect();
1183 veto_list.sort();
1184 out.extend(veto_list);
1185 }
1186
1187 if !self.amp_couplings.is_empty() {
1189 let mut keys: Vec<&String> = self.amp_couplings.keys().collect();
1190 keys.sort();
1191 for k in keys {
1192 let r = &self.amp_couplings[k];
1193 if let Some(eq) = r.eq {
1194 out.push(format!("{}=={}", k, eq));
1195 } else {
1196 if let Some(min) = r.min {
1197 if min > 0 {
1198 out.push(format!("{}>={}", k, min));
1199 }
1200 }
1201 if let Some(max) = r.max {
1202 out.push(format!("{}<={}", k, max));
1203 }
1204 }
1205 }
1206 }
1207
1208 let has_pert_block = self.pert.loops_sum_amp_or_sum.is_some()
1210 || self.pert.loops_forward_graph.is_some()
1211 || !self.pert.orders.is_empty();
1212 if has_pert_block {
1213 out.push("[".to_string());
1214 if let Some(n) = self.pert.loops_sum_amp_or_sum {
1215 out.push(format!("{{{}}}", n));
1216 }
1217 if let Some(n) = self.pert.loops_forward_graph {
1218 out.push(format!("{{{{{}}}}}", n));
1219 }
1220 if !self.pert.orders.is_empty() {
1221 let mut ords: Vec<(&String, &u32)> = self.pert.orders.iter().collect();
1222 ords.sort_by(|a, b| a.0.cmp(b.0));
1223 for (k, v) in ords {
1224 if *v > 0 {
1225 if *v == 1 {
1226 out.push(k.clone());
1227 } else {
1228 out.push(format!("{}={}", k, v));
1229 }
1230 }
1231 }
1232 }
1233 out.push("]".to_string());
1234 }
1235
1236 if !self.xs_couplings.is_empty() {
1238 let mut entries: Vec<(&CouplingKey, &OrderRange)> = self.xs_couplings.iter().collect();
1239 entries.sort_by(|(ka, _), (kb, _)| {
1240 use std::cmp::Ordering;
1241 match ka.name.cmp(&kb.name) {
1242 Ordering::Equal => ka.power.cmp(&kb.power),
1243 other => other,
1244 }
1245 });
1246 for (k, r) in entries {
1247 if let Some(eq) = r.eq {
1248 out.push(format!("{}^{}=={}", k.name, k.power, eq));
1249 } else {
1250 if let Some(min) = r.min {
1251 if min > 0 {
1252 out.push(format!("{}^{}>={}", k.name, k.power, min));
1253 }
1254 }
1255 if let Some(max) = r.max {
1256 out.push(format!("{}^{}<={}", k.name, k.power, max));
1257 }
1258 }
1259 }
1260 }
1261
1262 out.join(" ")
1263 }
1264}
1265
1266#[derive(Debug, Error, PartialEq, Eq)]
1269pub enum ParseError {
1270 #[error("expected 'to' or '>' between initial and final states")]
1271 MissingArrow,
1272 #[error("unbalanced brackets/braces")]
1273 Unbalanced,
1274 #[error("invalid token '{0}'")]
1275 InvalidToken(String),
1276 #[error("unknown particle '{name}'. Valid choices: {choices}")]
1277 UnknownParticle { name: String, choices: String },
1278 #[error("unknown coupling '{name}'. Valid choices: {choices}")]
1279 UnknownCoupling { name: String, choices: String },
1280 #[error("invalid lmb specification: {0}")]
1281 InvalidLmbSpec(ParseIntError),
1282}
1283
1284pub fn parse_spec_with_model(
1285 args: &SpecArgs,
1286 generation_type: GenerationType,
1287 model: &Model,
1288) -> std::result::Result<ProcessSpec, ParseError> {
1289 let raw = args.tokens.join(" ");
1290 let (lhs, rhs0) = split_top_level_arrow(&raw).ok_or(ParseError::MissingArrow)?;
1291 let (rhs_wo_bracket, pert) = extract_perturbative(rhs0.trim())?;
1292
1293 let lhs = lhs.trim();
1295 let (initial_names, empty_initial) = if lhs == "{}" || lhs.eq_ignore_ascii_case("empty-initial")
1296 {
1297 (Vec::new(), true)
1298 } else {
1299 (split_ws(lhs), false)
1300 };
1301
1302 let tokens = tokenize(rhs_wo_bracket)?;
1304 let (final_spec, empty_final, rest) = parse_final_states(tokens)?;
1305 let (veto_names, only, amp_couplings, xs_couplings) = parse_process_options(rest)?;
1306
1307 validate_coupling_names(model, &_couplings, &xs_couplings, &pert)?;
1309
1310 let initial_pdgs = resolve_pdgs(model, &initial_names)?;
1312 let primary_final_pdgs = resolve_pdgs(model, &final_spec.primary)?;
1313 let final_sets_pdgs = final_spec
1314 .sets
1315 .iter()
1316 .map(|set| resolve_pdgs(model, set))
1317 .collect::<Result<Vec<_>, _>>()?;
1318
1319 let mut veto_pdgs = resolve_pdgs(model, &veto_names.iter().cloned().collect::<Vec<_>>())?;
1321 if let Some(o) = only.clone() {
1322 let only_vec = o.iter().cloned().collect::<Vec<_>>();
1323 if !only_vec.is_empty() {
1324 let only_pdgs = resolve_pdgs(model, &only_vec)?
1325 .iter()
1326 .map(|pdg| pdg.abs())
1327 .collect::<Vec<_>>();
1328
1329 let all_pdgs = model
1330 .particles
1331 .iter()
1332 .map(|p| p.pdg_code.abs() as i64)
1333 .filter(|pdg| !only_pdgs.contains(pdg))
1334 .collect::<Vec<_>>();
1335
1336 veto_pdgs.extend(all_pdgs);
1337 veto_pdgs.sort_unstable();
1338 veto_pdgs.dedup();
1339 }
1340 }
1341
1342 let numerator_grouping = build_grouping_option(args);
1344 let process_definition = feyngen_from_spec_args(
1345 args,
1346 generation_type,
1347 &pert,
1348 &_couplings,
1349 &xs_couplings,
1350 &initial_pdgs,
1351 &primary_final_pdgs,
1352 &final_sets_pdgs,
1353 &veto_pdgs,
1354 );
1355
1356 let mut spec = ProcessSpec {
1357 initial: initial_names,
1358 final_: final_spec.primary.clone(),
1359 empty_initial,
1360 empty_final,
1361 veto: veto_names,
1362 only,
1363 amp_couplings,
1364 xs_couplings,
1365 pert,
1366 final_sets: final_spec.sets,
1367 process_definition,
1368 numerator_grouping,
1369 };
1370
1371 if let Some(process_name) = &args.process_name {
1372 spec.process_definition.folder_name = process_name.clone();
1373 } else {
1374 spec.process_definition.folder_name = spec.process_shell_name(true);
1375 }
1376 spec.process_definition.numerator_grouping = spec.numerator_grouping.clone();
1377 spec.process_definition.filter_self_loop = args.filter_self_loop.unwrap_or(false);
1378 spec.process_definition.filter_zero_flow_edges = args.filter_zero_flow_edges.unwrap_or(true);
1379
1380 spec.process_definition.graph_prefix = args
1381 .graph_prefix
1382 .clone()
1383 .unwrap_or_else(|| "GL".to_string());
1384 spec.process_definition.selected_graphs = args.select_graphs.clone();
1388 spec.process_definition.vetoed_graphs = args.veto_graphs.clone();
1389 spec.process_definition.loop_momentum_bases = args
1390 .loop_momentum_bases
1391 .as_deref()
1392 .map(|kvs| {
1393 kvs.iter()
1394 .map(|kv| {
1395 parse_csv_list(&kv.value)
1396 .into_iter()
1397 .map(|s| s.parse::<usize>())
1398 .collect::<core::result::Result<Vec<_>, _>>()
1399 .map(|lst| (kv.key.clone(), lst))
1400 })
1401 .collect::<core::result::Result<HashMap<_, _>, _>>()
1402 })
1403 .transpose()
1404 .map_err(ParseError::InvalidLmbSpec)?;
1405 let mut prefactor = GlobalPrefactor::default();
1406 if let Some(projector) = &args.global_prefactor_projector {
1407 prefactor.projector = parse!(projector);
1408 }
1409 if let Some(num) = &args.global_prefactor_num {
1410 prefactor.num = parse!(num);
1411 }
1412 spec.process_definition.prefactor = prefactor;
1413
1414 Ok(spec)
1415}
1416
1417pub(crate) fn parse_process_spec_string(
1418 spec: &str,
1419 generation_type: GenerationType,
1420 model: &Model,
1421) -> std::result::Result<ProcessSpec, ParseError> {
1422 let args = SpecArgs::from_process_spec_string(spec);
1423 parse_spec_with_model(&args, generation_type, model)
1424}
1425
1426fn split_top_level_arrow(s: &str) -> Option<(&str, &str)> {
1429 let mut depth_brace = 0i32;
1431 let mut depth_bracket = 0i32;
1432 let bytes = s.as_bytes();
1433 let mut i = 0usize;
1434 while i < bytes.len() {
1435 let c = bytes[i] as char;
1436 match c {
1437 '{' => depth_brace += 1,
1438 '}' => depth_brace -= 1,
1439 '[' => depth_bracket += 1,
1440 ']' => depth_bracket -= 1,
1441 '>' if depth_brace == 0 && depth_bracket == 0 => {
1442 return Some((&s[..i], &s[i + 1..]));
1443 }
1444 _ => {
1445 if depth_brace == 0
1446 && depth_bracket == 0
1447 && i + 2 < bytes.len()
1448 && (c == 't' || c == 'T')
1449 && &s[i..].to_ascii_lowercase().as_str()[..2] == "to"
1450 {
1451 let before = if i == 0 {
1452 ' '
1453 } else {
1454 s.as_bytes()[i - 1] as char
1455 };
1456 let after = if i + 2 >= s.len() {
1457 ' '
1458 } else {
1459 s.as_bytes()[i + 2] as char
1460 };
1461 if before.is_whitespace() && after.is_whitespace() {
1462 return Some((&s[..i], &s[i + 2..]));
1463 }
1464 }
1465 }
1466 }
1467 i += 1;
1468 }
1469 None
1470}
1471
1472fn extract_perturbative(rhs: &str) -> Result<(String, Perturbative), ParseError> {
1473 let mut out = rhs.to_string();
1474 let mut spec = Perturbative::default();
1475 if let Some((start, end)) = find_top_level_block(rhs, '[', ']') {
1476 let inside = &rhs[start + 1..end];
1477 spec = parse_perturbative_block(inside.trim())?;
1478 out.replace_range(start..=end, "");
1479 }
1480 Ok((out, spec))
1481}
1482
1483fn parse_perturbative_block(s: &str) -> Result<Perturbative, ParseError> {
1484 if s.is_empty() {
1485 return Ok(Perturbative::default());
1486 }
1487 let re_amp = Regex::new(r"^\{(\d+)\}$").unwrap();
1488 let re_fwd = Regex::new(r"^\{\{(\d+)\}\}$").unwrap();
1489 let re_order = Regex::new(r"^([A-Za-z_][A-Za-z0-9_]*)=(\d+)$").unwrap();
1490
1491 let mut spec = Perturbative::default();
1492 for tok in split_ws(s) {
1493 if let Some(c) = re_amp.captures(&tok) {
1494 spec.loops_sum_amp_or_sum = Some(c[1].parse().unwrap());
1495 continue;
1496 }
1497 if let Some(c) = re_fwd.captures(&tok) {
1498 spec.loops_forward_graph = Some(c[1].parse().unwrap());
1499 continue;
1500 }
1501 if let Some(c) = re_order.captures(&tok) {
1502 let n: u32 = c[2].parse().unwrap();
1503 spec.orders.insert(c[1].to_string(), n);
1504 continue;
1505 }
1506 if tok.chars().all(|ch| ch.is_ascii_alphabetic()) {
1507 spec.orders.insert(tok, 1);
1508 continue;
1509 }
1510 return Err(ParseError::InvalidToken(tok));
1511 }
1512 Ok(spec)
1513}
1514
1515fn tokenize(s: String) -> Result<Vec<String>, ParseError> {
1516 let mut out = Vec::new();
1517 let mut cur = String::new();
1518 let mut depth = 0i32;
1519 for ch in s.chars() {
1520 match ch {
1521 '{' => {
1522 depth += 1;
1523 cur.push(ch);
1524 }
1525 '}' => {
1526 depth -= 1;
1527 cur.push(ch);
1528 if depth < 0 {
1529 return Err(ParseError::Unbalanced);
1530 }
1531 }
1532 c if c.is_whitespace() && depth == 0 => {
1533 if !cur.trim().is_empty() {
1534 out.push(cur.trim().to_string());
1535 }
1536 cur.clear();
1537 }
1538 _ => cur.push(ch),
1539 }
1540 }
1541 if !cur.trim().is_empty() {
1542 out.push(cur.trim().to_string());
1543 }
1544 if depth != 0 {
1545 return Err(ParseError::Unbalanced);
1546 }
1547 Ok(out)
1548}
1549
1550fn parse_final_states(tokens: Vec<String>) -> Result<(FinalSpec, bool, Vec<String>), ParseError> {
1551 if tokens.is_empty() {
1552 return Ok((FinalSpec::default(), false, vec![]));
1553 }
1554 let mut t = tokens;
1555 let mut finals = Vec::<String>::new();
1556 let mut empty_final = false;
1557
1558 if t[0].eq_ignore_ascii_case("empty-final") || t[0] == "{}" {
1559 empty_final = true;
1560 t.remove(0);
1561 return Ok((FinalSpec::default(), empty_final, t));
1562 }
1563
1564 if t[0].starts_with('{') {
1565 let raw = t.remove(0);
1566 let sets = parse_alt_sets(&raw)?;
1567 return Ok((
1568 FinalSpec {
1569 primary: vec![],
1570 sets,
1571 },
1572 empty_final,
1573 t,
1574 ));
1575 }
1576
1577 while !t.is_empty() {
1578 if is_option_token(&t[0]) {
1579 break;
1580 }
1581 finals.push(t.remove(0));
1582 }
1583 Ok((
1584 FinalSpec {
1585 primary: finals,
1586 sets: vec![],
1587 },
1588 empty_final,
1589 t,
1590 ))
1591}
1592
1593#[derive(Default)]
1594struct FinalSpec {
1595 primary: Vec<String>,
1596 sets: Vec<Vec<String>>,
1597}
1598
1599fn parse_alt_sets(s: &str) -> Result<Vec<Vec<String>>, ParseError> {
1600 let trimmed = s.trim();
1601 if !(trimmed.starts_with('{') && trimmed.ends_with('}')) {
1602 return Err(ParseError::InvalidToken(s.to_string()));
1603 }
1604 let inner = &trimmed[1..trimmed.len() - 1];
1605 let mut out: Vec<Vec<String>> = Vec::new();
1606 let mut cur = String::new();
1607 let mut depth = 0i32;
1608 for ch in inner.chars() {
1609 match ch {
1610 '{' => {
1611 depth += 1;
1612 cur.push(ch);
1613 }
1614 '}' => {
1615 depth -= 1;
1616 if depth < 0 {
1617 return Err(ParseError::Unbalanced);
1618 }
1619 cur.push(ch);
1620 }
1621 ',' if depth == 0 => {
1622 let items = split_ws(cur.trim());
1623 if !items.is_empty() {
1624 out.push(items);
1625 } else {
1626 out.push(vec![]);
1627 }
1628 cur.clear();
1629 }
1630 _ => cur.push(ch),
1631 }
1632 }
1633 if !cur.trim().is_empty() {
1634 out.push(split_ws(cur.trim()));
1635 }
1636 Ok(out)
1637}
1638
1639fn is_option_token(tok: &str) -> bool {
1640 matches!(
1641 tok,
1642 "/" | "|" | "veto" | "only" | "amp" | "xs" | "pert" | "sets" | "empty-final"
1643 ) || tok.starts_with('[')
1644 || Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*(?:\^\d+)?(==|>=|<=)\d+$")
1645 .unwrap()
1646 .is_match(tok)
1647}
1648
1649#[allow(clippy::type_complexity)]
1650fn parse_process_options(
1651 tokens: Vec<String>,
1652) -> Result<
1653 (
1654 BTreeSet<String>,
1655 Option<BTreeSet<String>>,
1656 BTreeMap<String, OrderRange>,
1657 BTreeMap<CouplingKey, OrderRange>,
1658 ),
1659 ParseError,
1660> {
1661 let mut veto = BTreeSet::<String>::new();
1662 let mut only: Option<BTreeSet<String>> = None;
1663 let mut amp = BTreeMap::<String, OrderRange>::new();
1664 let mut xs = BTreeMap::<CouplingKey, OrderRange>::new();
1665
1666 let re = Regex::new(r"^([A-Za-z_][A-Za-z0-9_]*)(?:\^(\d+))?(==|>=|<=)(\d+)$").unwrap();
1667
1668 let mut i = 0usize;
1669 while i < tokens.len() {
1670 match tokens[i].as_str() {
1671 "/" | "veto" => {
1672 i += 1;
1673 while i < tokens.len() && !is_option_token(&tokens[i]) {
1674 veto.insert(tokens[i].clone());
1675 i += 1;
1676 }
1677 }
1678 "|" | "only" => {
1679 i += 1;
1680 let mut s = only.take().unwrap_or_default();
1681 while i < tokens.len() && !is_option_token(&tokens[i]) {
1682 s.insert(tokens[i].clone());
1683 i += 1;
1684 }
1685 only = Some(s);
1686 }
1687 "amp" | "xs" | "pert" | "sets" | "empty-final" => {
1688 return Err(ParseError::InvalidToken(tokens[i].clone()));
1689 }
1690 other => {
1691 if let Some(c) = re.captures(other) {
1692 let name = c[1].to_string();
1693 let power: u32 = c.get(2).map(|m| m.as_str().parse().unwrap()).unwrap_or(1);
1694 let op = c.get(3).unwrap().as_str();
1695 let val: u32 = c.get(4).unwrap().as_str().parse().unwrap();
1696 if power == 1 {
1697 let e = amp.entry(name).or_default();
1698 e.set(op, val);
1699 } else {
1700 let key = CouplingKey { name, power };
1701 let e = xs.entry(key).or_default();
1702 e.set(op, val);
1703 }
1704 } else {
1705 return Err(ParseError::InvalidToken(other.to_string()));
1706 }
1707 i += 1;
1708 }
1709 }
1710 }
1711 Ok((veto, only, amp, xs))
1712}
1713
1714fn find_top_level_block(s: &str, open: char, close: char) -> Option<(usize, usize)> {
1715 let mut depth = 0i32;
1716 let mut start: Option<usize> = None;
1717 for (i, ch) in s.char_indices() {
1718 if ch == open {
1719 if depth == 0 {
1720 start = Some(i);
1721 }
1722 depth += 1;
1723 } else if ch == close {
1724 depth -= 1;
1725 if depth == 0 {
1726 return start.map(|st| (st, i));
1727 }
1728 }
1729 }
1730 None
1731}
1732
1733fn split_ws(s: &str) -> Vec<String> {
1734 s.split_whitespace().map(|t| t.to_string()).collect()
1735}
1736
1737fn feyngen_from_spec_args(
1740 a: &SpecArgs,
1741 generation_type: GenerationType,
1742 pert: &Perturbative,
1743 amp_couplings: &BTreeMap<String, OrderRange>,
1744 xs_couplings: &BTreeMap<CouplingKey, OrderRange>,
1745 initial_pdgs: &[i64],
1746 primary_final_pdgs: &[i64],
1747 final_sets_pdgs: &[Vec<i64>],
1748 veto_pdgs: &[i64],
1749) -> ProcessDefinition {
1750 let is_vacuum = if initial_pdgs.is_empty() {
1752 match generation_type {
1753 GenerationType::CrossSection => true,
1754 GenerationType::Amplitude => {
1755 primary_final_pdgs.is_empty()
1756 && (final_sets_pdgs
1757 .first()
1758 .map(|v| v.is_empty())
1759 .unwrap_or(true))
1760 }
1761 }
1762 } else {
1763 false
1764 };
1765
1766 let filter_tadpoles_default = !is_vacuum;
1768 let filter_snails_default = !is_vacuum;
1769 let filter_selfenergies_default = !is_vacuum;
1770
1771 let filter_tadpoles = a.filter_tadpoles.unwrap_or(filter_tadpoles_default);
1773 let filter_snails = a.filter_snails.unwrap_or(filter_snails_default);
1774 let filter_selfenergies = a.filter_selfenergies.unwrap_or(filter_selfenergies_default);
1775
1776 let number_of_factorized_loop_subtopologies = a
1778 .number_of_factorized_loop_subtopologies
1779 .clone()
1780 .or(if is_vacuum {
1781 Some(vec![1_i32, 1_i32])
1782 } else {
1783 None
1784 })
1785 .and_then(|v| {
1786 if v[0] < 0 && v[1] < 0 {
1787 None
1788 } else {
1789 Some((v[0].max(0) as usize, v[1].max(0) as usize))
1790 }
1791 });
1792
1793 let max_n_bridges = a
1794 .max_n_bridges
1795 .or(if is_vacuum { Some(0) } else { None })
1796 .and_then(|n| if n < 0 { None } else { Some(n as usize) });
1797
1798 let number_of_fermion_loops = a
1799 .number_of_fermion_loops
1800 .as_ref()
1801 .map(|v| (v[0].max(0) as usize, v[1].max(0) as usize));
1802
1803 let blob_range: RangeInclusive<usize> = {
1805 let (lo, hi) = a
1806 .n_cut_blobs
1807 .as_ref()
1808 .map(|v| (v[0], v[1]))
1809 .unwrap_or((1, 1));
1810 lo..=hi
1811 };
1812 let spectator_range: RangeInclusive<usize> = {
1813 let (lo, hi) = a
1814 .n_cut_spectators
1815 .as_ref()
1816 .map(|v| (v[0], v[1]))
1817 .unwrap_or((0, 0));
1818 lo..=hi
1819 };
1820
1821 let sym_left_right = a.symmetrize_left_right_states.unwrap_or(false);
1823 let (sym_init, sym_final) = match generation_type {
1824 GenerationType::Amplitude => {
1825 let s_init = a.symmetrize_initial_states.unwrap_or(sym_left_right);
1827 let s_final = a.symmetrize_final_states.unwrap_or(sym_left_right);
1828 (s_init, s_final)
1829 }
1830 GenerationType::CrossSection => {
1831 let s_init = a.symmetrize_initial_states.unwrap_or(false);
1832 let s_final = a.symmetrize_final_states.unwrap_or(true);
1833 (s_init, s_final)
1834 }
1835 };
1836 let allow_symferm = a
1837 .allow_symmetrization_of_external_fermions_in_amplitudes
1838 .unwrap_or(false);
1839
1840 let mut fg = ProcessDefinition {
1842 generation_type,
1843 initial_pdgs: initial_pdgs.to_vec(),
1844 final_pdgs_lists: if final_sets_pdgs.is_empty() {
1845 vec![primary_final_pdgs.to_vec()]
1846 } else if !primary_final_pdgs.is_empty() {
1847 let mut sets = vec![primary_final_pdgs.to_vec()];
1848 sets.extend_from_slice(final_sets_pdgs);
1849 sets
1850 } else {
1851 final_sets_pdgs.to_vec()
1852 },
1853 loop_count_range: (1, 1), symmetrize_initial_states: sym_init,
1856 symmetrize_final_states: sym_final,
1857 symmetrize_left_right_states: sym_left_right,
1858 allow_symmetrization_of_external_fermions_in_amplitudes: allow_symferm,
1859 max_multiplicity_for_fast_cut_filter: a.max_multiplicity_for_fast_cut_filter,
1860 amplitude_filters: FeynGenFilters(vec![]),
1861 cross_section_filters: FeynGenFilters(vec![]),
1862 ..Default::default()
1863 };
1864
1865 let mut amp_filters: Vec<FeynGenFilter> = Vec::new();
1867 let mut xs_filters: Vec<FeynGenFilter> = Vec::new();
1868
1869 if let Some(n) = pert.loops_sum_amp_or_sum {
1871 amp_filters.push(FeynGenFilter::LoopCountRange((n as usize, n as usize)));
1872 if fg.generation_type == GenerationType::Amplitude {
1873 fg.loop_count_range = (n as usize, n as usize);
1874 }
1875 }
1876 if let Some(n) = pert.loops_forward_graph {
1877 xs_filters.push(FeynGenFilter::LoopCountRange((n as usize, n as usize)));
1878 if fg.generation_type == GenerationType::CrossSection {
1879 fg.loop_count_range = (n as usize, n as usize);
1880 }
1881 }
1882
1883 if !pert.orders.is_empty() {
1885 let map: HashMap<String, usize> = pert
1886 .orders
1887 .iter()
1888 .map(|(k, v)| (k.clone(), *v as usize))
1889 .collect();
1890 if fg.generation_type == GenerationType::Amplitude {
1891 amp_filters.push(FeynGenFilter::PerturbativeOrders(map));
1892 } else {
1893 xs_filters.push(FeynGenFilter::PerturbativeOrders(map));
1894 }
1895 }
1896
1897 if !amp_couplings.is_empty() {
1899 let cmap = coupling_orders_from_order_ranges_amp(amp_couplings);
1900 amp_filters.push(FeynGenFilter::CouplingOrders(cmap));
1901 }
1902 if !xs_couplings.is_empty() {
1903 let cmap = coupling_orders_from_order_ranges_xs(xs_couplings);
1904 xs_filters.push(FeynGenFilter::CouplingOrders(cmap));
1905 }
1906
1907 if let Some((n_min, n_max)) = number_of_factorized_loop_subtopologies {
1909 let filt = FeynGenFilter::FactorizedLoopTopologiesCountRange((n_min, n_max));
1910 if fg.generation_type == GenerationType::Amplitude {
1911 amp_filters.push(filt);
1912 } else {
1913 xs_filters.push(filt);
1914 }
1915 }
1916
1917 if let Some((n_min, n_max)) = number_of_fermion_loops {
1919 let filt = FeynGenFilter::FermionLoopCountRange((n_min, n_max));
1920 if fg.generation_type == GenerationType::Amplitude {
1921 amp_filters.push(filt);
1922 } else {
1923 xs_filters.push(filt);
1924 }
1925 }
1926
1927 if let Some(n) = max_n_bridges {
1929 let filt = FeynGenFilter::MaxNumberOfBridges(n);
1930 if fg.generation_type == GenerationType::Amplitude {
1931 amp_filters.push(filt);
1932 } else {
1933 xs_filters.push(filt);
1934 }
1935 }
1936
1937 if !veto_pdgs.is_empty() {
1939 let filt = FeynGenFilter::ParticleVeto(veto_pdgs.to_vec());
1940 if fg.generation_type == GenerationType::Amplitude {
1941 amp_filters.push(filt);
1942 } else {
1943 xs_filters.push(filt);
1944 }
1945 }
1946
1947 if fg.generation_type == GenerationType::CrossSection {
1949 xs_filters.push(FeynGenFilter::BlobRange(blob_range));
1950 xs_filters.push(FeynGenFilter::SpectatorRange(spectator_range));
1951 }
1952
1953 if filter_selfenergies {
1955 let mut se = SelfEnergyFilterOptions::default();
1956 if let Some(opt) = a.veto_self_energy_of_massive_lines {
1957 se.veto_self_energy_of_massive_lines = opt;
1958 }
1959 if let Some(opt) = a.veto_self_energy_of_massless_lines {
1960 se.veto_self_energy_of_massless_lines = opt;
1961 }
1962 if let Some(opt) = a.veto_only_scaleless_self_energy {
1963 se.veto_only_scaleless_self_energy = opt;
1964 }
1965 let filt = FeynGenFilter::SelfEnergyFilter(se);
1966 if fg.generation_type == GenerationType::Amplitude {
1967 amp_filters.push(filt);
1968 } else {
1969 xs_filters.push(filt);
1970 }
1971 }
1972 if filter_snails {
1973 let mut sn = SnailFilterOptions::default();
1974 if let Some(opt) = a.veto_snails_attached_to_massive_lines {
1975 sn.veto_snails_attached_to_massive_lines = opt;
1976 }
1977 if let Some(opt) = a.veto_snails_attached_to_massless_lines {
1978 sn.veto_snails_attached_to_massless_lines = opt;
1979 }
1980 if let Some(opt) = a.veto_only_scaleless_snails {
1981 sn.veto_only_scaleless_snails = opt;
1982 }
1983 let filt = FeynGenFilter::ZeroSnailsFilter(sn);
1984 if fg.generation_type == GenerationType::Amplitude {
1985 amp_filters.push(filt);
1986 } else {
1987 xs_filters.push(filt);
1988 }
1989 }
1990 if filter_tadpoles {
1991 let mut td = TadpolesFilterOptions::default();
1992 if let Some(opt) = a.veto_tadpoles_attached_to_massive_lines {
1993 td.veto_tadpoles_attached_to_massive_lines = opt;
1994 }
1995 if let Some(opt) = a.veto_tadpoles_attached_to_massless_lines {
1996 td.veto_tadpoles_attached_to_massless_lines = opt;
1997 }
1998 if let Some(opt) = a.veto_only_scaleless_tadpoles {
1999 td.veto_only_scaleless_tadpoles = opt;
2000 }
2001 let filt = FeynGenFilter::TadpolesFilter(td);
2002 if fg.generation_type == GenerationType::Amplitude {
2003 amp_filters.push(filt);
2004 } else {
2005 xs_filters.push(filt);
2006 }
2007 }
2008
2009 if fg.generation_type == GenerationType::CrossSection
2011 && a.filter_cross_section_tadpoles.unwrap_or(false)
2012 {
2013 if let Some(opt) = a.filter_cross_section_tadpoles {
2014 xs_filters.push(FeynGenFilter::SewedFilter(SewedFilterOptions {
2015 filter_tadpoles: opt,
2016 }));
2017 }
2018 }
2019
2020 if let Some(vertex_interactions_allowed) = a.allowed_vertex_interactions.as_ref() {
2021 if !vertex_interactions_allowed.is_empty() {
2022 let filt = FeynGenFilter::VertexAllow(vertex_interactions_allowed.clone());
2023 if fg.generation_type == GenerationType::Amplitude {
2024 amp_filters.push(filt);
2025 } else {
2026 xs_filters.push(filt);
2027 }
2028 }
2029 }
2030 if let Some(vertex_interactions_vetoed) = a.veto_vertex_interactions.as_ref() {
2031 let filt = FeynGenFilter::VertexVeto(vertex_interactions_vetoed.clone());
2032 if fg.generation_type == GenerationType::Amplitude {
2033 amp_filters.push(filt);
2034 } else {
2035 xs_filters.push(filt);
2036 }
2037 }
2038
2039 fg.amplitude_filters = FeynGenFilters(amp_filters);
2040 fg.cross_section_filters = FeynGenFilters(xs_filters);
2041
2042 fg
2043}
2044
2045fn coupling_orders_from_order_ranges_amp(
2046 orders: &BTreeMap<String, OrderRange>,
2047) -> HashMap<String, (usize, Option<usize>)> {
2048 let mut out = HashMap::default();
2049 for (name, r) in orders {
2050 if let Some(eq) = r.eq {
2051 out.insert(name.clone(), (eq as usize, Some(eq as usize)));
2052 } else {
2053 let minv = r.min.unwrap_or(0) as usize;
2054 let maxv = r.max.map(|m| m as usize);
2055 out.insert(name.clone(), (minv, maxv));
2056 }
2057 }
2058 out
2059}
2060
2061fn coupling_orders_from_order_ranges_xs(
2062 orders: &BTreeMap<CouplingKey, OrderRange>,
2063) -> HashMap<String, (usize, Option<usize>)> {
2064 let mut merged: BTreeMap<String, OrderRange> = BTreeMap::new();
2066 for (k, r) in orders {
2067 let e = merged.entry(k.name.clone()).or_default();
2068 if let Some(eq) = r.eq {
2069 e.eq = Some(eq);
2070 }
2071 if let Some(min) = r.min {
2072 e.min = Some(e.min.map_or(min, |m| m.max(min)));
2073 }
2074 if let Some(max) = r.max {
2075 e.max = Some(e.max.map_or(max, |m| m.min(max)));
2076 }
2077 }
2078 coupling_orders_from_order_ranges_amp(&merged)
2079}
2080
2081fn build_grouping_option(a: &SpecArgs) -> NumeratorAwareGraphGroupingOption {
2082 a.numerator_aware_isomorphism_grouping
2083 .unwrap_or(GroupingChoice::GroupIdenticalGraphsUpToScalarRescaling)
2084 .to_strategy(
2085 a.numerical_samples_seed,
2086 a.number_of_samples_for_numerator_comparisons,
2087 a.consider_internal_masses_only_in_numerator_isomorphisms,
2088 a.fully_numerical_substitution_when_comparing_numerators,
2089 a.compare_canonized_numerator,
2090 a.symmetric_left_right_polarizations,
2091 )
2092}
2093
2094fn all_particle_names(model: &Model) -> Vec<String> {
2097 model
2098 .particles
2099 .iter()
2100 .map(|p| p.name.clone().to_string())
2101 .collect::<Vec<_>>()
2102}
2103
2104fn resolve_pdgs(model: &Model, names: &[String]) -> Result<Vec<i64>, ParseError> {
2105 let all_particles = all_particle_names(model);
2106 let mut out = Vec::with_capacity(names.len());
2107 'outer: for n in names {
2108 if let Ok(p) = n.parse::<i64>() {
2110 out.push(p);
2111 continue;
2112 }
2113 if let Ok(p) = model.try_get_particle(n) {
2115 out.push(p.pdg_code as i64);
2116 continue;
2117 }
2118 for part in model.particles.iter() {
2120 if part.name.eq_ignore_ascii_case(n) || part.antiname.eq_ignore_ascii_case(n) {
2121 out.push(part.pdg_code as i64);
2122 continue 'outer;
2123 }
2124 }
2125 return Err(ParseError::UnknownParticle {
2127 name: n.clone(),
2128 choices: all_particles.join(", "),
2129 });
2130 }
2131 Ok(out)
2132}
2133
2134fn validate_coupling_names(
2135 model: &Model,
2136 amp: &BTreeMap<String, OrderRange>,
2137 xs: &BTreeMap<CouplingKey, OrderRange>,
2138 pert: &Perturbative,
2139) -> Result<(), ParseError> {
2140 let has_orders_info = !model.orders.is_empty();
2142
2143 if !has_orders_info {
2144 return Ok(());
2145 }
2146
2147 let mut names = BTreeSet::<String>::new();
2149 for k in amp.keys() {
2150 names.insert(k.clone());
2151 }
2152 for k in xs.keys() {
2153 names.insert(k.name.clone());
2154 }
2155 for k in pert.orders.keys() {
2156 names.insert(k.clone());
2157 }
2158
2159 if names.is_empty() {
2160 return Ok(());
2161 }
2162
2163 let known: Vec<String> = model
2165 .orders
2166 .iter()
2167 .map(|o| o.name.to_string())
2168 .collect::<Vec<_>>();
2169 let known_lower: BTreeSet<String> = known.iter().map(|s| s.to_ascii_lowercase()).collect();
2170
2171 for n in names {
2172 if !known_lower.contains(&n.to_ascii_lowercase()) {
2173 return Err(ParseError::UnknownCoupling {
2174 name: n,
2175 choices: known.join(", "),
2176 });
2177 }
2178 }
2179 Ok(())
2180}
2181
2182fn parse_csv_list(s: &str) -> Vec<String> {
2185 s.split(',')
2186 .flat_map(|x| x.split_whitespace())
2187 .filter(|t| !t.is_empty())
2188 .map(|t| t.to_string())
2189 .collect()
2190}
2191
2192#[allow(dead_code)]
2193fn parse_loop_momentum_bases(s: &str) -> Option<HashMap<String, Vec<String>>> {
2194 let mut out: HashMap<String, Vec<String>> = HashMap::default();
2196 for kv in s.split(';') {
2197 let kv = kv.trim();
2198 if kv.is_empty() {
2199 continue;
2200 }
2201 if let Some((k, v)) = kv.split_once('=') {
2202 let key = k.trim().to_string();
2203 let vals = v
2204 .split(',')
2205 .map(|x| x.trim().to_string())
2206 .filter(|x| !x.is_empty())
2207 .collect::<Vec<_>>();
2208 if !key.is_empty() {
2209 out.insert(key, vals);
2210 }
2211 } else {
2212 }
2214 }
2215 if out.is_empty() {
2216 None
2217 } else {
2218 Some(out)
2219 }
2220}
2221
2222#[cfg(test)]
2225mod tests {
2226 use super::*;
2227 use crate::{commands::Commands, Repl};
2228 use clap::Parser;
2229 use gammalooprs::utils::load_generic_model;
2230 use gammalooprs::{feyngen::GenerationType, initialisation::test_initialise};
2231 use std::time::Duration;
2232
2233 #[test]
2234 fn thermal_xs_generation_is_rejected_before_changing_processes() -> Result<()> {
2235 use gammalooprs::{processes::ProcessCollection, settings::global::MediumMode};
2236
2237 let mut state = State::new_test();
2238 let mut cross_sections = BTreeMap::new();
2239 cross_sections.insert(
2240 "default".to_string(),
2241 CrossSection::from_graph_list("default".to_string(), vec![], &state.model)?,
2242 );
2243 state.process_list.add_process(Process {
2244 definition: ProcessDefinition {
2245 generation_type: GenerationType::CrossSection,
2246 ..ProcessDefinition::default()
2247 },
2248 settings_history: None,
2249 collection: ProcessCollection::CrossSections(cross_sections),
2250 });
2251 let mut settings = GlobalSettings::default();
2252 let runtime = RuntimeSettings::default();
2253 let temp = tempfile::tempdir()?;
2254
2255 for mode in [
2256 MediumMode::ThermodynamicEquilibrium,
2257 MediumMode::ZeroTemperatureEquilibrium,
2258 ] {
2259 settings.generation.medium.mode = mode;
2260 for command in [
2261 "generate xs a > d d~ --clear-existing-processes",
2262 "generate xs a > d d~ --only-diagrams --clear-existing-processes",
2263 "generate existing -p 0",
2264 "generate existing -p 0 --integrand-name default",
2265 "generate",
2266 ] {
2267 let repl = Repl::try_parse_from(
2268 std::iter::once("gammaloop").chain(command.split_whitespace()),
2269 )?;
2270 let Commands::Generate(generate) = repl.command else {
2271 unreachable!();
2272 };
2273 let error = generate
2274 .run(&mut state, temp.path(), false, &settings, &runtime)
2275 .unwrap_err();
2276 assert!(error.to_string().contains("`xs`"), "{command}: {error}");
2277 assert!(error.to_string().contains("not supported"));
2278 assert_eq!(state.process_list.processes.len(), 1);
2279 assert!(state.process_list.processes[0].settings_history.is_none());
2280 }
2281 }
2282 let vacuum_settings = GlobalSettings::default();
2285 state.process_list.processes.insert(
2286 0,
2287 Process {
2288 definition: ProcessDefinition {
2289 generation_type: GenerationType::Amplitude,
2290 folder_name: "amplitude".to_string(),
2291 ..ProcessDefinition::default()
2292 },
2293 settings_history: Some(vacuum_settings.clone()),
2294 collection: ProcessCollection::Amplitudes(BTreeMap::from([(
2295 "default".to_string(),
2296 Amplitude::from_graph_list("default", vec![])?,
2297 )])),
2298 },
2299 );
2300 state.process_list.processes[1].definition.process_id = 1;
2301 settings.generation.threshold_subtraction.enable_thresholds = false;
2302 for mode in [
2303 MediumMode::ThermodynamicEquilibrium,
2304 MediumMode::ZeroTemperatureEquilibrium,
2305 ] {
2306 settings.generation.medium.mode = mode;
2307 let error = Generate {
2308 keep_sources: false,
2309 mode: None,
2310 }
2311 .run(&mut state, temp.path(), false, &settings, &runtime)
2312 .unwrap_err();
2313 assert!(error.to_string().contains("`xs`"), "{mode:?}: {error}");
2314 assert!(error.to_string().contains("not supported"));
2315 assert_eq!(state.process_list.processes.len(), 2);
2316 assert!(
2317 state.process_list.processes[0].settings_history.as_ref() == Some(&vacuum_settings),
2318 "failed batch generation changed the preceding amplitude's settings in {mode:?}"
2319 );
2320 assert!(state.process_list.processes[1].settings_history.is_none());
2321 }
2322 assert_eq!(std::fs::read_dir(temp.path())?.count(), 0);
2323 Ok(())
2324 }
2325
2326 #[test]
2327 fn remove_compiled_cpp_sources_only_removes_sources_with_matching_libraries() -> Result<()> {
2328 let temp = tempfile::tempdir()?;
2329 let compiled_source = temp.path().join("integrand.cpp");
2330 let compiled_library = temp.path().join("integrand.so");
2331 let uncompiled_source = temp.path().join("orphan.cpp");
2332 std::fs::write(&compiled_source, "compiled source")?;
2333 std::fs::write(&compiled_library, "compiled library")?;
2334 std::fs::write(&uncompiled_source, "orphan source")?;
2335
2336 let removed_count = remove_compiled_cpp_sources(temp.path())?;
2337
2338 assert_eq!(removed_count, 1);
2339 assert!(!compiled_source.exists());
2340 assert!(compiled_library.is_file());
2341 assert!(uncompiled_source.is_file());
2342 Ok(())
2343 }
2344
2345 #[test]
2346 fn parse_generate_keep_sources_after_subcommand() {
2347 let repl =
2348 Repl::try_parse_from(["gammaloop", "generate", "existing", "--keep-sources"]).unwrap();
2349
2350 match repl.command {
2351 Commands::Generate(generate) => {
2352 assert!(generate.keep_sources);
2353 assert!(matches!(generate.mode, Some(GenerateCmd::Existing(_))));
2354 }
2355 other => panic!("Expected generate command, got {other:?}"),
2356 }
2357 }
2358
2359 #[test]
2360 fn generation_summary_includes_separator_and_total_row() {
2361 let reports = vec![
2362 GeneratedGraphReport {
2363 process_id: 0,
2364 integrand_name: "itg".to_string(),
2365 graph_name: "GL01".to_string(),
2366 stats: GraphGenerationStats {
2367 evaluator_count: 2,
2368 total_time: Duration::from_secs(4),
2369 evaluator_spenso_time: Duration::from_secs(1),
2370 evaluator_symbolica_time: Duration::from_secs(1),
2371 evaluator_compile_time: Duration::from_secs(1),
2372 ..GraphGenerationStats::default()
2373 },
2374 },
2375 GeneratedGraphReport {
2376 process_id: 0,
2377 integrand_name: "itg".to_string(),
2378 graph_name: "GL02".to_string(),
2379 stats: GraphGenerationStats {
2380 evaluator_count: 3,
2381 total_time: Duration::from_secs(6),
2382 evaluator_spenso_time: Duration::from_secs(2),
2383 evaluator_symbolica_time: Duration::ZERO,
2384 evaluator_compile_time: Duration::from_secs(3),
2385 ..GraphGenerationStats::default()
2386 },
2387 },
2388 ];
2389
2390 let summary = render_generation_summary(&reports, 0, Some(4), None).unwrap();
2391 let plain = Regex::new(r"\x1b\[[0-9;]*m")
2392 .unwrap()
2393 .replace_all(&summary, "")
2394 .into_owned();
2395
2396 assert!(plain.contains("Total"));
2397 assert!(plain.contains("5"));
2398 assert!(plain.contains("20%"));
2399 assert!(plain.contains("30%"));
2400 assert!(plain.contains("10%"));
2401 assert!(plain.contains("40%"));
2402
2403 let lines = plain.lines().collect::<Vec<_>>();
2404 let total_line_index = lines
2405 .iter()
2406 .position(|line| line.contains("Total"))
2407 .expect("missing Total row");
2408 let separator_line = lines[..total_line_index]
2409 .iter()
2410 .rev()
2411 .find(|line| !line.trim().is_empty())
2412 .expect("missing line before Total row");
2413 assert!(
2414 separator_line.chars().any(|ch| "┼╪╫┿├┤".contains(ch)),
2415 "missing horizontal separator before Total row: {separator_line}"
2416 );
2417 }
2418
2419 fn base_args(tokens: &str) -> SpecArgs {
2420 SpecArgs {
2421 tokens: tokens.split_whitespace().map(|x| x.to_string()).collect(),
2422 clear_existing_processes: false,
2423 filter_selfenergies: None,
2424 filter_snails: None,
2425 filter_tadpoles: None,
2426 filter_cross_section_tadpoles: None,
2427 veto_tadpoles_attached_to_massive_lines: None,
2428 veto_tadpoles_attached_to_massless_lines: None,
2429 veto_only_scaleless_tadpoles: None,
2430 veto_snails_attached_to_massive_lines: None,
2431 veto_snails_attached_to_massless_lines: None,
2432 veto_only_scaleless_snails: None,
2433 veto_self_energy_of_massive_lines: None,
2434 veto_self_energy_of_massless_lines: None,
2435 veto_only_scaleless_self_energy: None,
2436 max_n_bridges: None,
2437 number_of_factorized_loop_subtopologies: None,
2438 number_of_fermion_loops: None,
2439 symmetric_left_right_polarizations: None,
2440 n_cut_blobs: None,
2441 n_cut_spectators: None,
2442 allow_symmetrization_of_external_fermions_in_amplitudes: None,
2443 symmetrize_initial_states: None,
2444 symmetrize_final_states: None,
2445 symmetrize_left_right_states: None,
2446 numerator_aware_isomorphism_grouping: None,
2447 numerical_samples_seed: None,
2448 number_of_samples_for_numerator_comparisons: None,
2449 consider_internal_masses_only_in_numerator_isomorphisms: None,
2450 fully_numerical_substitution_when_comparing_numerators: None,
2451 compare_canonized_numerator: None,
2452 loop_momentum_bases: None,
2453 select_graphs: None,
2454 veto_graphs: None,
2455 graph_prefix: None,
2456 max_multiplicity_for_fast_cut_filter: 6,
2457 filter_self_loop: None,
2458 filter_zero_flow_edges: None,
2459 process_name: None,
2460 append: false,
2461 integrand_name: None,
2462 only_diagrams: true,
2463 allowed_vertex_interactions: None,
2464 veto_vertex_interactions: None,
2465 global_prefactor_num: None,
2466 global_prefactor_projector: None,
2467 }
2468 }
2469
2470 fn parse_ok_amp(s: &str) -> ProcessSpec {
2472 let model = &load_generic_model("sm");
2473 let a = base_args(s);
2474 parse_spec_with_model(&a, GenerationType::Amplitude, model).unwrap()
2475 }
2476
2477 fn parse_ok_xs(s: &str) -> ProcessSpec {
2478 let model = &load_generic_model("sm");
2479 let a = base_args(s);
2480 parse_spec_with_model(&a, GenerationType::CrossSection, model).unwrap()
2481 }
2482
2483 #[test]
2484 fn cp_symmetrization_option_defaults_false_and_roundtrips() {
2485 let model = load_generic_model("sm");
2486 for option in [None, Some(false), Some(true)] {
2487 let mut command = vec!["gammaloop", "generate", "xs", "a", ">", "d", "d~"];
2488 if let Some(enabled) = option {
2489 command.extend([
2490 "--symmetrize-left-right-states",
2491 if enabled { "true" } else { "false" },
2492 ]);
2493 }
2494 let repl = Repl::try_parse_from(command).unwrap();
2495 let Commands::Generate(Generate {
2496 mode: Some(GenerateCmd::Xs(args)),
2497 ..
2498 }) = repl.command
2499 else {
2500 panic!("expected a cross-section generation command");
2501 };
2502 assert_eq!(args.symmetrize_left_right_states, option);
2503 let serialized = serde_json::to_value(&args).unwrap();
2504 assert_eq!(
2505 serialized["symmetrize_left_right_states"],
2506 serde_json::json!(option)
2507 );
2508 let decoded: SpecArgs = serde_json::from_value(serialized).unwrap();
2509 assert_eq!(decoded, args);
2510 let spec =
2511 parse_spec_with_model(&decoded, GenerationType::CrossSection, &model).unwrap();
2512 assert_eq!(
2513 spec.process_definition.symmetrize_left_right_states,
2514 option.unwrap_or(false)
2515 );
2516 }
2517 }
2518
2519 #[test]
2520 fn amplitude_left_right_shorthand_respects_explicit_side_options() {
2521 let model = load_generic_model("sm");
2522 let mut args = base_args("e+ e- > d d~");
2523 for (left_right, initial, final_state, expected) in [
2524 (None, None, None, (false, false)),
2525 (Some(true), None, None, (true, true)),
2526 (Some(true), Some(false), None, (false, true)),
2527 (Some(true), None, Some(false), (true, false)),
2528 ] {
2529 args.symmetrize_left_right_states = left_right;
2530 args.symmetrize_initial_states = initial;
2531 args.symmetrize_final_states = final_state;
2532 let definition = parse_spec_with_model(&args, GenerationType::Amplitude, &model)
2533 .unwrap()
2534 .process_definition;
2535 assert_eq!(
2536 (
2537 definition.symmetrize_initial_states,
2538 definition.symmetrize_final_states
2539 ),
2540 expected
2541 );
2542 assert_eq!(
2543 definition.symmetrize_left_right_states,
2544 left_right.unwrap_or(false)
2545 );
2546 }
2547 }
2548
2549 #[test]
2550 fn fermion_loop_filter_is_added_in_both_generation_modes() {
2551 let model = &load_generic_model("sm");
2552 let repl = Repl::try_parse_from([
2553 "gammaloop",
2554 "generate",
2555 "amp",
2556 "e+",
2557 "e-",
2558 ">",
2559 "d",
2560 "d~",
2561 "--number-of-fermion-loops",
2562 "1",
2563 "2",
2564 ])
2565 .unwrap();
2566 let Commands::Generate(Generate {
2567 mode: Some(GenerateCmd::Amp(args)),
2568 ..
2569 }) = repl.command
2570 else {
2571 panic!("expected an amplitude generation command");
2572 };
2573 for mode in [GenerationType::Amplitude, GenerationType::CrossSection] {
2574 let spec = parse_spec_with_model(&args, mode, model).unwrap();
2575 let filters = if mode == GenerationType::Amplitude {
2576 &spec.process_definition.amplitude_filters
2577 } else {
2578 &spec.process_definition.cross_section_filters
2579 };
2580 assert!(filters
2581 .0
2582 .iter()
2583 .any(|filter| matches!(filter, FeynGenFilter::FermionLoopCountRange((1, 2)))));
2584 }
2585 }
2586
2587 #[test]
2588 fn allowed_vertex_interactions_are_added_to_generation_filters() {
2589 let model = &load_generic_model("sm");
2590 let mut args = base_args("e+ e- > d d~");
2591 args.allowed_vertex_interactions = Some(vec!["V_6".to_string(), "V_9".to_string()]);
2592
2593 let spec = parse_spec_with_model(&args, GenerationType::Amplitude, model).unwrap();
2594
2595 assert!(spec
2596 .process_definition
2597 .amplitude_filters
2598 .0
2599 .iter()
2600 .any(|filter| matches!(
2601 filter,
2602 FeynGenFilter::VertexAllow(vertex_names)
2603 if vertex_names == &vec!["V_6".to_string(), "V_9".to_string()]
2604 )));
2605 }
2606
2607 #[test]
2608 fn basic_process_list_arrow() {
2609 test_initialise().unwrap();
2610 let ps = parse_ok_amp("e+ e- > d d~ g");
2611 assert_eq!(ps.initial, vec!["e+", "e-"]);
2612 assert_eq!(ps.final_, vec!["d", "d~", "g"]);
2613 assert!(!ps.empty_initial && !ps.empty_final);
2614 }
2615
2616 #[test]
2617 fn basic_process_list_to() {
2618 test_initialise().unwrap();
2619 let ps = parse_ok_amp("e+ e- to d d~ g");
2620 assert_eq!(ps.initial, vec!["e+", "e-"]);
2621 assert_eq!(ps.final_, vec!["d", "d~", "g"]);
2622 }
2623
2624 #[test]
2625 fn empty_sets_allowed() {
2626 test_initialise().unwrap();
2627 let ps = parse_ok_xs("{} to {}");
2628 assert!(ps.empty_initial);
2629 assert!(ps.empty_final);
2630 }
2631
2632 #[test]
2633 fn alternatives_in_final_states() {
2634 test_initialise().unwrap();
2635 let ps = parse_ok_xs("e+ e- > { Z Z, a a }");
2636 assert!(ps.final_.is_empty());
2637 assert_eq!(ps.final_sets.len(), 2);
2638 assert_eq!(ps.final_sets[0], vec!["Z", "Z"]);
2639 assert_eq!(ps.final_sets[1], vec!["a", "a"]);
2640 }
2641
2642 #[test]
2643 fn veto_and_only() {
2644 test_initialise().unwrap();
2645 let ps = parse_ok_xs("e+ e- > mu+ mu- / u d c g ghG e- | u g ghG");
2646 assert!(ps.veto.contains("u"));
2647 assert!(ps.veto.contains("e-"));
2648 let sel = ps.only.as_ref().unwrap();
2649 assert!(sel.contains("u"));
2650 assert!(sel.contains("g"));
2651 assert!(sel.contains("ghG"));
2652 }
2653
2654 #[test]
2655 fn amp_order_constraints() {
2656 test_initialise().unwrap();
2657 let ps = parse_ok_amp("e+ e- > mu+ mu- QED==2 QCD>=2 QCD<=4");
2658 let qcd = ps.amp_couplings.get("QCD").unwrap();
2659 assert_eq!(qcd.min, Some(2));
2660 assert_eq!(qcd.max, Some(4));
2661 let qed = ps.amp_couplings.get("QED").unwrap();
2662 assert_eq!(qed.eq, Some(2));
2663 }
2664
2665 #[test]
2666 fn xs_order_constraints_powered() {
2667 test_initialise().unwrap();
2668 let ps = parse_ok_xs("e+ e- > mu+ mu- QED^2==2 QCD^2>=2 QCD^2<=4");
2669 let key = CouplingKey {
2670 name: "QCD".into(),
2671 power: 2,
2672 };
2673 let qcd2 = ps.xs_couplings.get(&key).unwrap();
2674 assert_eq!(qcd2.min, Some(2));
2675 assert_eq!(qcd2.max, Some(4));
2676 let keyq = CouplingKey {
2677 name: "QED".into(),
2678 power: 2,
2679 };
2680 let qed2 = ps.xs_couplings.get(&keyq).unwrap();
2681 assert_eq!(qed2.eq, Some(2));
2682 }
2683
2684 #[test]
2685 fn perturbative_block_all_forms() {
2686 test_initialise().unwrap();
2687 let ps = parse_ok_xs("e+ e- > z [ {1} {{2}} QCD=2 QED=1 ]");
2688 assert_eq!(ps.pert.loops_sum_amp_or_sum, Some(1));
2689 assert_eq!(ps.pert.loops_forward_graph, Some(2));
2690 assert_eq!(ps.pert.orders.get("QCD"), Some(&2));
2691 assert_eq!(ps.pert.orders.get("QED"), Some(&1));
2692 }
2693
2694 #[test]
2695 fn perturbative_block_shorthand() {
2696 test_initialise().unwrap();
2697 let ps = parse_ok_xs("e+ e- > z [ QCD ]");
2698 assert_eq!(ps.pert.orders.get("QCD"), Some(&1));
2699 }
2700
2701 #[test]
2702 fn amplitude_generation_type_is_set() {
2703 test_initialise().unwrap();
2704 let ps = parse_ok_amp("e+ e- > mu+ mu-");
2705 assert_eq!(
2706 ps.process_definition.generation_type,
2707 GenerationType::Amplitude
2708 );
2709 }
2710
2711 #[test]
2712 fn rejects_malformed_tokens() {
2713 test_initialise().unwrap();
2714 let model = &load_generic_model("sm");
2715
2716 let args = base_args("e+ > z [ {{}} ]");
2717 let err = parse_spec_with_model(&args, GenerationType::CrossSection, model).unwrap_err();
2718 assert_eq!(err, ParseError::InvalidToken("{{}}".into()));
2719 }
2720
2721 #[test]
2722 fn missing_arrow_detected() {
2723 test_initialise().unwrap();
2724 let model = &load_generic_model("sm");
2725
2726 let args = base_args("e+ e- z z");
2727 let err = parse_spec_with_model(&args, GenerationType::CrossSection, model).unwrap_err();
2728 assert!(matches!(err, ParseError::MissingArrow));
2729 }
2730
2731 #[test]
2732 fn vacuum_defaults_and_filters_xs() {
2733 test_initialise().unwrap();
2734 let ps = parse_ok_xs("{} to {}");
2735 assert_eq!(
2736 ps.process_definition.generation_type,
2737 GenerationType::CrossSection
2738 );
2739
2740 let xs_filters = &ps.process_definition.cross_section_filters.0;
2741
2742 assert!(xs_filters
2744 .iter()
2745 .any(|f| matches!(f, FeynGenFilter::MaxNumberOfBridges(0))));
2746 assert!(xs_filters
2747 .iter()
2748 .any(|f| matches!(f, FeynGenFilter::FactorizedLoopTopologiesCountRange((1, 1)))));
2749
2750 assert!(xs_filters
2752 .iter()
2753 .any(|f| matches!(f, FeynGenFilter::BlobRange(r) if r.clone()==(1..=1))));
2754 assert!(xs_filters
2755 .iter()
2756 .any(|f| matches!(f, FeynGenFilter::SpectatorRange(r) if r.clone()==(0..=0))));
2757 }
2758
2759 #[test]
2760 fn particle_veto_resolution_cross_section() {
2761 use std::collections::BTreeSet;
2762 test_initialise().unwrap();
2763 let ps = parse_ok_xs("e+ e- > mu+ mu- / u d u~ e+");
2765 let xs_filters = &ps.process_definition.cross_section_filters.0;
2766
2767 let veto_pdgs = xs_filters
2768 .iter()
2769 .find_map(|f| match f {
2770 FeynGenFilter::ParticleVeto(v) => Some(v.clone()),
2771 _ => None,
2772 })
2773 .expect("ParticleVeto filter not found");
2774 let set: BTreeSet<i64> = veto_pdgs.into_iter().collect();
2775
2776 for pdg in [2_i64, 1_i64, -2_i64, -11_i64] {
2778 assert!(set.contains(&pdg), "missing PDG {pdg} in veto");
2779 }
2780 }
2781
2782 #[test]
2783 fn particle_inclusion_resolution_cross_section() {
2784 use std::collections::BTreeSet;
2785 test_initialise().unwrap();
2786 let ps = parse_ok_xs("e+ e- > mu+ mu- | u d u~ e+");
2788 let xs_filters = &ps.process_definition.cross_section_filters.0;
2789
2790 let veto_pdgs = xs_filters
2791 .iter()
2792 .find_map(|f| match f {
2793 FeynGenFilter::ParticleVeto(v) => Some(v.clone()),
2794 _ => None,
2795 })
2796 .expect("ParticleVeto filter not found");
2797 let set: BTreeSet<i64> = veto_pdgs.into_iter().collect();
2798
2799 for pdg in [
2800 3, 4, 5, 6, 12, 13, 14, 15, 16, 21, 22, 23, 24, 25, 250, 251, 9000001, 9000002,
2801 9000003, 9000004, 9000005,
2802 ] {
2803 assert!(set.contains(&pdg), "missing PDG {pdg} in veto");
2804 }
2805 }
2806
2807 #[test]
2808 fn final_sets_and_orders_and_loops_cross_section() {
2809 test_initialise().unwrap();
2810 let ps = parse_ok_xs("e+ e- > { Z Z, a a, H H } [ {{3}} QCD=2 QED=1 ]");
2812
2813 assert_eq!(ps.final_sets.len(), 3);
2815 assert_eq!(
2816 ps.process_definition.generation_type,
2817 GenerationType::CrossSection
2818 );
2819
2820 assert_eq!(ps.process_definition.loop_count_range, (3, 3));
2822
2823 let xs_filters = &ps.process_definition.cross_section_filters.0;
2825 assert!(xs_filters.iter().any(|f| {
2826 if let FeynGenFilter::PerturbativeOrders(m) = f {
2827 m.get("QCD") == Some(&2usize) && m.get("QED") == Some(&1usize)
2828 } else {
2829 false
2830 }
2831 }));
2832 }
2833
2834 #[test]
2835 fn amplitude_with_powered_xs_constraints_and_only() {
2836 test_initialise().unwrap();
2837 let ps = parse_ok_amp("e+ e- > mu+ mu- | g ghG QED^2==2 QCD^2>=2 QCD^2<=4 [ {1} QCD ]");
2839
2840 assert_eq!(ps.pert.loops_sum_amp_or_sum, Some(1));
2842 assert_eq!(ps.pert.orders.get("QCD"), Some(&1));
2843
2844 let key_qed2 = CouplingKey {
2846 name: "QED".into(),
2847 power: 2,
2848 };
2849 let key_qcd2 = CouplingKey {
2850 name: "QCD".into(),
2851 power: 2,
2852 };
2853 let qed2 = ps.xs_couplings.get(&key_qed2).unwrap();
2854 assert_eq!(qed2.eq, Some(2));
2855 let qcd2 = ps.xs_couplings.get(&key_qcd2).unwrap();
2856 assert_eq!(qcd2.min, Some(2));
2857 assert_eq!(qcd2.max, Some(4));
2858
2859 let only = ps.only.as_ref().expect("missing only-set");
2861 assert!(only.contains("g"));
2862 assert!(only.contains("ghG"));
2863 }
2864
2865 #[test]
2866 fn amplitude_case_insensitive_and_anti_in_veto() {
2867 use std::collections::BTreeSet;
2868 test_initialise().unwrap();
2869 let ps = parse_ok_amp("E+ e- to Z Z / u~ d~ A");
2871
2872 let amp_filters = &ps.process_definition.amplitude_filters.0;
2873 let veto_pdgs = amp_filters
2874 .iter()
2875 .find_map(|f| match f {
2876 FeynGenFilter::ParticleVeto(v) => Some(v.clone()),
2877 _ => None,
2878 })
2879 .expect("AMP ParticleVeto filter not found");
2880
2881 let set: BTreeSet<i64> = veto_pdgs.into_iter().collect();
2882 for pdg in [-2_i64, -1_i64, 22_i64] {
2884 assert!(set.contains(&pdg), "missing PDG {pdg} in AMP veto");
2885 }
2886 }
2887
2888 #[test]
2889 fn shell_name_long_with_sets_veto_orders_pert_and_xs() {
2890 test_initialise().unwrap();
2891 let model = &load_generic_model("sm");
2892
2893 let args =
2895 base_args("e+ e- > { Z Z, a a } / u d QED==2 QCD>=1 QED^2<=4 QCD^2>=2 [ QED=1 QCD=2 ]");
2896 let ps = parse_spec_with_model(&args, GenerationType::Amplitude, model).unwrap();
2897
2898 let long = ps.process_shell_name(false);
2899
2900 let expected = "epem_zz_or_aa-\
2901 no__d_u-\
2902 QCD_ge_1__QED_eq_2-\
2903 QCDsq_ge_2__QEDsq_le_4-\
2904 QCDloop_eq_2__QEDloop_eq_1";
2905
2906 assert_eq!(long, expected);
2907 }
2908
2909 #[test]
2910 fn repr_str_canonical_with_amp_xs_veto_loops_orders() {
2911 test_initialise().unwrap();
2912 let model = &load_generic_model("sm");
2913
2914 let args = base_args(
2916 "mu+ mu- > g g / ghg gha QED==1 QCD<=3 [ {2} QED=0 QCD=1 ] QED^2==0 QCD^2>=2",
2917 );
2918 let ps = parse_spec_with_model(&args, GenerationType::Amplitude, model).unwrap();
2919
2920 let r = ps.repr_str();
2921
2922 let expected = "mu+ mu- > g g / gha ghg QCD<=3 QED==1 [ {2} QCD ] QCD^2>=2 QED^2==0";
2923
2924 assert_eq!(r, expected);
2925 }
2926
2927 #[test]
2928 fn repr_str_with_both_loop_counts_and_sorted_orders() {
2929 test_initialise().unwrap();
2930 let model = &load_generic_model("sm");
2931
2932 let args = base_args("e+ e- > z z [ {{2}} QED=2 QCD=1 {1} ]");
2933 let ps = parse_spec_with_model(&args, GenerationType::CrossSection, model).unwrap();
2934
2935 let r = ps.repr_str();
2936
2937 let expected = "e+ e- > z z [ {1} {{2}} QCD QED=2 ]";
2938
2939 assert_eq!(r, expected);
2940 }
2941
2942 #[test]
2943 fn shell_name_short_canonical_and_sanitization() {
2944 test_initialise().unwrap();
2945 let model = &load_generic_model("sm");
2946
2947 let args = base_args("W+ W- > {}");
2948 let ps = parse_spec_with_model(&args, GenerationType::CrossSection, model).unwrap();
2949
2950 let short = ps.process_shell_name(true);
2952 let expected = "wpwm_empty";
2953
2954 assert_eq!(short, expected);
2955 }
2956}