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
359fn format_generation_duration(duration: std::time::Duration) -> String {
362 if duration.as_secs() >= 60 {
363 let minutes = duration.as_secs() / 60;
364 let seconds = duration.as_secs_f64() - (minutes * 60) as f64;
365 format!("{minutes}m {seconds:.1}s")
366 } else if duration.as_secs_f64() >= 1.0 {
367 format!("{:.2}s", duration.as_secs_f64())
368 } else {
369 format!("{}ms", duration.as_millis())
370 }
371}
372
373fn format_generation_fraction(
374 numerator: std::time::Duration,
375 total: std::time::Duration,
376) -> String {
377 let percent = if total.is_zero() {
378 0.0
379 } else {
380 (numerator.as_secs_f64() / total.as_secs_f64()) * 100.0
381 };
382
383 if percent >= 10.0 {
384 format!("{percent:.0}%")
385 } else if percent >= 1.0 {
386 format!("{percent:.1}%")
387 } else {
388 format!("{percent:.2}%")
389 }
390}
391
392fn format_generation_memory(bytes: u64) -> String {
393 const KIB: f64 = 1024.0;
394 const MIB: f64 = KIB * 1024.0;
395 const GIB: f64 = MIB * 1024.0;
396 const TIB: f64 = GIB * 1024.0;
397
398 let value = bytes as f64;
399 if value < KIB {
400 format!("{bytes} B")
401 } else if value < MIB {
402 format!("{:.2} KiB", value / KIB)
403 } else if value < GIB {
404 format!("{:.2} MiB", value / MIB)
405 } else if value < TIB {
406 format!("{:.2} GiB", value / GIB)
407 } else {
408 format!("{:.2} TiB", value / TIB)
409 }
410}
411
412pub(crate) fn render_generation_summary(
413 reports: &[GeneratedGraphReport],
414 peak_ram_bytes: u64,
415 generation_cores: Option<usize>,
416 title: Option<&str>,
417) -> Option<String> {
418 if reports.is_empty() {
419 return None;
420 }
421
422 let mut sorted_reports = reports.to_vec();
423 sorted_reports.sort_by(|left, right| {
424 (
425 left.process_id,
426 left.integrand_name.as_str(),
427 left.graph_name.as_str(),
428 )
429 .cmp(&(
430 right.process_id,
431 right.integrand_name.as_str(),
432 right.graph_name.as_str(),
433 ))
434 });
435
436 let mut builder = Builder::new();
437 builder.push_record([
438 "integrand".bold().blue().to_string(),
439 "graph".bold().blue().to_string(),
440 "# evals".bold().blue().to_string(),
441 "expr build".bold().blue().to_string(),
442 "spenso".bold().blue().to_string(),
443 "symbolica eval".bold().blue().to_string(),
444 "compile".bold().blue().to_string(),
445 ]);
446
447 let mut total_stats = GraphGenerationStats::default();
448 for report in &sorted_reports {
449 total_stats.merge_in_place(&report.stats);
450 let expr_time = report.stats.expression_build_time();
451 let total_time = report.stats.total_time;
452 let expr_value = format!(
453 "{} ({})",
454 format_generation_duration(expr_time).magenta(),
455 format_generation_fraction(expr_time, total_time).cyan()
456 );
457 let spenso_value = format!(
458 "{} ({})",
459 format_generation_duration(report.stats.evaluator_spenso_time).magenta(),
460 format_generation_fraction(report.stats.evaluator_spenso_time, total_time).cyan()
461 );
462 let symbolica_value = format!(
463 "{} ({})",
464 format_generation_duration(report.stats.evaluator_symbolica_time).magenta(),
465 format_generation_fraction(report.stats.evaluator_symbolica_time, total_time).cyan()
466 );
467 let compile_value = format!(
468 "{} ({})",
469 format_generation_duration(report.stats.evaluator_compile_time).magenta(),
470 format_generation_fraction(report.stats.evaluator_compile_time, total_time).cyan()
471 );
472
473 builder.push_record([
474 report.integrand_name.yellow().to_string(),
475 report.graph_name.yellow().to_string(),
476 report
477 .stats
478 .evaluator_count
479 .to_string()
480 .yellow()
481 .to_string(),
482 expr_value,
483 spenso_value,
484 symbolica_value,
485 compile_value,
486 ]);
487 }
488
489 let total_time = total_stats.total_time;
490 let total_expr_time = total_stats.expression_build_time();
491 let total_expr_value = format!(
492 "{} ({})",
493 format_generation_duration(total_expr_time).magenta(),
494 format_generation_fraction(total_expr_time, total_time).cyan()
495 );
496 let total_spenso_value = format!(
497 "{} ({})",
498 format_generation_duration(total_stats.evaluator_spenso_time).magenta(),
499 format_generation_fraction(total_stats.evaluator_spenso_time, total_time).cyan()
500 );
501 let total_symbolica_value = format!(
502 "{} ({})",
503 format_generation_duration(total_stats.evaluator_symbolica_time).magenta(),
504 format_generation_fraction(total_stats.evaluator_symbolica_time, total_time).cyan()
505 );
506 let total_compile_value = format!(
507 "{} ({})",
508 format_generation_duration(total_stats.evaluator_compile_time).magenta(),
509 format_generation_fraction(total_stats.evaluator_compile_time, total_time).cyan()
510 );
511 builder.push_record([
512 "Total".bold().yellow().to_string(),
513 String::new(),
514 total_stats
515 .evaluator_count
516 .to_string()
517 .bold()
518 .yellow()
519 .to_string(),
520 total_expr_value,
521 total_spenso_value,
522 total_symbolica_value,
523 total_compile_value,
524 ]);
525
526 let mut table = builder.build();
527 let mut style = Theme::from_style(Style::rounded());
528 style.insert_horizontal_line(
529 sorted_reports.len() + 1,
530 HorizontalLine::inherit(Style::modern()),
531 );
532 table.with(style);
533 let mut sections = Vec::new();
534 if let Some(title) = title {
535 sections.push(title.bold().blue().to_string());
536 }
537 let resources = match generation_cores {
538 Some(cores) => {
539 format!(
540 "{} {} | {} {}",
541 "peak RAM".bold().blue(),
542 format_generation_memory(peak_ram_bytes).yellow(),
543 "cores".bold().blue(),
544 cores.to_string().yellow(),
545 )
546 }
547 None => format!(
548 "{} {}",
549 "peak RAM".bold().blue(),
550 format_generation_memory(peak_ram_bytes).yellow(),
551 ),
552 };
553 sections.push(resources);
554 sections.push(table.to_string());
555 Some(sections.join("\n"))
556}
557
558fn log_generation_summary_table(
559 reports: &[GeneratedGraphReport],
560 resources: GenerationResourceSummary,
561) {
562 if let Some(summary) = render_generation_summary(
563 reports,
564 resources.peak_ram_bytes,
565 Some(resources.generation_cores),
566 Some("Integrand generation summary"),
567 ) {
568 info!("\n{summary}");
569 }
570}
571
572fn remove_compiled_cpp_sources(root: &Path) -> Result<usize> {
573 let mut removed_count = 0usize;
574 for entry in WalkDir::new(root) {
575 let entry = entry?;
576 if !entry.file_type().is_file() {
577 continue;
578 }
579
580 let source_path = entry.path();
581 if source_path.extension() != Some(OsStr::new("cpp")) {
582 continue;
583 }
584
585 let library_path = source_path.with_extension("so");
586 if !library_path.is_file() {
587 continue;
588 }
589
590 fs::remove_file(source_path).with_context(|| {
591 format!(
592 "Trying to remove generated C++ source {} after compiling {}",
593 source_path.display(),
594 library_path.display()
595 )
596 })?;
597 removed_count += 1;
598 }
599
600 Ok(removed_count)
601}
602
603fn compile_integrands_for_generation(
604 state: &mut State,
605 compile_folder: &Path,
606 override_existing_compiled: bool,
607 global_settings: &GlobalSettings,
608 process_id: Option<usize>,
609 integrand_name: Option<String>,
610 keep_sources: bool,
611) -> Result<Vec<GeneratedGraphReport>> {
612 let reports = state.compile_integrands(
613 compile_folder,
614 override_existing_compiled,
615 global_settings,
616 process_id,
617 integrand_name,
618 )?;
619
620 if !keep_sources {
621 let removed_count = remove_compiled_cpp_sources(compile_folder)?;
622 if removed_count > 0 {
623 info!(
624 "Removed {} generated C++ source file(s). Use '{}' to keep them.",
625 removed_count,
626 "--keep-sources".green()
627 );
628 }
629 }
630
631 Ok(reports)
632}
633
634fn finish_generation(
635 state: &mut State,
636 reports: &[GeneratedGraphReport],
637 resources: GenerationResourceSummary,
638) {
639 state.record_generation_summary(reports, resources);
640 log_generation_summary_table(reports, resources);
641}
642
643impl Generate {
644 pub fn run(
645 &self,
646 state: &mut State,
647 compile_folder: impl AsRef<Path>,
648 override_existing_compiled: bool,
649 global_settings: &GlobalSettings,
650 runtime_settings: &RuntimeSettings,
651 ) -> Result<()> {
652 let compile_folder = compile_folder.as_ref();
653 let generation_mode = match &self.mode {
654 Some(GenerateCmd::Xs(a)) => Some((GenerationType::CrossSection, a)),
655 Some(GenerateCmd::Amp(a)) => Some((GenerationType::Amplitude, a)),
656 _ => None,
657 };
658 if let Some((_, args)) = generation_mode.as_ref() {
659 if !state.process_list.processes.is_empty() && args.clear_existing_processes {
660 info!(
661 "Clearing all {} existing processes as requested.",
662 state.process_list.processes.len()
663 );
664 state.process_list = ProcessList::default();
665 }
666 }
667 let generation_info = if let Some((gen_mode, args)) = generation_mode {
668 let mut spec = parse_spec_with_model(args, gen_mode, &state.model)?;
669 spec.process_definition.process_id = state.process_list.processes.len();
670
671 let mut existing_process = None;
672 if let Some(ep) = state
673 .process_list
674 .processes
675 .iter_mut()
676 .find(|p| p.definition.folder_name == spec.process_definition.folder_name)
677 {
678 if ep.definition != spec.process_definition {
679 if !args.append {
680 return Err(eyre!(
681 "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>'.",
682 spec.process_definition.folder_name
683 ));
684 }
685 } else {
686 info!(
687 "Identical process definition, with name '{}', already exists. Gammaloop will recycle it.",
688 spec.process_definition.folder_name
689 );
690 return Ok(());
691 }
692 spec.process_definition.process_id = ep.definition.process_id;
693 existing_process = Some(ep);
694 }
695 Some((spec, existing_process))
696 } else {
697 None
698 };
699 match &self.mode {
700 Some(GenerateCmd::Amp(args)) | Some(GenerateCmd::Xs(args)) => {
701 let generation_type = generation_mode.as_ref().unwrap().0;
702 let model: &Model = &state.model;
703 let (spec, existing_process) = generation_info.unwrap();
704 let this_process_id = spec.process_definition.process_id;
705 let graphs = spec.process_definition.generate(model, global_settings)?;
707 info!(
708 "Generated {} {} graphs.",
709 if matches!(self.mode, Some(GenerateCmd::Amp(_))) {
710 "amplitude"
711 } else {
712 "cross-section"
713 },
714 graphs.len()
715 );
716 let integrand_base_name = matches!(self.mode, Some(GenerateCmd::Amp(_)))
718 .then(|| args.integrand_name.clone().unwrap_or("default".to_string()))
719 .unwrap_or_else(|| {
720 args.integrand_name.clone().unwrap_or("default".to_string())
721 });
722 let generated_integrand_name = if let Some(p) = existing_process {
723 let existing_names = p.collection.get_integrand_names();
724 let integrand_name = if existing_names.contains(&integrand_base_name.as_str()) {
725 let mut integrand_i = 0;
726 while existing_names
727 .iter()
728 .any(|ce| *ce == format!("{}_{}", integrand_base_name, integrand_i))
729 {
730 integrand_i += 1;
731 }
732 format!("{}_{}", integrand_base_name, integrand_i)
733 } else {
734 integrand_base_name
735 };
736 match &self.mode {
737 Some(GenerateCmd::Amp(_)) => {
738 p.collection.add_amplitude(Amplitude::from_graph_list(
739 integrand_name.clone(),
740 graphs,
741 )?);
742 }
743 Some(GenerateCmd::Xs(_)) => {
744 p.collection
745 .add_cross_section(CrossSection::from_graph_list(
746 integrand_name.clone(),
747 graphs,
748 model,
749 )?);
750 }
751 _ => unreachable!(),
752 }
753 integrand_name
754 } else {
755 let process = Process::from_graph_list(
756 spec.process_definition.folder_name.clone(),
757 integrand_base_name.clone(),
758 graphs,
759 generation_type,
760 Some(spec.process_definition),
761 None,
762 model,
763 )?;
764 state.process_list.add_process(process);
765 integrand_base_name
766 };
767 if !args.only_diagrams {
768 let generated_integrand_name_for_compile = generated_integrand_name.clone();
769 let generation = state.generate_integrand(
770 global_settings,
771 runtime_settings.into(),
772 this_process_id,
773 Some(generated_integrand_name),
774 )?;
775 let mut reports = generation.reports;
776 if global_settings.generation.evaluator.compile
777 && global_settings
778 .generation
779 .compile
780 .requires_external_compilation()
781 {
782 merge_generated_graph_reports(
783 &mut reports,
784 compile_integrands_for_generation(
785 state,
786 compile_folder,
787 override_existing_compiled,
788 global_settings,
789 Some(this_process_id),
790 Some(generated_integrand_name_for_compile),
791 self.keep_sources,
792 )?,
793 );
794 }
795 finish_generation(state, &reports, generation.resources);
796 Ok(())
797 } else {
798 info!(
799 "Only diagram generation was requested, skipping integrand generation. You can generate integrands later using the '{}' command.",
800 "generate existing <options>".green()
801 );
802 Ok(())
803 }
804 }
805 Some(GenerateCmd::Existing(process_args)) => {
806 let process_id = state.resolve_process_ref(process_args.process.as_ref())?;
807
808 let generation = state.generate_integrand(
809 global_settings,
810 runtime_settings.into(),
811 process_id,
812 process_args.integrand_name.clone(),
813 )?;
814 let mut reports = generation.reports;
815 if global_settings.generation.evaluator.compile
816 && global_settings
817 .generation
818 .compile
819 .requires_external_compilation()
820 {
821 merge_generated_graph_reports(
822 &mut reports,
823 compile_integrands_for_generation(
824 state,
825 compile_folder,
826 override_existing_compiled,
827 global_settings,
828 Some(process_id),
829 process_args.integrand_name.clone(),
830 self.keep_sources,
831 )?,
832 );
833 }
834 finish_generation(state, &reports, generation.resources);
835 Ok(())
836 }
837 None => {
838 let generation =
839 state.generate_integrands(global_settings, runtime_settings.into())?;
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 None,
855 None,
856 self.keep_sources,
857 )?,
858 );
859 }
860 finish_generation(state, &reports, generation.resources);
861 Ok(())
862 }
863 }
864 }
865}
866
867#[derive(Args, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
868pub struct ProcessArgs {
869 #[arg(
871 long = "process",
872 short = 'p',
873 value_name = "PROCESS",
874 completion_process_selector(crate::completion::SelectorKind::Any)
875 )]
876 pub process: Option<ProcessRef>,
877
878 #[arg(
880 long = "integrand-name",
881 short = 'i',
882 completion_integrand_selector(crate::completion::SelectorKind::Any)
883 )]
884 pub integrand_name: Option<String>,
885}
886
887#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
890pub struct OrderRange {
891 pub eq: Option<u32>,
892 pub min: Option<u32>,
893 pub max: Option<u32>,
894}
895impl OrderRange {
896 fn set(&mut self, op: &str, v: u32) {
897 match op {
898 "==" => self.eq = Some(v),
899 ">=" => self.min = Some(v),
900 "<=" => self.max = Some(v),
901 _ => {}
902 }
903 }
904}
905
906#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord)]
907pub struct CouplingKey {
908 pub name: String,
909 pub power: u32, }
911
912#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
913pub struct Perturbative {
914 pub loops_sum_amp_or_sum: Option<u32>,
916 pub loops_forward_graph: Option<u32>,
918 pub orders: BTreeMap<String, u32>,
920}
921
922#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
923pub struct ProcessSpec {
924 pub initial: Vec<String>,
925 pub final_: Vec<String>,
926 pub empty_initial: bool,
927 pub empty_final: bool,
928
929 pub veto: BTreeSet<String>,
931 pub only: Option<BTreeSet<String>>,
933
934 pub amp_couplings: BTreeMap<String, OrderRange>,
936 pub xs_couplings: BTreeMap<CouplingKey, OrderRange>,
938
939 pub pert: Perturbative,
941
942 pub final_sets: Vec<Vec<String>>,
944
945 pub process_definition: ProcessDefinition,
947
948 pub numerator_grouping: NumeratorAwareGraphGroupingOption,
950}
951
952impl ProcessSpec {
953 pub fn process_shell_name(&self, short: bool) -> String {
955 let sanitize_piece = |s: &str| s.replace('~', "x").replace('+', "p").replace('-', "m");
957 let lower = |s: &str| s.to_ascii_lowercase();
958
959 let init_slug = if self.initial.is_empty() {
961 "empty".to_string()
962 } else {
963 self.initial.iter().map(|p| lower(p)).collect::<String>()
964 };
965
966 let finals_lists: Vec<Vec<String>> = if self.final_sets.is_empty() {
967 vec![self.final_.clone()]
968 } else {
969 self.final_sets.clone()
970 };
971 let finals_slugs: Vec<String> = finals_lists
972 .iter()
973 .map(|lst| {
974 if lst.is_empty() {
975 "empty".to_string()
976 } else {
977 lst.iter().map(|p| lower(p)).collect::<String>()
978 }
979 })
980 .collect();
981
982 let mut pieces: Vec<String> = vec![sanitize_piece(&format!(
983 "{}_{}",
984 init_slug,
985 finals_slugs.join("_or_")
986 ))];
987
988 if !short {
989 if !self.veto.is_empty() {
991 let mut veto_list: Vec<String> = self.veto.iter().map(|v| lower(v)).collect();
992 veto_list.sort();
993 pieces.push(sanitize_piece(&format!("no__{}", veto_list.join("_"))));
994 }
995
996 if !self.amp_couplings.is_empty() {
998 let mut keys: Vec<&String> = self.amp_couplings.keys().collect();
999 keys.sort();
1000 let mut parts: Vec<String> = Vec::new();
1001 for k in keys {
1002 let r = &self.amp_couplings[k];
1003 if let Some(eq) = r.eq {
1004 parts.push(format!("{}_eq_{}", k, eq));
1005 } else {
1006 if let Some(min) = r.min {
1007 if min > 0 {
1008 parts.push(format!("{}_ge_{}", k, min));
1009 }
1010 }
1011 if let Some(max) = r.max {
1012 parts.push(format!("{}_le_{}", k, max));
1013 }
1014 }
1015 }
1016 if !parts.is_empty() {
1017 pieces.push(parts.join("__"));
1018 }
1019 }
1020
1021 if !self.xs_couplings.is_empty() {
1023 let mut entries: Vec<(&CouplingKey, &OrderRange)> =
1024 self.xs_couplings.iter().collect();
1025 entries.sort_by(|(ka, _), (kb, _)| {
1026 use std::cmp::Ordering;
1027 match ka.name.cmp(&kb.name) {
1028 Ordering::Equal => ka.power.cmp(&kb.power),
1029 other => other,
1030 }
1031 });
1032 let mut parts: Vec<String> = Vec::new();
1033 for (k, r) in entries {
1034 let base = format!("{}sq", k.name);
1035 if let Some(eq) = r.eq {
1036 parts.push(format!("{}_eq_{}", base, eq));
1037 } else {
1038 if let Some(min) = r.min {
1039 if min > 0 {
1040 parts.push(format!("{}_ge_{}", base, min));
1041 }
1042 }
1043 if let Some(max) = r.max {
1044 parts.push(format!("{}_le_{}", base, max));
1045 }
1046 }
1047 }
1048 if !parts.is_empty() {
1049 pieces.push(parts.join("__"));
1050 }
1051 }
1052
1053 if !self.pert.orders.is_empty() {
1055 let mut ords: Vec<(&String, &u32)> = self.pert.orders.iter().collect();
1056 ords.sort_by(|a, b| a.0.cmp(b.0));
1057 let parts: Vec<String> = ords
1058 .into_iter()
1059 .map(|(k, v)| format!("{}loop_eq_{}", k, v))
1060 .collect();
1061 if !parts.is_empty() {
1062 pieces.push(parts.join("__"));
1063 }
1064 }
1065 }
1066
1067 pieces.join("-")
1068 }
1069
1070 pub fn repr_str(&self) -> String {
1072 let lower = |s: &str| s.to_ascii_lowercase();
1073
1074 let mut out: Vec<String> = Vec::new();
1075
1076 if self.initial.is_empty() {
1078 out.push("{}".to_string());
1079 } else {
1080 for p in &self.initial {
1081 out.push(lower(p));
1082 }
1083 }
1084
1085 out.push(">".to_string());
1087
1088 if self.final_sets.is_empty() {
1090 if self.final_.is_empty() {
1091 out.push("{}".to_string());
1092 } else {
1093 for p in &self.final_ {
1094 out.push(lower(p));
1095 }
1096 }
1097 } else {
1098 let sets_str = self
1100 .final_sets
1101 .iter()
1102 .map(|set| {
1103 if set.is_empty() {
1104 "empty".to_string()
1105 } else {
1106 set.iter().map(|p| lower(p)).collect::<Vec<_>>().join(" ")
1107 }
1108 })
1109 .collect::<Vec<_>>()
1110 .join(", ");
1111 out.push(format!("{{ {} }}", sets_str));
1112 }
1113
1114 if !self.veto.is_empty() {
1116 out.push("/".to_string());
1117 let mut veto_list: Vec<String> = self.veto.iter().map(|v| lower(v)).collect();
1118 veto_list.sort();
1119 out.extend(veto_list);
1120 }
1121
1122 if !self.amp_couplings.is_empty() {
1124 let mut keys: Vec<&String> = self.amp_couplings.keys().collect();
1125 keys.sort();
1126 for k in keys {
1127 let r = &self.amp_couplings[k];
1128 if let Some(eq) = r.eq {
1129 out.push(format!("{}=={}", k, eq));
1130 } else {
1131 if let Some(min) = r.min {
1132 if min > 0 {
1133 out.push(format!("{}>={}", k, min));
1134 }
1135 }
1136 if let Some(max) = r.max {
1137 out.push(format!("{}<={}", k, max));
1138 }
1139 }
1140 }
1141 }
1142
1143 let has_pert_block = self.pert.loops_sum_amp_or_sum.is_some()
1145 || self.pert.loops_forward_graph.is_some()
1146 || !self.pert.orders.is_empty();
1147 if has_pert_block {
1148 out.push("[".to_string());
1149 if let Some(n) = self.pert.loops_sum_amp_or_sum {
1150 out.push(format!("{{{}}}", n));
1151 }
1152 if let Some(n) = self.pert.loops_forward_graph {
1153 out.push(format!("{{{{{}}}}}", n));
1154 }
1155 if !self.pert.orders.is_empty() {
1156 let mut ords: Vec<(&String, &u32)> = self.pert.orders.iter().collect();
1157 ords.sort_by(|a, b| a.0.cmp(b.0));
1158 for (k, v) in ords {
1159 if *v > 0 {
1160 if *v == 1 {
1161 out.push(k.clone());
1162 } else {
1163 out.push(format!("{}={}", k, v));
1164 }
1165 }
1166 }
1167 }
1168 out.push("]".to_string());
1169 }
1170
1171 if !self.xs_couplings.is_empty() {
1173 let mut entries: Vec<(&CouplingKey, &OrderRange)> = self.xs_couplings.iter().collect();
1174 entries.sort_by(|(ka, _), (kb, _)| {
1175 use std::cmp::Ordering;
1176 match ka.name.cmp(&kb.name) {
1177 Ordering::Equal => ka.power.cmp(&kb.power),
1178 other => other,
1179 }
1180 });
1181 for (k, r) in entries {
1182 if let Some(eq) = r.eq {
1183 out.push(format!("{}^{}=={}", k.name, k.power, eq));
1184 } else {
1185 if let Some(min) = r.min {
1186 if min > 0 {
1187 out.push(format!("{}^{}>={}", k.name, k.power, min));
1188 }
1189 }
1190 if let Some(max) = r.max {
1191 out.push(format!("{}^{}<={}", k.name, k.power, max));
1192 }
1193 }
1194 }
1195 }
1196
1197 out.join(" ")
1198 }
1199}
1200
1201#[derive(Debug, Error, PartialEq, Eq)]
1204pub enum ParseError {
1205 #[error("expected 'to' or '>' between initial and final states")]
1206 MissingArrow,
1207 #[error("unbalanced brackets/braces")]
1208 Unbalanced,
1209 #[error("invalid token '{0}'")]
1210 InvalidToken(String),
1211 #[error("unknown particle '{name}'. Valid choices: {choices}")]
1212 UnknownParticle { name: String, choices: String },
1213 #[error("unknown coupling '{name}'. Valid choices: {choices}")]
1214 UnknownCoupling { name: String, choices: String },
1215 #[error("invalid lmb specification: {0}")]
1216 InvalidLmbSpec(ParseIntError),
1217}
1218
1219pub fn parse_spec_with_model(
1220 args: &SpecArgs,
1221 generation_type: GenerationType,
1222 model: &Model,
1223) -> std::result::Result<ProcessSpec, ParseError> {
1224 let raw = args.tokens.join(" ");
1225 let (lhs, rhs0) = split_top_level_arrow(&raw).ok_or(ParseError::MissingArrow)?;
1226 let (rhs_wo_bracket, pert) = extract_perturbative(rhs0.trim())?;
1227
1228 let lhs = lhs.trim();
1230 let (initial_names, empty_initial) = if lhs == "{}" || lhs.eq_ignore_ascii_case("empty-initial")
1231 {
1232 (Vec::new(), true)
1233 } else {
1234 (split_ws(lhs), false)
1235 };
1236
1237 let tokens = tokenize(rhs_wo_bracket)?;
1239 let (final_spec, empty_final, rest) = parse_final_states(tokens)?;
1240 let (veto_names, only, amp_couplings, xs_couplings) = parse_process_options(rest)?;
1241
1242 validate_coupling_names(model, &_couplings, &xs_couplings, &pert)?;
1244
1245 let initial_pdgs = resolve_pdgs(model, &initial_names)?;
1247 let primary_final_pdgs = resolve_pdgs(model, &final_spec.primary)?;
1248 let final_sets_pdgs = final_spec
1249 .sets
1250 .iter()
1251 .map(|set| resolve_pdgs(model, set))
1252 .collect::<Result<Vec<_>, _>>()?;
1253
1254 let mut veto_pdgs = resolve_pdgs(model, &veto_names.iter().cloned().collect::<Vec<_>>())?;
1256 if let Some(o) = only.clone() {
1257 let only_vec = o.iter().cloned().collect::<Vec<_>>();
1258 if !only_vec.is_empty() {
1259 let only_pdgs = resolve_pdgs(model, &only_vec)?
1260 .iter()
1261 .map(|pdg| pdg.abs())
1262 .collect::<Vec<_>>();
1263
1264 let all_pdgs = model
1265 .particles
1266 .iter()
1267 .map(|p| p.pdg_code.abs() as i64)
1268 .filter(|pdg| !only_pdgs.contains(pdg))
1269 .collect::<Vec<_>>();
1270
1271 veto_pdgs.extend(all_pdgs);
1272 veto_pdgs.sort_unstable();
1273 veto_pdgs.dedup();
1274 }
1275 }
1276
1277 let numerator_grouping = build_grouping_option(args);
1279 let process_definition = feyngen_from_spec_args(
1280 args,
1281 generation_type,
1282 &pert,
1283 &_couplings,
1284 &xs_couplings,
1285 &initial_pdgs,
1286 &primary_final_pdgs,
1287 &final_sets_pdgs,
1288 &veto_pdgs,
1289 );
1290
1291 let mut spec = ProcessSpec {
1292 initial: initial_names,
1293 final_: final_spec.primary.clone(),
1294 empty_initial,
1295 empty_final,
1296 veto: veto_names,
1297 only,
1298 amp_couplings,
1299 xs_couplings,
1300 pert,
1301 final_sets: final_spec.sets,
1302 process_definition,
1303 numerator_grouping,
1304 };
1305
1306 if let Some(process_name) = &args.process_name {
1307 spec.process_definition.folder_name = process_name.clone();
1308 } else {
1309 spec.process_definition.folder_name = spec.process_shell_name(true);
1310 }
1311 spec.process_definition.numerator_grouping = spec.numerator_grouping.clone();
1312 spec.process_definition.filter_self_loop = args.filter_self_loop.unwrap_or(false);
1313 spec.process_definition.filter_zero_flow_edges = args.filter_zero_flow_edges.unwrap_or(true);
1314
1315 spec.process_definition.graph_prefix = args
1316 .graph_prefix
1317 .clone()
1318 .unwrap_or_else(|| "GL".to_string());
1319 spec.process_definition.selected_graphs = args.select_graphs.clone();
1323 spec.process_definition.vetoed_graphs = args.veto_graphs.clone();
1324 spec.process_definition.loop_momentum_bases = args
1325 .loop_momentum_bases
1326 .as_deref()
1327 .map(|kvs| {
1328 kvs.iter()
1329 .map(|kv| {
1330 parse_csv_list(&kv.value)
1331 .into_iter()
1332 .map(|s| s.parse::<usize>())
1333 .collect::<core::result::Result<Vec<_>, _>>()
1334 .map(|lst| (kv.key.clone(), lst))
1335 })
1336 .collect::<core::result::Result<HashMap<_, _>, _>>()
1337 })
1338 .transpose()
1339 .map_err(ParseError::InvalidLmbSpec)?;
1340 let mut prefactor = GlobalPrefactor::default();
1341 if let Some(projector) = &args.global_prefactor_projector {
1342 prefactor.projector = parse!(projector);
1343 }
1344 if let Some(num) = &args.global_prefactor_num {
1345 prefactor.num = parse!(num);
1346 }
1347 spec.process_definition.prefactor = prefactor;
1348
1349 Ok(spec)
1350}
1351
1352fn split_top_level_arrow(s: &str) -> Option<(&str, &str)> {
1355 let mut depth_brace = 0i32;
1357 let mut depth_bracket = 0i32;
1358 let bytes = s.as_bytes();
1359 let mut i = 0usize;
1360 while i < bytes.len() {
1361 let c = bytes[i] as char;
1362 match c {
1363 '{' => depth_brace += 1,
1364 '}' => depth_brace -= 1,
1365 '[' => depth_bracket += 1,
1366 ']' => depth_bracket -= 1,
1367 '>' if depth_brace == 0 && depth_bracket == 0 => {
1368 return Some((&s[..i], &s[i + 1..]));
1369 }
1370 _ => {
1371 if depth_brace == 0
1372 && depth_bracket == 0
1373 && i + 2 < bytes.len()
1374 && (c == 't' || c == 'T')
1375 && &s[i..].to_ascii_lowercase().as_str()[..2] == "to"
1376 {
1377 let before = if i == 0 {
1378 ' '
1379 } else {
1380 s.as_bytes()[i - 1] as char
1381 };
1382 let after = if i + 2 >= s.len() {
1383 ' '
1384 } else {
1385 s.as_bytes()[i + 2] as char
1386 };
1387 if before.is_whitespace() && after.is_whitespace() {
1388 return Some((&s[..i], &s[i + 2..]));
1389 }
1390 }
1391 }
1392 }
1393 i += 1;
1394 }
1395 None
1396}
1397
1398fn extract_perturbative(rhs: &str) -> Result<(String, Perturbative), ParseError> {
1399 let mut out = rhs.to_string();
1400 let mut spec = Perturbative::default();
1401 if let Some((start, end)) = find_top_level_block(rhs, '[', ']') {
1402 let inside = &rhs[start + 1..end];
1403 spec = parse_perturbative_block(inside.trim())?;
1404 out.replace_range(start..=end, "");
1405 }
1406 Ok((out, spec))
1407}
1408
1409fn parse_perturbative_block(s: &str) -> Result<Perturbative, ParseError> {
1410 if s.is_empty() {
1411 return Ok(Perturbative::default());
1412 }
1413 let re_amp = Regex::new(r"^\{(\d+)\}$").unwrap();
1414 let re_fwd = Regex::new(r"^\{\{(\d+)\}\}$").unwrap();
1415 let re_order = Regex::new(r"^([A-Za-z_][A-Za-z0-9_]*)=(\d+)$").unwrap();
1416
1417 let mut spec = Perturbative::default();
1418 for tok in split_ws(s) {
1419 if let Some(c) = re_amp.captures(&tok) {
1420 spec.loops_sum_amp_or_sum = Some(c[1].parse().unwrap());
1421 continue;
1422 }
1423 if let Some(c) = re_fwd.captures(&tok) {
1424 spec.loops_forward_graph = Some(c[1].parse().unwrap());
1425 continue;
1426 }
1427 if let Some(c) = re_order.captures(&tok) {
1428 let n: u32 = c[2].parse().unwrap();
1429 spec.orders.insert(c[1].to_string(), n);
1430 continue;
1431 }
1432 if tok.chars().all(|ch| ch.is_ascii_alphabetic()) {
1433 spec.orders.insert(tok, 1);
1434 continue;
1435 }
1436 return Err(ParseError::InvalidToken(tok));
1437 }
1438 Ok(spec)
1439}
1440
1441fn tokenize(s: String) -> Result<Vec<String>, ParseError> {
1442 let mut out = Vec::new();
1443 let mut cur = String::new();
1444 let mut depth = 0i32;
1445 for ch in s.chars() {
1446 match ch {
1447 '{' => {
1448 depth += 1;
1449 cur.push(ch);
1450 }
1451 '}' => {
1452 depth -= 1;
1453 cur.push(ch);
1454 if depth < 0 {
1455 return Err(ParseError::Unbalanced);
1456 }
1457 }
1458 c if c.is_whitespace() && depth == 0 => {
1459 if !cur.trim().is_empty() {
1460 out.push(cur.trim().to_string());
1461 }
1462 cur.clear();
1463 }
1464 _ => cur.push(ch),
1465 }
1466 }
1467 if !cur.trim().is_empty() {
1468 out.push(cur.trim().to_string());
1469 }
1470 if depth != 0 {
1471 return Err(ParseError::Unbalanced);
1472 }
1473 Ok(out)
1474}
1475
1476fn parse_final_states(tokens: Vec<String>) -> Result<(FinalSpec, bool, Vec<String>), ParseError> {
1477 if tokens.is_empty() {
1478 return Ok((FinalSpec::default(), false, vec![]));
1479 }
1480 let mut t = tokens;
1481 let mut finals = Vec::<String>::new();
1482 let mut empty_final = false;
1483
1484 if t[0].eq_ignore_ascii_case("empty-final") || t[0] == "{}" {
1485 empty_final = true;
1486 t.remove(0);
1487 return Ok((FinalSpec::default(), empty_final, t));
1488 }
1489
1490 if t[0].starts_with('{') {
1491 let raw = t.remove(0);
1492 let sets = parse_alt_sets(&raw)?;
1493 return Ok((
1494 FinalSpec {
1495 primary: vec![],
1496 sets,
1497 },
1498 empty_final,
1499 t,
1500 ));
1501 }
1502
1503 while !t.is_empty() {
1504 if is_option_token(&t[0]) {
1505 break;
1506 }
1507 finals.push(t.remove(0));
1508 }
1509 Ok((
1510 FinalSpec {
1511 primary: finals,
1512 sets: vec![],
1513 },
1514 empty_final,
1515 t,
1516 ))
1517}
1518
1519#[derive(Default)]
1520struct FinalSpec {
1521 primary: Vec<String>,
1522 sets: Vec<Vec<String>>,
1523}
1524
1525fn parse_alt_sets(s: &str) -> Result<Vec<Vec<String>>, ParseError> {
1526 let trimmed = s.trim();
1527 if !(trimmed.starts_with('{') && trimmed.ends_with('}')) {
1528 return Err(ParseError::InvalidToken(s.to_string()));
1529 }
1530 let inner = &trimmed[1..trimmed.len() - 1];
1531 let mut out: Vec<Vec<String>> = Vec::new();
1532 let mut cur = String::new();
1533 let mut depth = 0i32;
1534 for ch in inner.chars() {
1535 match ch {
1536 '{' => {
1537 depth += 1;
1538 cur.push(ch);
1539 }
1540 '}' => {
1541 depth -= 1;
1542 if depth < 0 {
1543 return Err(ParseError::Unbalanced);
1544 }
1545 cur.push(ch);
1546 }
1547 ',' if depth == 0 => {
1548 let items = split_ws(cur.trim());
1549 if !items.is_empty() {
1550 out.push(items);
1551 } else {
1552 out.push(vec![]);
1553 }
1554 cur.clear();
1555 }
1556 _ => cur.push(ch),
1557 }
1558 }
1559 if !cur.trim().is_empty() {
1560 out.push(split_ws(cur.trim()));
1561 }
1562 Ok(out)
1563}
1564
1565fn is_option_token(tok: &str) -> bool {
1566 matches!(
1567 tok,
1568 "/" | "|" | "veto" | "only" | "amp" | "xs" | "pert" | "sets" | "empty-final"
1569 ) || tok.starts_with('[')
1570 || Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*(?:\^\d+)?(==|>=|<=)\d+$")
1571 .unwrap()
1572 .is_match(tok)
1573}
1574
1575#[allow(clippy::type_complexity)]
1576fn parse_process_options(
1577 tokens: Vec<String>,
1578) -> Result<
1579 (
1580 BTreeSet<String>,
1581 Option<BTreeSet<String>>,
1582 BTreeMap<String, OrderRange>,
1583 BTreeMap<CouplingKey, OrderRange>,
1584 ),
1585 ParseError,
1586> {
1587 let mut veto = BTreeSet::<String>::new();
1588 let mut only: Option<BTreeSet<String>> = None;
1589 let mut amp = BTreeMap::<String, OrderRange>::new();
1590 let mut xs = BTreeMap::<CouplingKey, OrderRange>::new();
1591
1592 let re = Regex::new(r"^([A-Za-z_][A-Za-z0-9_]*)(?:\^(\d+))?(==|>=|<=)(\d+)$").unwrap();
1593
1594 let mut i = 0usize;
1595 while i < tokens.len() {
1596 match tokens[i].as_str() {
1597 "/" | "veto" => {
1598 i += 1;
1599 while i < tokens.len() && !is_option_token(&tokens[i]) {
1600 veto.insert(tokens[i].clone());
1601 i += 1;
1602 }
1603 }
1604 "|" | "only" => {
1605 i += 1;
1606 let mut s = only.take().unwrap_or_default();
1607 while i < tokens.len() && !is_option_token(&tokens[i]) {
1608 s.insert(tokens[i].clone());
1609 i += 1;
1610 }
1611 only = Some(s);
1612 }
1613 "amp" | "xs" | "pert" | "sets" | "empty-final" => {
1614 return Err(ParseError::InvalidToken(tokens[i].clone()));
1615 }
1616 other => {
1617 if let Some(c) = re.captures(other) {
1618 let name = c[1].to_string();
1619 let power: u32 = c.get(2).map(|m| m.as_str().parse().unwrap()).unwrap_or(1);
1620 let op = c.get(3).unwrap().as_str();
1621 let val: u32 = c.get(4).unwrap().as_str().parse().unwrap();
1622 if power == 1 {
1623 let e = amp.entry(name).or_default();
1624 e.set(op, val);
1625 } else {
1626 let key = CouplingKey { name, power };
1627 let e = xs.entry(key).or_default();
1628 e.set(op, val);
1629 }
1630 } else {
1631 return Err(ParseError::InvalidToken(other.to_string()));
1632 }
1633 i += 1;
1634 }
1635 }
1636 }
1637 Ok((veto, only, amp, xs))
1638}
1639
1640fn find_top_level_block(s: &str, open: char, close: char) -> Option<(usize, usize)> {
1641 let mut depth = 0i32;
1642 let mut start: Option<usize> = None;
1643 for (i, ch) in s.char_indices() {
1644 if ch == open {
1645 if depth == 0 {
1646 start = Some(i);
1647 }
1648 depth += 1;
1649 } else if ch == close {
1650 depth -= 1;
1651 if depth == 0 {
1652 return start.map(|st| (st, i));
1653 }
1654 }
1655 }
1656 None
1657}
1658
1659fn split_ws(s: &str) -> Vec<String> {
1660 s.split_whitespace().map(|t| t.to_string()).collect()
1661}
1662
1663fn feyngen_from_spec_args(
1666 a: &SpecArgs,
1667 generation_type: GenerationType,
1668 pert: &Perturbative,
1669 amp_couplings: &BTreeMap<String, OrderRange>,
1670 xs_couplings: &BTreeMap<CouplingKey, OrderRange>,
1671 initial_pdgs: &[i64],
1672 primary_final_pdgs: &[i64],
1673 final_sets_pdgs: &[Vec<i64>],
1674 veto_pdgs: &[i64],
1675) -> ProcessDefinition {
1676 let is_vacuum = if initial_pdgs.is_empty() {
1678 match generation_type {
1679 GenerationType::CrossSection => true,
1680 GenerationType::Amplitude => {
1681 primary_final_pdgs.is_empty()
1682 && (final_sets_pdgs
1683 .first()
1684 .map(|v| v.is_empty())
1685 .unwrap_or(true))
1686 }
1687 }
1688 } else {
1689 false
1690 };
1691
1692 let filter_tadpoles_default = !is_vacuum;
1694 let filter_snails_default = !is_vacuum;
1695 let filter_selfenergies_default = !is_vacuum;
1696
1697 let filter_tadpoles = a.filter_tadpoles.unwrap_or(filter_tadpoles_default);
1699 let filter_snails = a.filter_snails.unwrap_or(filter_snails_default);
1700 let filter_selfenergies = a.filter_selfenergies.unwrap_or(filter_selfenergies_default);
1701
1702 let number_of_factorized_loop_subtopologies = a
1704 .number_of_factorized_loop_subtopologies
1705 .clone()
1706 .or(if is_vacuum {
1707 Some(vec![1_i32, 1_i32])
1708 } else {
1709 None
1710 })
1711 .and_then(|v| {
1712 if v[0] < 0 && v[1] < 0 {
1713 None
1714 } else {
1715 Some((v[0].max(0) as usize, v[1].max(0) as usize))
1716 }
1717 });
1718
1719 let max_n_bridges = a
1720 .max_n_bridges
1721 .or(if is_vacuum { Some(0) } else { None })
1722 .and_then(|n| if n < 0 { None } else { Some(n as usize) });
1723
1724 let number_of_fermion_loops = a
1725 .number_of_fermion_loops
1726 .as_ref()
1727 .map(|v| (v[0].max(0) as usize, v[1].max(0) as usize));
1728
1729 let blob_range: RangeInclusive<usize> = {
1731 let (lo, hi) = a
1732 .n_cut_blobs
1733 .as_ref()
1734 .map(|v| (v[0], v[1]))
1735 .unwrap_or((1, 1));
1736 lo..=hi
1737 };
1738 let spectator_range: RangeInclusive<usize> = {
1739 let (lo, hi) = a
1740 .n_cut_spectators
1741 .as_ref()
1742 .map(|v| (v[0], v[1]))
1743 .unwrap_or((0, 0));
1744 lo..=hi
1745 };
1746
1747 let sym_left_right = a.symmetrize_left_right_states.unwrap_or(false);
1749 let (sym_init, sym_final) = match generation_type {
1750 GenerationType::Amplitude => {
1751 let s_init = a
1752 .symmetrize_initial_states
1753 .unwrap_or(sym_left_right );
1754 let s_final = a
1755 .symmetrize_final_states
1756 .unwrap_or(sym_left_right );
1757 (s_init, s_final)
1758 }
1759 GenerationType::CrossSection => {
1760 let s_init = a.symmetrize_initial_states.unwrap_or(false);
1761 let s_final = a.symmetrize_final_states.unwrap_or(true);
1762 (s_init, s_final)
1763 }
1764 };
1765 let allow_symferm = a
1766 .allow_symmetrization_of_external_fermions_in_amplitudes
1767 .unwrap_or(false);
1768
1769 let mut fg = ProcessDefinition {
1771 generation_type,
1772 initial_pdgs: initial_pdgs.to_vec(),
1773 final_pdgs_lists: if final_sets_pdgs.is_empty() {
1774 vec![primary_final_pdgs.to_vec()]
1775 } else if !primary_final_pdgs.is_empty() {
1776 let mut sets = vec![primary_final_pdgs.to_vec()];
1777 sets.extend_from_slice(final_sets_pdgs);
1778 sets
1779 } else {
1780 final_sets_pdgs.to_vec()
1781 },
1782 loop_count_range: (1, 1), symmetrize_initial_states: sym_init,
1785 symmetrize_final_states: sym_final,
1786 symmetrize_left_right_states: sym_left_right,
1787 allow_symmetrization_of_external_fermions_in_amplitudes: allow_symferm,
1788 max_multiplicity_for_fast_cut_filter: a.max_multiplicity_for_fast_cut_filter,
1789 amplitude_filters: FeynGenFilters(vec![]),
1790 cross_section_filters: FeynGenFilters(vec![]),
1791 ..Default::default()
1792 };
1793
1794 let mut amp_filters: Vec<FeynGenFilter> = Vec::new();
1796 let mut xs_filters: Vec<FeynGenFilter> = Vec::new();
1797
1798 if let Some(n) = pert.loops_sum_amp_or_sum {
1800 amp_filters.push(FeynGenFilter::LoopCountRange((n as usize, n as usize)));
1801 if fg.generation_type == GenerationType::Amplitude {
1802 fg.loop_count_range = (n as usize, n as usize);
1803 }
1804 }
1805 if let Some(n) = pert.loops_forward_graph {
1806 xs_filters.push(FeynGenFilter::LoopCountRange((n as usize, n as usize)));
1807 if fg.generation_type == GenerationType::CrossSection {
1808 fg.loop_count_range = (n as usize, n as usize);
1809 }
1810 }
1811
1812 if !pert.orders.is_empty() {
1814 let map: HashMap<String, usize> = pert
1815 .orders
1816 .iter()
1817 .map(|(k, v)| (k.clone(), *v as usize))
1818 .collect();
1819 if fg.generation_type == GenerationType::Amplitude {
1820 amp_filters.push(FeynGenFilter::PerturbativeOrders(map));
1821 } else {
1822 xs_filters.push(FeynGenFilter::PerturbativeOrders(map));
1823 }
1824 }
1825
1826 if !amp_couplings.is_empty() {
1828 let cmap = coupling_orders_from_order_ranges_amp(amp_couplings);
1829 amp_filters.push(FeynGenFilter::CouplingOrders(cmap));
1830 }
1831 if !xs_couplings.is_empty() {
1832 let cmap = coupling_orders_from_order_ranges_xs(xs_couplings);
1833 xs_filters.push(FeynGenFilter::CouplingOrders(cmap));
1834 }
1835
1836 if let Some((n_min, n_max)) = number_of_factorized_loop_subtopologies {
1838 let filt = FeynGenFilter::FactorizedLoopTopologiesCountRange((n_min, n_max));
1839 if fg.generation_type == GenerationType::Amplitude {
1840 amp_filters.push(filt);
1841 } else {
1842 xs_filters.push(filt);
1843 }
1844 }
1845
1846 if let Some((n_min, n_max)) = number_of_fermion_loops {
1848 let filt = FeynGenFilter::FermionLoopCountRange((n_min, n_max));
1849 if fg.generation_type == GenerationType::Amplitude {
1850 amp_filters.push(filt);
1851 } else {
1852 xs_filters.push(filt);
1853 }
1854 }
1855
1856 if let Some(n) = max_n_bridges {
1858 let filt = FeynGenFilter::MaxNumberOfBridges(n);
1859 if fg.generation_type == GenerationType::Amplitude {
1860 amp_filters.push(filt);
1861 } else {
1862 xs_filters.push(filt);
1863 }
1864 }
1865
1866 if !veto_pdgs.is_empty() {
1868 let filt = FeynGenFilter::ParticleVeto(veto_pdgs.to_vec());
1869 if fg.generation_type == GenerationType::Amplitude {
1870 amp_filters.push(filt);
1871 } else {
1872 xs_filters.push(filt);
1873 }
1874 }
1875
1876 if fg.generation_type == GenerationType::CrossSection {
1878 xs_filters.push(FeynGenFilter::BlobRange(blob_range));
1879 xs_filters.push(FeynGenFilter::SpectatorRange(spectator_range));
1880 }
1881
1882 if filter_selfenergies {
1884 let mut se = SelfEnergyFilterOptions::default();
1885 if let Some(opt) = a.veto_self_energy_of_massive_lines {
1886 se.veto_self_energy_of_massive_lines = opt;
1887 }
1888 if let Some(opt) = a.veto_self_energy_of_massless_lines {
1889 se.veto_self_energy_of_massless_lines = opt;
1890 }
1891 if let Some(opt) = a.veto_only_scaleless_self_energy {
1892 se.veto_only_scaleless_self_energy = opt;
1893 }
1894 let filt = FeynGenFilter::SelfEnergyFilter(se);
1895 if fg.generation_type == GenerationType::Amplitude {
1896 amp_filters.push(filt);
1897 } else {
1898 xs_filters.push(filt);
1899 }
1900 }
1901 if filter_snails {
1902 let mut sn = SnailFilterOptions::default();
1903 if let Some(opt) = a.veto_snails_attached_to_massive_lines {
1904 sn.veto_snails_attached_to_massive_lines = opt;
1905 }
1906 if let Some(opt) = a.veto_snails_attached_to_massless_lines {
1907 sn.veto_snails_attached_to_massless_lines = opt;
1908 }
1909 if let Some(opt) = a.veto_only_scaleless_snails {
1910 sn.veto_only_scaleless_snails = opt;
1911 }
1912 let filt = FeynGenFilter::ZeroSnailsFilter(sn);
1913 if fg.generation_type == GenerationType::Amplitude {
1914 amp_filters.push(filt);
1915 } else {
1916 xs_filters.push(filt);
1917 }
1918 }
1919 if filter_tadpoles {
1920 let mut td = TadpolesFilterOptions::default();
1921 if let Some(opt) = a.veto_tadpoles_attached_to_massive_lines {
1922 td.veto_tadpoles_attached_to_massive_lines = opt;
1923 }
1924 if let Some(opt) = a.veto_tadpoles_attached_to_massless_lines {
1925 td.veto_tadpoles_attached_to_massless_lines = opt;
1926 }
1927 if let Some(opt) = a.veto_only_scaleless_tadpoles {
1928 td.veto_only_scaleless_tadpoles = opt;
1929 }
1930 let filt = FeynGenFilter::TadpolesFilter(td);
1931 if fg.generation_type == GenerationType::Amplitude {
1932 amp_filters.push(filt);
1933 } else {
1934 xs_filters.push(filt);
1935 }
1936 }
1937
1938 if fg.generation_type == GenerationType::CrossSection
1940 && a.filter_cross_section_tadpoles.unwrap_or(false)
1941 {
1942 if let Some(opt) = a.filter_cross_section_tadpoles {
1943 xs_filters.push(FeynGenFilter::SewedFilter(SewedFilterOptions {
1944 filter_tadpoles: opt,
1945 }));
1946 }
1947 }
1948
1949 if let Some(vertex_interactions_allowed) = a.allowed_vertex_interactions.as_ref() {
1950 if !vertex_interactions_allowed.is_empty() {
1951 let filt = FeynGenFilter::VertexAllow(vertex_interactions_allowed.clone());
1952 if fg.generation_type == GenerationType::Amplitude {
1953 amp_filters.push(filt);
1954 } else {
1955 xs_filters.push(filt);
1956 }
1957 }
1958 }
1959 if let Some(vertex_interactions_vetoed) = a.veto_vertex_interactions.as_ref() {
1960 let filt = FeynGenFilter::VertexVeto(vertex_interactions_vetoed.clone());
1961 if fg.generation_type == GenerationType::Amplitude {
1962 amp_filters.push(filt);
1963 } else {
1964 xs_filters.push(filt);
1965 }
1966 }
1967
1968 fg.amplitude_filters = FeynGenFilters(amp_filters);
1969 fg.cross_section_filters = FeynGenFilters(xs_filters);
1970
1971 fg
1972}
1973
1974fn coupling_orders_from_order_ranges_amp(
1975 orders: &BTreeMap<String, OrderRange>,
1976) -> HashMap<String, (usize, Option<usize>)> {
1977 let mut out = HashMap::default();
1978 for (name, r) in orders {
1979 if let Some(eq) = r.eq {
1980 out.insert(name.clone(), (eq as usize, Some(eq as usize)));
1981 } else {
1982 let minv = r.min.unwrap_or(0) as usize;
1983 let maxv = r.max.map(|m| m as usize);
1984 out.insert(name.clone(), (minv, maxv));
1985 }
1986 }
1987 out
1988}
1989
1990fn coupling_orders_from_order_ranges_xs(
1991 orders: &BTreeMap<CouplingKey, OrderRange>,
1992) -> HashMap<String, (usize, Option<usize>)> {
1993 let mut merged: BTreeMap<String, OrderRange> = BTreeMap::new();
1995 for (k, r) in orders {
1996 let e = merged.entry(k.name.clone()).or_default();
1997 if let Some(eq) = r.eq {
1998 e.eq = Some(eq);
1999 }
2000 if let Some(min) = r.min {
2001 e.min = Some(e.min.map_or(min, |m| m.max(min)));
2002 }
2003 if let Some(max) = r.max {
2004 e.max = Some(e.max.map_or(max, |m| m.min(max)));
2005 }
2006 }
2007 coupling_orders_from_order_ranges_amp(&merged)
2008}
2009
2010fn build_grouping_option(a: &SpecArgs) -> NumeratorAwareGraphGroupingOption {
2011 a.numerator_aware_isomorphism_grouping
2012 .unwrap_or(GroupingChoice::GroupIdenticalGraphsUpToScalarRescaling)
2013 .to_strategy(
2014 a.numerical_samples_seed,
2015 a.number_of_samples_for_numerator_comparisons,
2016 a.consider_internal_masses_only_in_numerator_isomorphisms,
2017 a.fully_numerical_substitution_when_comparing_numerators,
2018 a.compare_canonized_numerator,
2019 a.symmetric_left_right_polarizations,
2020 )
2021}
2022
2023fn all_particle_names(model: &Model) -> Vec<String> {
2026 model
2027 .particles
2028 .iter()
2029 .map(|p| p.name.clone().to_string())
2030 .collect::<Vec<_>>()
2031}
2032
2033fn resolve_pdgs(model: &Model, names: &[String]) -> Result<Vec<i64>, ParseError> {
2034 let all_particles = all_particle_names(model);
2035 let mut out = Vec::with_capacity(names.len());
2036 'outer: for n in names {
2037 if let Ok(p) = n.parse::<i64>() {
2039 out.push(p);
2040 continue;
2041 }
2042 if let Ok(p) = model.try_get_particle(n) {
2044 out.push(p.pdg_code as i64);
2045 continue;
2046 }
2047 for part in model.particles.iter() {
2049 if part.name.eq_ignore_ascii_case(n) || part.antiname.eq_ignore_ascii_case(n) {
2050 out.push(part.pdg_code as i64);
2051 continue 'outer;
2052 }
2053 }
2054 return Err(ParseError::UnknownParticle {
2056 name: n.clone(),
2057 choices: all_particles.join(", "),
2058 });
2059 }
2060 Ok(out)
2061}
2062
2063fn validate_coupling_names(
2064 model: &Model,
2065 amp: &BTreeMap<String, OrderRange>,
2066 xs: &BTreeMap<CouplingKey, OrderRange>,
2067 pert: &Perturbative,
2068) -> Result<(), ParseError> {
2069 let has_orders_info = !model.orders.is_empty();
2071
2072 if !has_orders_info {
2073 return Ok(());
2074 }
2075
2076 let mut names = BTreeSet::<String>::new();
2078 for k in amp.keys() {
2079 names.insert(k.clone());
2080 }
2081 for k in xs.keys() {
2082 names.insert(k.name.clone());
2083 }
2084 for k in pert.orders.keys() {
2085 names.insert(k.clone());
2086 }
2087
2088 if names.is_empty() {
2089 return Ok(());
2090 }
2091
2092 let known: Vec<String> = model
2094 .orders
2095 .iter()
2096 .map(|o| o.name.to_string())
2097 .collect::<Vec<_>>();
2098 let known_lower: BTreeSet<String> = known.iter().map(|s| s.to_ascii_lowercase()).collect();
2099
2100 for n in names {
2101 if !known_lower.contains(&n.to_ascii_lowercase()) {
2102 return Err(ParseError::UnknownCoupling {
2103 name: n,
2104 choices: known.join(", "),
2105 });
2106 }
2107 }
2108 Ok(())
2109}
2110
2111fn parse_csv_list(s: &str) -> Vec<String> {
2114 s.split(',')
2115 .flat_map(|x| x.split_whitespace())
2116 .filter(|t| !t.is_empty())
2117 .map(|t| t.to_string())
2118 .collect()
2119}
2120
2121#[allow(dead_code)]
2122fn parse_loop_momentum_bases(s: &str) -> Option<HashMap<String, Vec<String>>> {
2123 let mut out: HashMap<String, Vec<String>> = HashMap::default();
2125 for kv in s.split(';') {
2126 let kv = kv.trim();
2127 if kv.is_empty() {
2128 continue;
2129 }
2130 if let Some((k, v)) = kv.split_once('=') {
2131 let key = k.trim().to_string();
2132 let vals = v
2133 .split(',')
2134 .map(|x| x.trim().to_string())
2135 .filter(|x| !x.is_empty())
2136 .collect::<Vec<_>>();
2137 if !key.is_empty() {
2138 out.insert(key, vals);
2139 }
2140 } else {
2141 }
2143 }
2144 if out.is_empty() {
2145 None
2146 } else {
2147 Some(out)
2148 }
2149}
2150
2151#[cfg(test)]
2154mod tests {
2155 use super::*;
2156 use crate::{commands::Commands, Repl};
2157 use clap::Parser;
2158 use gammalooprs::utils::load_generic_model;
2159 use gammalooprs::{feyngen::GenerationType, initialisation::test_initialise};
2160 use std::time::Duration;
2161
2162 #[test]
2163 fn remove_compiled_cpp_sources_only_removes_sources_with_matching_libraries() -> Result<()> {
2164 let temp = tempfile::tempdir()?;
2165 let compiled_source = temp.path().join("integrand.cpp");
2166 let compiled_library = temp.path().join("integrand.so");
2167 let uncompiled_source = temp.path().join("orphan.cpp");
2168 std::fs::write(&compiled_source, "compiled source")?;
2169 std::fs::write(&compiled_library, "compiled library")?;
2170 std::fs::write(&uncompiled_source, "orphan source")?;
2171
2172 let removed_count = remove_compiled_cpp_sources(temp.path())?;
2173
2174 assert_eq!(removed_count, 1);
2175 assert!(!compiled_source.exists());
2176 assert!(compiled_library.is_file());
2177 assert!(uncompiled_source.is_file());
2178 Ok(())
2179 }
2180
2181 #[test]
2182 fn parse_generate_keep_sources_after_subcommand() {
2183 let repl =
2184 Repl::try_parse_from(["gammaloop", "generate", "existing", "--keep-sources"]).unwrap();
2185
2186 match repl.command {
2187 Commands::Generate(generate) => {
2188 assert!(generate.keep_sources);
2189 assert!(matches!(generate.mode, Some(GenerateCmd::Existing(_))));
2190 }
2191 other => panic!("Expected generate command, got {other:?}"),
2192 }
2193 }
2194
2195 #[test]
2196 fn generation_summary_includes_separator_and_total_row() {
2197 let reports = vec![
2198 GeneratedGraphReport {
2199 process_id: 0,
2200 integrand_name: "itg".to_string(),
2201 graph_name: "GL01".to_string(),
2202 stats: GraphGenerationStats {
2203 evaluator_count: 2,
2204 total_time: Duration::from_secs(4),
2205 evaluator_spenso_time: Duration::from_secs(1),
2206 evaluator_symbolica_time: Duration::from_secs(1),
2207 evaluator_compile_time: Duration::from_secs(1),
2208 },
2209 },
2210 GeneratedGraphReport {
2211 process_id: 0,
2212 integrand_name: "itg".to_string(),
2213 graph_name: "GL02".to_string(),
2214 stats: GraphGenerationStats {
2215 evaluator_count: 3,
2216 total_time: Duration::from_secs(6),
2217 evaluator_spenso_time: Duration::from_secs(2),
2218 evaluator_symbolica_time: Duration::ZERO,
2219 evaluator_compile_time: Duration::from_secs(3),
2220 },
2221 },
2222 ];
2223
2224 let summary = render_generation_summary(&reports, 0, Some(4), None).unwrap();
2225 let plain = Regex::new(r"\x1b\[[0-9;]*m")
2226 .unwrap()
2227 .replace_all(&summary, "")
2228 .into_owned();
2229
2230 assert!(plain.contains("Total"));
2231 assert!(plain.contains("5"));
2232 assert!(plain.contains("20%"));
2233 assert!(plain.contains("30%"));
2234 assert!(plain.contains("10%"));
2235 assert!(plain.contains("40%"));
2236
2237 let lines = plain.lines().collect::<Vec<_>>();
2238 let total_line_index = lines
2239 .iter()
2240 .position(|line| line.contains("Total"))
2241 .expect("missing Total row");
2242 let separator_line = lines[..total_line_index]
2243 .iter()
2244 .rev()
2245 .find(|line| !line.trim().is_empty())
2246 .expect("missing line before Total row");
2247 assert!(
2248 separator_line.chars().any(|ch| "┼╪╫┿├┤".contains(ch)),
2249 "missing horizontal separator before Total row: {separator_line}"
2250 );
2251 }
2252
2253 fn base_args(tokens: &str) -> SpecArgs {
2254 SpecArgs {
2255 tokens: tokens.split_whitespace().map(|x| x.to_string()).collect(),
2256 clear_existing_processes: false,
2257 filter_selfenergies: None,
2258 filter_snails: None,
2259 filter_tadpoles: None,
2260 filter_cross_section_tadpoles: None,
2261 veto_tadpoles_attached_to_massive_lines: None,
2262 veto_tadpoles_attached_to_massless_lines: None,
2263 veto_only_scaleless_tadpoles: None,
2264 veto_snails_attached_to_massive_lines: None,
2265 veto_snails_attached_to_massless_lines: None,
2266 veto_only_scaleless_snails: None,
2267 veto_self_energy_of_massive_lines: None,
2268 veto_self_energy_of_massless_lines: None,
2269 veto_only_scaleless_self_energy: None,
2270 max_n_bridges: None,
2271 number_of_factorized_loop_subtopologies: None,
2272 number_of_fermion_loops: None,
2273 symmetric_left_right_polarizations: None,
2274 n_cut_blobs: None,
2275 n_cut_spectators: None,
2276 allow_symmetrization_of_external_fermions_in_amplitudes: None,
2277 symmetrize_initial_states: None,
2278 symmetrize_final_states: None,
2279 symmetrize_left_right_states: None,
2280 numerator_aware_isomorphism_grouping: None,
2281 numerical_samples_seed: None,
2282 number_of_samples_for_numerator_comparisons: None,
2283 consider_internal_masses_only_in_numerator_isomorphisms: None,
2284 fully_numerical_substitution_when_comparing_numerators: None,
2285 compare_canonized_numerator: None,
2286 loop_momentum_bases: None,
2287 select_graphs: None,
2288 veto_graphs: None,
2289 graph_prefix: None,
2290 max_multiplicity_for_fast_cut_filter: 6,
2291 filter_self_loop: None,
2292 filter_zero_flow_edges: None,
2293 process_name: None,
2294 append: false,
2295 integrand_name: None,
2296 only_diagrams: true,
2297 allowed_vertex_interactions: None,
2298 veto_vertex_interactions: None,
2299 global_prefactor_num: None,
2300 global_prefactor_projector: None,
2301 }
2302 }
2303
2304 fn parse_ok_amp(s: &str) -> ProcessSpec {
2306 let model = &load_generic_model("sm");
2307 let a = base_args(s);
2308 parse_spec_with_model(&a, GenerationType::Amplitude, model).unwrap()
2309 }
2310
2311 fn parse_ok_xs(s: &str) -> ProcessSpec {
2312 let model = &load_generic_model("sm");
2313 let a = base_args(s);
2314 parse_spec_with_model(&a, GenerationType::CrossSection, model).unwrap()
2315 }
2316
2317 #[test]
2318 fn allowed_vertex_interactions_are_added_to_generation_filters() {
2319 let model = &load_generic_model("sm");
2320 let mut args = base_args("e+ e- > d d~");
2321 args.allowed_vertex_interactions = Some(vec!["V_6".to_string(), "V_9".to_string()]);
2322
2323 let spec = parse_spec_with_model(&args, GenerationType::Amplitude, model).unwrap();
2324
2325 assert!(spec
2326 .process_definition
2327 .amplitude_filters
2328 .0
2329 .iter()
2330 .any(|filter| matches!(
2331 filter,
2332 FeynGenFilter::VertexAllow(vertex_names)
2333 if vertex_names == &vec!["V_6".to_string(), "V_9".to_string()]
2334 )));
2335 }
2336
2337 #[test]
2338 fn basic_process_list_arrow() {
2339 test_initialise().unwrap();
2340 let ps = parse_ok_amp("e+ e- > d d~ g");
2341 assert_eq!(ps.initial, vec!["e+", "e-"]);
2342 assert_eq!(ps.final_, vec!["d", "d~", "g"]);
2343 assert!(!ps.empty_initial && !ps.empty_final);
2344 }
2345
2346 #[test]
2347 fn basic_process_list_to() {
2348 test_initialise().unwrap();
2349 let ps = parse_ok_amp("e+ e- to d d~ g");
2350 assert_eq!(ps.initial, vec!["e+", "e-"]);
2351 assert_eq!(ps.final_, vec!["d", "d~", "g"]);
2352 }
2353
2354 #[test]
2355 fn empty_sets_allowed() {
2356 test_initialise().unwrap();
2357 let ps = parse_ok_xs("{} to {}");
2358 assert!(ps.empty_initial);
2359 assert!(ps.empty_final);
2360 }
2361
2362 #[test]
2363 fn alternatives_in_final_states() {
2364 test_initialise().unwrap();
2365 let ps = parse_ok_xs("e+ e- > { Z Z, a a }");
2366 assert!(ps.final_.is_empty());
2367 assert_eq!(ps.final_sets.len(), 2);
2368 assert_eq!(ps.final_sets[0], vec!["Z", "Z"]);
2369 assert_eq!(ps.final_sets[1], vec!["a", "a"]);
2370 }
2371
2372 #[test]
2373 fn veto_and_only() {
2374 test_initialise().unwrap();
2375 let ps = parse_ok_xs("e+ e- > mu+ mu- / u d c g ghG e- | u g ghG");
2376 assert!(ps.veto.contains("u"));
2377 assert!(ps.veto.contains("e-"));
2378 let sel = ps.only.as_ref().unwrap();
2379 assert!(sel.contains("u"));
2380 assert!(sel.contains("g"));
2381 assert!(sel.contains("ghG"));
2382 }
2383
2384 #[test]
2385 fn amp_order_constraints() {
2386 test_initialise().unwrap();
2387 let ps = parse_ok_amp("e+ e- > mu+ mu- QED==2 QCD>=2 QCD<=4");
2388 let qcd = ps.amp_couplings.get("QCD").unwrap();
2389 assert_eq!(qcd.min, Some(2));
2390 assert_eq!(qcd.max, Some(4));
2391 let qed = ps.amp_couplings.get("QED").unwrap();
2392 assert_eq!(qed.eq, Some(2));
2393 }
2394
2395 #[test]
2396 fn xs_order_constraints_powered() {
2397 test_initialise().unwrap();
2398 let ps = parse_ok_xs("e+ e- > mu+ mu- QED^2==2 QCD^2>=2 QCD^2<=4");
2399 let key = CouplingKey {
2400 name: "QCD".into(),
2401 power: 2,
2402 };
2403 let qcd2 = ps.xs_couplings.get(&key).unwrap();
2404 assert_eq!(qcd2.min, Some(2));
2405 assert_eq!(qcd2.max, Some(4));
2406 let keyq = CouplingKey {
2407 name: "QED".into(),
2408 power: 2,
2409 };
2410 let qed2 = ps.xs_couplings.get(&keyq).unwrap();
2411 assert_eq!(qed2.eq, Some(2));
2412 }
2413
2414 #[test]
2415 fn perturbative_block_all_forms() {
2416 test_initialise().unwrap();
2417 let ps = parse_ok_xs("e+ e- > z [ {1} {{2}} QCD=2 QED=1 ]");
2418 assert_eq!(ps.pert.loops_sum_amp_or_sum, Some(1));
2419 assert_eq!(ps.pert.loops_forward_graph, Some(2));
2420 assert_eq!(ps.pert.orders.get("QCD"), Some(&2));
2421 assert_eq!(ps.pert.orders.get("QED"), Some(&1));
2422 }
2423
2424 #[test]
2425 fn perturbative_block_shorthand() {
2426 test_initialise().unwrap();
2427 let ps = parse_ok_xs("e+ e- > z [ QCD ]");
2428 assert_eq!(ps.pert.orders.get("QCD"), Some(&1));
2429 }
2430
2431 #[test]
2432 fn amplitude_generation_type_is_set() {
2433 test_initialise().unwrap();
2434 let ps = parse_ok_amp("e+ e- > mu+ mu-");
2435 assert_eq!(
2436 ps.process_definition.generation_type,
2437 GenerationType::Amplitude
2438 );
2439 }
2440
2441 #[test]
2442 fn rejects_malformed_tokens() {
2443 test_initialise().unwrap();
2444 let model = &load_generic_model("sm");
2445
2446 let args = base_args("e+ > z [ {{}} ]");
2447 let err = parse_spec_with_model(&args, GenerationType::CrossSection, model).unwrap_err();
2448 assert_eq!(err, ParseError::InvalidToken("{{}}".into()));
2449 }
2450
2451 #[test]
2452 fn missing_arrow_detected() {
2453 test_initialise().unwrap();
2454 let model = &load_generic_model("sm");
2455
2456 let args = base_args("e+ e- z z");
2457 let err = parse_spec_with_model(&args, GenerationType::CrossSection, model).unwrap_err();
2458 assert!(matches!(err, ParseError::MissingArrow));
2459 }
2460
2461 #[test]
2462 fn vacuum_defaults_and_filters_xs() {
2463 test_initialise().unwrap();
2464 let ps = parse_ok_xs("{} to {}");
2465 assert_eq!(
2466 ps.process_definition.generation_type,
2467 GenerationType::CrossSection
2468 );
2469
2470 let xs_filters = &ps.process_definition.cross_section_filters.0;
2471
2472 assert!(xs_filters
2474 .iter()
2475 .any(|f| matches!(f, FeynGenFilter::MaxNumberOfBridges(0))));
2476 assert!(xs_filters
2477 .iter()
2478 .any(|f| matches!(f, FeynGenFilter::FactorizedLoopTopologiesCountRange((1, 1)))));
2479
2480 assert!(xs_filters
2482 .iter()
2483 .any(|f| matches!(f, FeynGenFilter::BlobRange(r) if r.clone()==(1..=1))));
2484 assert!(xs_filters
2485 .iter()
2486 .any(|f| matches!(f, FeynGenFilter::SpectatorRange(r) if r.clone()==(0..=0))));
2487 }
2488
2489 #[test]
2490 fn particle_veto_resolution_cross_section() {
2491 use std::collections::BTreeSet;
2492 test_initialise().unwrap();
2493 let ps = parse_ok_xs("e+ e- > mu+ mu- / u d u~ e+");
2495 let xs_filters = &ps.process_definition.cross_section_filters.0;
2496
2497 let veto_pdgs = xs_filters
2498 .iter()
2499 .find_map(|f| match f {
2500 FeynGenFilter::ParticleVeto(v) => Some(v.clone()),
2501 _ => None,
2502 })
2503 .expect("ParticleVeto filter not found");
2504 let set: BTreeSet<i64> = veto_pdgs.into_iter().collect();
2505
2506 for pdg in [2_i64, 1_i64, -2_i64, -11_i64] {
2508 assert!(set.contains(&pdg), "missing PDG {pdg} in veto");
2509 }
2510 }
2511
2512 #[test]
2513 fn particle_inclusion_resolution_cross_section() {
2514 use std::collections::BTreeSet;
2515 test_initialise().unwrap();
2516 let ps = parse_ok_xs("e+ e- > mu+ mu- | u d u~ e+");
2518 let xs_filters = &ps.process_definition.cross_section_filters.0;
2519
2520 let veto_pdgs = xs_filters
2521 .iter()
2522 .find_map(|f| match f {
2523 FeynGenFilter::ParticleVeto(v) => Some(v.clone()),
2524 _ => None,
2525 })
2526 .expect("ParticleVeto filter not found");
2527 let set: BTreeSet<i64> = veto_pdgs.into_iter().collect();
2528
2529 for pdg in [
2530 3, 4, 5, 6, 12, 13, 14, 15, 16, 21, 22, 23, 24, 25, 250, 251, 9000001, 9000002,
2531 9000003, 9000004, 9000005,
2532 ] {
2533 assert!(set.contains(&pdg), "missing PDG {pdg} in veto");
2534 }
2535 }
2536
2537 #[test]
2538 fn final_sets_and_orders_and_loops_cross_section() {
2539 test_initialise().unwrap();
2540 let ps = parse_ok_xs("e+ e- > { Z Z, a a, H H } [ {{3}} QCD=2 QED=1 ]");
2542
2543 assert_eq!(ps.final_sets.len(), 3);
2545 assert_eq!(
2546 ps.process_definition.generation_type,
2547 GenerationType::CrossSection
2548 );
2549
2550 assert_eq!(ps.process_definition.loop_count_range, (3, 3));
2552
2553 let xs_filters = &ps.process_definition.cross_section_filters.0;
2555 assert!(xs_filters.iter().any(|f| {
2556 if let FeynGenFilter::PerturbativeOrders(m) = f {
2557 m.get("QCD") == Some(&2usize) && m.get("QED") == Some(&1usize)
2558 } else {
2559 false
2560 }
2561 }));
2562 }
2563
2564 #[test]
2565 fn amplitude_with_powered_xs_constraints_and_only() {
2566 test_initialise().unwrap();
2567 let ps = parse_ok_amp("e+ e- > mu+ mu- | g ghG QED^2==2 QCD^2>=2 QCD^2<=4 [ {1} QCD ]");
2569
2570 assert_eq!(ps.pert.loops_sum_amp_or_sum, Some(1));
2572 assert_eq!(ps.pert.orders.get("QCD"), Some(&1));
2573
2574 let key_qed2 = CouplingKey {
2576 name: "QED".into(),
2577 power: 2,
2578 };
2579 let key_qcd2 = CouplingKey {
2580 name: "QCD".into(),
2581 power: 2,
2582 };
2583 let qed2 = ps.xs_couplings.get(&key_qed2).unwrap();
2584 assert_eq!(qed2.eq, Some(2));
2585 let qcd2 = ps.xs_couplings.get(&key_qcd2).unwrap();
2586 assert_eq!(qcd2.min, Some(2));
2587 assert_eq!(qcd2.max, Some(4));
2588
2589 let only = ps.only.as_ref().expect("missing only-set");
2591 assert!(only.contains("g"));
2592 assert!(only.contains("ghG"));
2593 }
2594
2595 #[test]
2596 fn amplitude_case_insensitive_and_anti_in_veto() {
2597 use std::collections::BTreeSet;
2598 test_initialise().unwrap();
2599 let ps = parse_ok_amp("E+ e- to Z Z / u~ d~ A");
2601
2602 let amp_filters = &ps.process_definition.amplitude_filters.0;
2603 let veto_pdgs = amp_filters
2604 .iter()
2605 .find_map(|f| match f {
2606 FeynGenFilter::ParticleVeto(v) => Some(v.clone()),
2607 _ => None,
2608 })
2609 .expect("AMP ParticleVeto filter not found");
2610
2611 let set: BTreeSet<i64> = veto_pdgs.into_iter().collect();
2612 for pdg in [-2_i64, -1_i64, 22_i64] {
2614 assert!(set.contains(&pdg), "missing PDG {pdg} in AMP veto");
2615 }
2616 }
2617
2618 #[test]
2619 fn shell_name_long_with_sets_veto_orders_pert_and_xs() {
2620 test_initialise().unwrap();
2621 let model = &load_generic_model("sm");
2622
2623 let args =
2625 base_args("e+ e- > { Z Z, a a } / u d QED==2 QCD>=1 QED^2<=4 QCD^2>=2 [ QED=1 QCD=2 ]");
2626 let ps = parse_spec_with_model(&args, GenerationType::Amplitude, model).unwrap();
2627
2628 let long = ps.process_shell_name(false);
2629
2630 let expected = "epem_zz_or_aa-\
2631 no__d_u-\
2632 QCD_ge_1__QED_eq_2-\
2633 QCDsq_ge_2__QEDsq_le_4-\
2634 QCDloop_eq_2__QEDloop_eq_1";
2635
2636 assert_eq!(long, expected);
2637 }
2638
2639 #[test]
2640 fn repr_str_canonical_with_amp_xs_veto_loops_orders() {
2641 test_initialise().unwrap();
2642 let model = &load_generic_model("sm");
2643
2644 let args = base_args(
2646 "mu+ mu- > g g / ghg gha QED==1 QCD<=3 [ {2} QED=0 QCD=1 ] QED^2==0 QCD^2>=2",
2647 );
2648 let ps = parse_spec_with_model(&args, GenerationType::Amplitude, model).unwrap();
2649
2650 let r = ps.repr_str();
2651
2652 let expected = "mu+ mu- > g g / gha ghg QCD<=3 QED==1 [ {2} QCD ] QCD^2>=2 QED^2==0";
2653
2654 assert_eq!(r, expected);
2655 }
2656
2657 #[test]
2658 fn repr_str_with_both_loop_counts_and_sorted_orders() {
2659 test_initialise().unwrap();
2660 let model = &load_generic_model("sm");
2661
2662 let args = base_args("e+ e- > z z [ {{2}} QED=2 QCD=1 {1} ]");
2663 let ps = parse_spec_with_model(&args, GenerationType::CrossSection, model).unwrap();
2664
2665 let r = ps.repr_str();
2666
2667 let expected = "e+ e- > z z [ {1} {{2}} QCD QED=2 ]";
2668
2669 assert_eq!(r, expected);
2670 }
2671
2672 #[test]
2673 fn shell_name_short_canonical_and_sanitization() {
2674 test_initialise().unwrap();
2675 let model = &load_generic_model("sm");
2676
2677 let args = base_args("W+ W- > {}");
2678 let ps = parse_spec_with_model(&args, GenerationType::CrossSection, model).unwrap();
2679
2680 let short = ps.process_shell_name(true);
2682 let expected = "wpwm_empty";
2683
2684 assert_eq!(short, expected);
2685 }
2686}