1#[cfg(test)]
2use gammalooprs::integrands::process::EvaluationTarget;
3use std::{
4 collections::{BTreeMap, BTreeSet, HashSet},
5 fs::{self},
6 io::{self, IsTerminal},
7 ops::ControlFlow,
8 path::{Path, PathBuf},
9 str::FromStr,
10 sync::{
11 atomic::{AtomicBool, AtomicU64, Ordering},
12 Arc, Mutex,
13 },
14 thread,
15 time::Duration,
16};
17
18use clap::Args;
19use color_eyre::{Result, Section};
20use colored::Colorize;
21use eyre::{eyre, Context};
22use gammalooprs::{
23 processes::{Amplitude, CrossSection},
24 utils::serde_utils::IsDefault,
25};
26use linnet::half_edge::subgraph::{SuBitGraph, SubGraphLike, SubSetLike};
27use schemars::{schema_for, JsonSchema, Schema};
28use serde::{Deserialize, Serialize};
29use spenso::algebra::complex::Complex;
30#[cfg(not(target_os = "linux"))]
31use sysinfo::{get_current_pid, ProcessRefreshKind, ProcessesToUpdate, System};
32use toml::Value as TomlValue;
33use tracing::{debug, info, info_span, Span};
34use tracing_indicatif::span_ext::IndicatifSpanExt;
35
36use gammalooprs::{
37 clear_interrupt_request,
38 feyngen::GenerationType,
39 graph::Graph,
40 initialisation::initialise,
41 integrands::process::ProcessIntegrand,
42 is_interrupt_requested,
43 model::{InputParamCard, Model, SerializableInputParamCard, UFOSymbol},
44 processes::{
45 begin_phase, merge_generated_graph_reports, DotExportSettings, GeneratedGraphReport,
46 GenerationProcessKind, GenerationProgressMode, GenerationProgressModeGuard,
47 GenerationProgressObserver, GenerationProgressObserverGuard, GenerationProgressPhase,
48 GraphGenerationStats, GraphGroupSelectionMode, GraphGroupSelectionPlan,
49 GraphGroupSelectionReport, GraphGroupSelectionSpec, NamedGraphGenerationReport, Process,
50 ProcessCollection, ProcessDefinition, ProcessList, ProcessLoadSelection,
51 },
52 settings::{
53 global::GenerationSettings, runtime::LockedRuntimeSettings, GlobalSettings, RuntimeSettings,
54 },
55 utils::{
56 serde_utils::{get_schema_folder, SmartSerde},
57 tracing::{init_bench_tracing, init_test_tracing},
58 F,
59 },
60 GammaLoopContextContainer,
61};
62
63use crate::{
64 command_parser::{normalize_clap_args, split_command_line},
65 command_template::{contains_placeholder, placeholder_specs, PlaceholderSpec},
66 commands::{save::SaveState, Commands},
67 integrand_info::{collect_integrand_info, IntegrandInfo},
68 model_parameters::{external_model_parameter_type, validate_model_parameter_type},
69 render_smart_toml,
70 tracing::{set_file_log_filter, set_log_style, set_stderr_log_filter},
71 CLISettings,
72};
73
74#[derive(Debug, Clone, Copy, Default)]
75pub struct GenerationResourceSummary {
76 pub peak_ram_bytes: u64,
77 pub generation_cores: usize,
78}
79
80#[derive(Debug, Clone, Default)]
81pub struct GenerationReports {
82 pub reports: Vec<GeneratedGraphReport>,
83 pub resources: GenerationResourceSummary,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
87pub struct IntegrandGenerationSummaryKey {
88 pub process_id: usize,
89 pub integrand_name: String,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct IntegrandGenerationSummary {
94 pub peak_ram_bytes: u64,
95 pub reports: Vec<GeneratedGraphReport>,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct SelectedGraphGroups {
100 pub source_process_id: usize,
101 pub source_process_name: String,
102 pub source_integrand_name: String,
103 pub process_id: usize,
104 pub process_name: String,
105 pub integrand_name: String,
106 pub report: GraphGroupSelectionReport,
107 pub copied_to_output: bool,
108 pub replaced_existing_target: bool,
109 pub removed_target_artifacts: bool,
110 pub discarded_generated_integrand: bool,
111 pub removed_generated_artifacts: bool,
112 pub removed_generation_summary: bool,
113}
114
115#[derive(Debug, Clone, Default, PartialEq, Eq)]
116pub struct GraphGroupSelectionTarget {
117 pub output_process_name: Option<String>,
118 pub output_integrand_name: Option<String>,
119 pub clear_existing_processes: bool,
120}
121
122impl GraphGroupSelectionTarget {
123 pub fn in_place() -> Self {
124 Self::default()
125 }
126
127 pub fn copy(
128 output_process_name: Option<String>,
129 output_integrand_name: Option<String>,
130 clear_existing_processes: bool,
131 ) -> Self {
132 Self {
133 output_process_name,
134 output_integrand_name,
135 clear_existing_processes,
136 }
137 }
138
139 fn is_copy_mode(&self) -> bool {
140 self.output_process_name.is_some() || self.output_integrand_name.is_some()
141 }
142}
143
144#[derive(Debug, Clone, Copy)]
145pub struct GraphGroupSelectionContext<'a> {
146 pub generation_settings: &'a GenerationSettings,
147 pub state_folder: &'a Path,
148 pub read_only_state: bool,
149}
150
151impl<'a> GraphGroupSelectionContext<'a> {
152 pub fn new(
153 generation_settings: &'a GenerationSettings,
154 state_folder: &'a Path,
155 read_only_state: bool,
156 ) -> Self {
157 Self {
158 generation_settings,
159 state_folder,
160 read_only_state,
161 }
162 }
163}
164
165struct IntegrandCopyInsertion {
166 process_id: usize,
167 process_name: String,
168 integrand_name: String,
169 replaced_existing_target: bool,
170 removed_target_artifacts: bool,
171}
172
173struct SelectionSource {
174 process_id: usize,
175 process_name: String,
176 integrand_name: String,
177}
178
179enum IntegrandCopyPayload {
180 Amplitude(Amplitude),
181 CrossSection(CrossSection),
182}
183
184impl IntegrandCopyPayload {
185 fn rename(&mut self, new_name: &str) {
186 match self {
187 Self::Amplitude(amplitude) => {
188 amplitude.name = new_name.to_string();
189 rename_process_integrand(amplitude.integrand.as_mut(), new_name);
190 }
191 Self::CrossSection(cross_section) => {
192 cross_section.name = new_name.to_string();
193 rename_process_integrand(cross_section.integrand.as_mut(), new_name);
194 }
195 }
196 }
197
198 fn apply_graph_group_selection(&mut self, plan: &GraphGroupSelectionPlan) -> Result<()> {
199 match self {
200 Self::Amplitude(amplitude) => amplitude.apply_graph_group_selection(plan),
201 Self::CrossSection(cross_section) => cross_section.apply_graph_group_selection(plan),
202 }
203 }
204
205 fn is_compatible_with(&self, collection: &ProcessCollection) -> bool {
206 matches!(
207 (self, collection),
208 (Self::Amplitude(_), ProcessCollection::Amplitudes(_))
209 | (Self::CrossSection(_), ProcessCollection::CrossSections(_))
210 )
211 }
212
213 fn kind_name(&self) -> &'static str {
214 match self {
215 Self::Amplitude(_) => "amplitudes",
216 Self::CrossSection(_) => "cross sections",
217 }
218 }
219
220 fn into_collection(self) -> ProcessCollection {
221 match self {
222 Self::Amplitude(amplitude) => {
223 let mut collection = ProcessCollection::Amplitudes(BTreeMap::new());
224 collection.add_amplitude(amplitude);
225 collection
226 }
227 Self::CrossSection(cross_section) => {
228 let mut collection = ProcessCollection::CrossSections(BTreeMap::new());
229 collection.add_cross_section(cross_section);
230 collection
231 }
232 }
233 }
234
235 fn insert_into_process(self, process: &mut Process, process_name: &str) -> Result<()> {
236 match (self, &mut process.collection) {
237 (Self::Amplitude(amplitude), ProcessCollection::Amplitudes(amplitudes)) => {
238 amplitudes.insert(amplitude.name.clone(), amplitude);
239 Ok(())
240 }
241 (
242 Self::CrossSection(cross_section),
243 ProcessCollection::CrossSections(cross_sections),
244 ) => {
245 cross_sections.insert(cross_section.name.clone(), cross_section);
246 Ok(())
247 }
248 (payload, _) => Err(eyre!(
249 "Destination process '{}' exists but does not contain {}",
250 process_name,
251 payload.kind_name()
252 )),
253 }
254 }
255}
256
257struct GenerationMonitor {
258 peak_ram_bytes: Arc<AtomicU64>,
259 current_ram_bytes: Arc<AtomicU64>,
260 stop_requested: Arc<AtomicBool>,
261 handle: Option<thread::JoinHandle<()>>,
262}
263
264struct AggregateGenerationProgressReporter {
265 progress_span: Span,
266 current_ram_bytes: Arc<AtomicU64>,
267 peak_ram_bytes: Arc<AtomicU64>,
268 state: Mutex<AggregateGenerationProgressState>,
269}
270
271#[derive(Default)]
272struct AggregateGenerationProgressState {
273 phase: Option<GenerationProgressPhase>,
274 kind: Option<GenerationProcessKind>,
275 process: String,
276 integrand: String,
277 total_graphs: usize,
278 done_graphs: usize,
279 total_cuts: Option<usize>,
280 done_cuts: usize,
281 discovered_st_cuts: usize,
282 discovered_valid_cuts: usize,
283 active_graphs: BTreeSet<String>,
284 last_graph: Option<String>,
285 stats: GraphGenerationStats,
286}
287
288impl AggregateGenerationProgressReporter {
289 fn new(
290 current_ram_bytes: Arc<AtomicU64>,
291 peak_ram_bytes: Arc<AtomicU64>,
292 eta_warmup_steps: u64,
293 ) -> Arc<Self> {
294 let span = info_span!(
295 "Integrand generation",
296 indicatif.pb_show = true,
297 indicatif.pb_msg = "Starting integrand generation"
298 );
299 span.pb_set_style(
300 &gammalooprs::utils::long_running_progress_style_with_eta_warmup(eta_warmup_steps),
301 );
302 span.pb_start();
303 span.pb_set_length(0);
304 span.pb_set_position(0);
305 span.pb_tick();
306 Self::new_with_span(current_ram_bytes, peak_ram_bytes, span)
307 }
308
309 #[cfg(test)]
310 fn new_hidden(current_ram_bytes: Arc<AtomicU64>, peak_ram_bytes: Arc<AtomicU64>) -> Arc<Self> {
311 Self::new_with_span(current_ram_bytes, peak_ram_bytes, Span::none())
312 }
313
314 fn new_with_span(
315 current_ram_bytes: Arc<AtomicU64>,
316 peak_ram_bytes: Arc<AtomicU64>,
317 progress_span: Span,
318 ) -> Arc<Self> {
319 Arc::new(Self {
320 progress_span,
321 current_ram_bytes,
322 peak_ram_bytes,
323 state: Mutex::new(AggregateGenerationProgressState::default()),
324 })
325 }
326
327 fn fixed_field(value: &str, width: usize) -> String {
328 let mut chars = value.chars().collect::<Vec<_>>();
329 if chars.len() > width {
330 chars.truncate(width.saturating_sub(1));
331 chars.push('~');
332 }
333 format!("{:<width$}", chars.into_iter().collect::<String>())
334 }
335
336 fn progress_memory(bytes: u64) -> String {
337 const MIB: f64 = 1024.0 * 1024.0;
338 const GIB: f64 = MIB * 1024.0;
339 let value = bytes as f64;
340 if value >= GIB {
341 format!("{:>6.2} GiB", value / GIB)
342 } else {
343 format!("{:>6.0} MiB", value / MIB)
344 }
345 }
346
347 fn progress_percent(percent: f64) -> String {
348 if percent < 0.01 {
349 "0%".to_string()
350 } else if percent < 0.1 {
351 format!("{percent:.3}%")
352 } else if percent < 1.0 {
353 format!("{percent:.2}%")
354 } else if percent < 10.0 {
355 format!("{percent:.1}%")
356 } else {
357 format!("{percent:.0}%")
358 }
359 }
360
361 fn progress_time_share(duration: Duration, total: Duration) -> String {
362 if total.is_zero() {
363 "--".to_string()
364 } else {
365 Self::progress_percent(duration.as_secs_f64() * 100.0 / total.as_secs_f64())
366 }
367 }
368
369 fn progress_units(state: &AggregateGenerationProgressState) -> (u64, u64) {
370 match (state.phase, state.total_cuts) {
371 (Some(GenerationProgressPhase::GraphGeneration), Some(total_cuts)) => {
372 let total = total_cuts.saturating_add(state.total_graphs) as u64;
373 let completed = state
374 .done_cuts
375 .saturating_add(state.done_graphs)
376 .min(total_cuts.saturating_add(state.total_graphs))
377 as u64;
378 (completed, total)
379 }
380 _ => (state.done_graphs as u64, state.total_graphs as u64),
381 }
382 }
383
384 fn graph_progress_counts(state: &AggregateGenerationProgressState) -> (usize, usize, usize) {
385 let active = state
386 .active_graphs
387 .len()
388 .min(state.total_graphs.saturating_sub(state.done_graphs));
389 (state.done_graphs, active, state.total_graphs)
390 }
391
392 fn refresh(&self, state: &AggregateGenerationProgressState) {
393 let (completed_units, total_units) = Self::progress_units(state);
394 self.progress_span.pb_set_length(total_units);
395 self.progress_span.pb_set_position(completed_units);
396
397 let current_ram = Self::progress_memory(self.current_ram_bytes.load(Ordering::Relaxed));
398 let peak_ram = Self::progress_memory(self.peak_ram_bytes.load(Ordering::Relaxed));
399 let kind = match state.kind {
400 Some(GenerationProcessKind::Amplitude) => "AMP",
401 Some(GenerationProcessKind::CrossSection) => "XS",
402 None => "GEN",
403 };
404 let phase = match state.phase {
405 Some(GenerationProgressPhase::GraphPreprocessing) => "prep",
406 Some(GenerationProgressPhase::GraphGeneration) => "graphs",
407 Some(GenerationProgressPhase::Backend) => "backend",
408 None => "gen",
409 };
410 let last_graph = state.last_graph.as_deref().unwrap_or("-");
411 let cut_context = match (state.phase, state.kind, state.total_cuts) {
412 (
413 Some(GenerationProgressPhase::GraphPreprocessing),
414 Some(GenerationProcessKind::Amplitude),
415 _,
416 ) => None,
417 (Some(GenerationProgressPhase::GraphPreprocessing), _, _) => Some(format!(
418 "{:>4}/{:<4}",
419 state.discovered_valid_cuts, state.discovered_st_cuts
420 )),
421 (_, _, Some(total_cuts)) => Some(format!("{:>4}/{:<4}", state.done_cuts, total_cuts)),
422 _ => Some(format!("{:>4}/{:<4}", "-", "-")),
423 };
424 let phase = Self::fixed_field(phase, 7).bold().blue();
425 let kind = Self::fixed_field(kind, 3).cyan();
426 let identifier = if state.process.is_empty() {
427 state.integrand.clone()
428 } else {
429 format!("{}@{}", state.integrand, state.process)
430 };
431 let identifier = Self::fixed_field(&identifier, 24).yellow();
432 let last_graph = Self::fixed_field(last_graph, 8).green();
433 let (done_graphs, active_graphs, total_graphs) = Self::graph_progress_counts(state);
434 let graph_ratio = format!(
435 "{} {} / {}",
436 format!("{done_graphs:>3}").green(),
437 format!("({active_graphs:>3})").dimmed(),
438 format!("{total_graphs:<3}").green(),
439 );
440 let ram = format!("{current_ram}/{peak_ram}").yellow();
441 let phase_detail = if state.phase == Some(GenerationProgressPhase::GraphPreprocessing) {
442 let steps = match state.kind {
443 Some(GenerationProcessKind::Amplitude) => {
444 "CFFs + LMBs + integrands + threshold CTs + tropical samplers"
445 }
446 _ => "cuts + CFFs + LMBs + integrands + threshold CTs",
447 };
448 format!("{} {}", "step".bold().blue(), steps.cyan())
449 } else {
450 let total_time = state.stats.total_time;
451 let expression_share =
452 Self::progress_time_share(state.stats.expression_build_time(), total_time);
453 let spenso_share =
454 Self::progress_time_share(state.stats.evaluator_spenso_time, total_time);
455 let symbolica_share =
456 Self::progress_time_share(state.stats.evaluator_symbolica_time, total_time);
457 let compile_share =
458 Self::progress_time_share(state.stats.evaluator_compile_time, total_time);
459 format!(
460 "{} {} {} / {} {} / {} {} / {} {}",
461 "time".bold().blue(),
462 "expr".bold().blue(),
463 expression_share.magenta(),
464 "spenso".bold().blue(),
465 spenso_share.cyan(),
466 "eval".bold().blue(),
467 symbolica_share.green(),
468 "compile".bold().blue(),
469 compile_share.yellow(),
470 )
471 };
472 let mut sections = vec![
473 format!("{} {} {}", phase, kind, identifier),
474 format!("{} {}", "g".bold().blue(), graph_ratio),
475 format!("{} {}", "last".bold().blue(), last_graph),
476 ];
477 if let Some(cut_context) = cut_context {
478 sections.push(format!("{} {}", "cut".bold().blue(), cut_context.green()));
479 }
480 sections.push(format!("{} {}", "ram".bold().blue(), ram));
481 sections.push(phase_detail);
482 self.progress_span.pb_set_message(§ions.join(" | "));
483 self.progress_span.pb_tick();
484 }
485}
486
487impl GenerationProgressObserver for AggregateGenerationProgressReporter {
488 fn begin_phase(
489 &self,
490 phase: GenerationProgressPhase,
491 kind: GenerationProcessKind,
492 process: &str,
493 integrand: &str,
494 total_graphs: usize,
495 total_cuts: Option<usize>,
496 ) {
497 let mut state = self
498 .state
499 .lock()
500 .expect("aggregate generation progress state mutex is poisoned");
501 state.phase = Some(phase);
502 state.kind = Some(kind);
503 state.process.clear();
504 state.process.push_str(process);
505 state.integrand.clear();
506 state.integrand.push_str(integrand);
507 state.total_graphs = total_graphs;
508 state.done_graphs = 0;
509 state.total_cuts = total_cuts;
510 state.done_cuts = 0;
511 state.discovered_st_cuts = 0;
512 state.discovered_valid_cuts = 0;
513 state.active_graphs.clear();
514 state.last_graph = None;
515 state.stats = GraphGenerationStats::default();
516 self.progress_span.pb_reset_elapsed();
517 self.refresh(&state);
518 }
519
520 fn graph_started(
521 &self,
522 _kind: GenerationProcessKind,
523 _integrand: &str,
524 graph: &str,
525 _cut_count: Option<usize>,
526 ) {
527 let mut state = self
528 .state
529 .lock()
530 .expect("aggregate generation progress state mutex is poisoned");
531 state.active_graphs.insert(graph.to_string());
532 state.last_graph = Some(graph.to_string());
533 self.refresh(&state);
534 }
535
536 fn graph_finished(
537 &self,
538 _kind: GenerationProcessKind,
539 _integrand: &str,
540 graph: &str,
541 stats: &GraphGenerationStats,
542 completed_cuts: Option<usize>,
543 ) {
544 let mut state = self
545 .state
546 .lock()
547 .expect("aggregate generation progress state mutex is poisoned");
548 state.done_graphs = state.done_graphs.saturating_add(1).min(state.total_graphs);
549 state.done_cuts += completed_cuts.unwrap_or(0);
550 state.active_graphs.remove(graph);
551 state.last_graph = Some(graph.to_string());
552 state.stats.merge_in_place(stats);
553 self.refresh(&state);
554 }
555
556 fn cuts_discovered(
557 &self,
558 _integrand: &str,
559 graph: &str,
560 st_cut_count: usize,
561 valid_cut_count: usize,
562 ) {
563 let mut state = self
564 .state
565 .lock()
566 .expect("aggregate generation progress state mutex is poisoned");
567 state.discovered_st_cuts += st_cut_count;
568 state.discovered_valid_cuts += valid_cut_count;
569 state.last_graph = Some(graph.to_string());
570 self.refresh(&state);
571 }
572
573 fn cut_finished(&self, _integrand: &str, graph: &str, cut_count: usize) {
574 let mut state = self
575 .state
576 .lock()
577 .expect("aggregate generation progress state mutex is poisoned");
578 state.done_cuts = state.done_cuts.saturating_add(cut_count);
579 if let Some(total_cuts) = state.total_cuts {
580 state.done_cuts = state.done_cuts.min(total_cuts);
581 }
582 state.last_graph = Some(graph.to_string());
583 self.refresh(&state);
584 }
585
586 fn backend_started(&self, kind: GenerationProcessKind, integrand: &str, graph_count: usize) {
587 let mut state = self
588 .state
589 .lock()
590 .expect("aggregate generation progress state mutex is poisoned");
591 state.phase = Some(GenerationProgressPhase::Backend);
592 state.kind = Some(kind);
593 state.integrand.clear();
594 state.integrand.push_str(integrand);
595 state.total_graphs = graph_count;
596 state.done_graphs = 0;
597 state.total_cuts = None;
598 state.done_cuts = 0;
599 state.active_graphs.clear();
600 state.last_graph = None;
601 self.progress_span.pb_reset_elapsed();
602 self.refresh(&state);
603 }
604
605 fn backend_finished(&self, _kind: GenerationProcessKind, _integrand: &str, elapsed: Duration) {
606 let mut state = self
607 .state
608 .lock()
609 .expect("aggregate generation progress state mutex is poisoned");
610 state.done_graphs = state.total_graphs;
611 state.stats.total_time += elapsed;
612 state.stats.evaluator_compile_time += elapsed;
613 self.refresh(&state);
614 }
615}
616
617impl GenerationMonitor {
618 const POLL_INTERVAL: Duration = Duration::from_millis(100);
619
620 #[cfg(target_os = "linux")]
621 fn parse_resident_memory_bytes(status: &str) -> Option<u64> {
622 let mut fields = status
623 .lines()
624 .find_map(|line| line.strip_prefix("VmRSS:"))?
625 .split_ascii_whitespace();
626 let kibibytes = fields.next()?.parse::<u64>().ok()?;
627 if fields.next()? != "kB" || fields.next().is_some() {
628 return None;
629 }
630 kibibytes.checked_mul(1024)
631 }
632
633 #[cfg(target_os = "linux")]
634 fn resident_memory_bytes() -> Option<u64> {
635 let status = fs::read_to_string("/proc/self/status").ok()?;
637 Self::parse_resident_memory_bytes(&status)
638 }
639
640 fn start() -> Result<Self> {
641 #[cfg(not(target_os = "linux"))]
642 let pid = get_current_pid().map_err(|err| eyre!("Failed to resolve current pid: {err}"))?;
643 #[cfg(target_os = "linux")]
644 let initial_memory = Self::resident_memory_bytes().unwrap_or(0);
645 #[cfg(not(target_os = "linux"))]
646 let initial_memory = 0;
647 let peak_ram_bytes = Arc::new(AtomicU64::new(initial_memory));
648 let current_ram_bytes = Arc::new(AtomicU64::new(initial_memory));
649 let stop_requested = Arc::new(AtomicBool::new(false));
650 let peak_ram_bytes_for_thread = Arc::clone(&peak_ram_bytes);
651 let current_ram_bytes_for_thread = Arc::clone(¤t_ram_bytes);
652 let stop_requested_for_thread = Arc::clone(&stop_requested);
653
654 let handle = thread::Builder::new()
655 .name("generation-ram-monitor".to_string())
656 .spawn(move || {
657 #[cfg(not(target_os = "linux"))]
658 let mut system = System::new();
659 loop {
660 if stop_requested_for_thread.load(Ordering::Relaxed) || is_interrupt_requested()
661 {
662 break;
663 }
664
665 #[cfg(target_os = "linux")]
666 let memory = Self::resident_memory_bytes();
667 #[cfg(not(target_os = "linux"))]
668 let memory = {
669 system.refresh_processes_specifics(
670 ProcessesToUpdate::Some(&[pid]),
671 true,
672 ProcessRefreshKind::nothing().with_memory(),
673 );
674 system.process(pid).map(|process| process.memory())
675 };
676 if let Some(memory) = memory {
677 current_ram_bytes_for_thread.store(memory, Ordering::Relaxed);
678 peak_ram_bytes_for_thread.fetch_max(memory, Ordering::Relaxed);
679 }
680 thread::park_timeout(Self::POLL_INTERVAL);
681 }
682 })
683 .map_err(|err| eyre!("Failed to spawn generation RAM monitor: {err}"))?;
684
685 Ok(Self {
686 peak_ram_bytes,
687 current_ram_bytes,
688 stop_requested,
689 handle: Some(handle),
690 })
691 }
692
693 fn current_ram_bytes(&self) -> Arc<AtomicU64> {
694 Arc::clone(&self.current_ram_bytes)
695 }
696
697 fn peak_ram_bytes(&self) -> Arc<AtomicU64> {
698 Arc::clone(&self.peak_ram_bytes)
699 }
700
701 fn finish(&mut self) -> u64 {
702 self.stop_requested.store(true, Ordering::Relaxed);
703 if let Some(handle) = self.handle.take() {
704 handle.thread().unpark();
705 let _ = handle.join();
706 }
707 self.peak_ram_bytes.load(Ordering::Relaxed)
708 }
709}
710
711impl Drop for GenerationMonitor {
712 fn drop(&mut self) {
713 let _ = self.finish();
714 }
715}
716
717#[derive(Debug, Clone, PartialEq, Eq, JsonSchema)]
718pub enum ProcessRef {
719 Id(usize),
720 Name(String),
721 Unqualified(String),
722}
723
724#[derive(Debug, Clone, Default)]
725pub struct GraphImportOptions {
726 pub process_name: Option<String>,
727 pub process_id: Option<usize>,
728 pub process_definition: Option<ProcessDefinition>,
729 pub integrand_name: Option<String>,
730 pub overwrite: bool,
731 pub append: bool,
732}
733
734impl Serialize for ProcessRef {
735 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
736 where
737 S: serde::Serializer,
738 {
739 match self {
740 ProcessRef::Id(id) => serializer.serialize_u64(*id as u64),
741 ProcessRef::Name(name) => serializer.serialize_str(&format!("name:{name}")),
742 ProcessRef::Unqualified(value) => serializer.serialize_str(value),
743 }
744 }
745}
746
747impl<'de> Deserialize<'de> for ProcessRef {
748 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
749 where
750 D: serde::Deserializer<'de>,
751 {
752 use serde::de::{self, Visitor};
753 use std::fmt;
754
755 struct ProcessRefVisitor;
756
757 impl<'de> Visitor<'de> for ProcessRefVisitor {
758 type Value = ProcessRef;
759
760 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
761 formatter.write_str("a process reference string or numeric id")
762 }
763
764 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
765 where
766 E: de::Error,
767 {
768 usize::try_from(value)
769 .map(ProcessRef::Id)
770 .map_err(E::custom)
771 }
772
773 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
774 where
775 E: de::Error,
776 {
777 usize::try_from(value)
778 .map(ProcessRef::Id)
779 .map_err(E::custom)
780 }
781
782 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
783 where
784 E: de::Error,
785 {
786 ProcessRef::from_str(value).map_err(E::custom)
787 }
788
789 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
790 where
791 E: de::Error,
792 {
793 self.visit_str(&value)
794 }
795 }
796
797 deserializer.deserialize_any(ProcessRefVisitor)
798 }
799}
800
801#[cfg(feature = "python")]
802impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for ProcessRef {
803 type Error = pyo3::PyErr;
804
805 fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::types::PyAny>) -> pyo3::PyResult<Self> {
806 if let Ok(process_id) = <usize as pyo3::FromPyObject<'a, 'py>>::extract(obj) {
807 return Ok(ProcessRef::Id(process_id));
808 }
809
810 let selector = <String as pyo3::FromPyObject<'a, 'py>>::extract(obj).map_err(|_| {
811 pyo3::exceptions::PyTypeError::new_err(
812 "process selectors must be either an integer process id or a string selector",
813 )
814 })?;
815 ProcessRef::from_str(&selector).map_err(pyo3::exceptions::PyValueError::new_err)
816 }
817}
818
819impl FromStr for ProcessRef {
820 type Err = String;
821
822 fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
823 if let Some(rest) = value.strip_prefix('#') {
824 let id = rest
825 .parse::<usize>()
826 .map_err(|_| format!("Invalid process id in '{value}'"))?;
827 return Ok(ProcessRef::Id(id));
828 }
829 if let Some(rest) = value.strip_prefix("name:") {
830 if rest.is_empty() {
831 return Err("Process name cannot be empty".to_string());
832 }
833 return Ok(ProcessRef::Name(rest.to_string()));
834 }
835 if value.is_empty() {
836 return Err("Process reference cannot be empty".to_string());
837 }
838 Ok(ProcessRef::Unqualified(value.to_string()))
839 }
840}
841
842#[test]
843fn try_complicated() {
844 "GGHHH3loop_no_iterative_optimization_3L"
845 .parse::<ProcessRef>()
846 .unwrap();
847 }
849
850impl ProcessRef {
851 pub fn resolve(&self, process_list: &ProcessList) -> Result<usize> {
852 let process_count = process_list.processes.len();
853 match self {
854 ProcessRef::Id(id) => {
855 if *id >= process_count {
856 return Err(eyre!(
857 "Process ID {} invalid, only {} processes available",
858 id,
859 process_count
860 ));
861 }
862 Ok(*id)
863 }
864 ProcessRef::Name(name) => process_list
865 .processes
866 .iter()
867 .position(|p| p.definition.folder_name == *name)
868 .ok_or_else(|| {
869 eyre!(
870 "No process named '{}'. Use 'display processes' to list available processes.",
871 name
872 )
873 }),
874 ProcessRef::Unqualified(value) => {
875 let name_match = process_list
876 .processes
877 .iter()
878 .position(|p| p.definition.folder_name == *value);
879 if let Ok(id) = value.parse::<usize>() {
880 let id_valid = id < process_count;
881 match (id_valid, name_match) {
882 (true, Some(_)) => Err(eyre!(
883 "Ambiguous process reference '{}'. Use '#{}' or 'name:{}' to disambiguate.",
884 value,
885 id,
886 value
887 )),
888 (true, None) => Ok(id),
889 (false, Some(index)) => Ok(index),
890 (false, None) => Err(eyre!(
891 "No process named '{}'. Use 'display processes' to list available processes.",
892 value
893 )),
894 }
895 } else if let Some(index) = name_match {
896 Ok(index)
897 } else {
898 Err(eyre!(
899 "No process named '{}'. Use 'display processes' to list available processes.",
900 value
901 ))
902 }
903 }
904 }
905 }
906}
907
908pub trait ProcessListExt {
909 fn find_integrand_ref(
910 &self,
911 process: Option<&ProcessRef>,
912 integrand_name: Option<&String>,
913 ) -> Result<(usize, String)>;
914 fn get_amplitude_mut_ref(
915 &mut self,
916 process: Option<&ProcessRef>,
917 integrand_name: Option<&String>,
918 ) -> Result<&mut Amplitude>;
919 fn get_cross_section_mut_ref(
920 &mut self,
921 process: Option<&ProcessRef>,
922 integrand_name: Option<&String>,
923 ) -> Result<&mut CrossSection>;
924}
925
926impl ProcessListExt for ProcessList {
927 fn find_integrand_ref(
928 &self,
929 process: Option<&ProcessRef>,
930 integrand_name: Option<&String>,
931 ) -> Result<(usize, String)> {
932 let process_id = match process {
933 Some(process_ref) => process_ref.resolve(self)?,
934 None => self.find_process(None)?,
935 };
936 let integrand_name = self.processes[process_id]
937 .collection
938 .find_integrand(integrand_name.cloned())
939 .with_note(|| format!("in process id {process_id}"))?;
940 Ok((process_id, integrand_name))
941 }
942
943 fn get_amplitude_mut_ref(
944 &mut self,
945 process: Option<&ProcessRef>,
946 integrand_name: Option<&String>,
947 ) -> Result<&mut Amplitude> {
948 let (process_id, integrand_name) = self.find_integrand_ref(process, integrand_name)?;
949 let process = &mut self.processes[process_id];
950 match &mut process.collection {
951 ProcessCollection::Amplitudes(amplitudes) => {
952 amplitudes.get_mut(&integrand_name).ok_or_else(|| {
953 eyre!(
954 "No amplitude named '{}' in process '{}'",
955 integrand_name,
956 process.definition.folder_name
957 )
958 })
959 }
960 ProcessCollection::CrossSections(_) => Err(eyre!(
961 "Process '{}' does not contain amplitudes",
962 process.definition.folder_name
963 )),
964 }
965 }
966
967 fn get_cross_section_mut_ref(
968 &mut self,
969 process: Option<&ProcessRef>,
970 integrand_name: Option<&String>,
971 ) -> Result<&mut CrossSection> {
972 let (process_id, integrand_name) = self.find_integrand_ref(process, integrand_name)?;
973 let process = &mut self.processes[process_id];
974 match &mut process.collection {
975 ProcessCollection::CrossSections(crosssections) => {
976 crosssections.get_mut(&integrand_name).ok_or_else(|| {
977 eyre!(
978 "No cross section named '{}' in process '{}'",
979 integrand_name,
980 process.definition.folder_name
981 )
982 })
983 }
984 ProcessCollection::Amplitudes(_) => Err(eyre!(
985 "Process '{}' does not contain crosssections",
986 process.definition.folder_name
987 )),
988 }
989 }
990}
991
992pub trait SyncSettings {
993 fn sync_settings(&self) -> Result<()>;
994}
995
996impl SyncSettings for CLISettings {
997 fn sync_settings(&self) -> Result<()> {
998 set_file_log_filter(&self.global.logfile_directive)?;
1000 set_stderr_log_filter(&self.global.display_directive)?;
1001 set_log_style(self.global.log_style.to_runtime());
1002 Ok(())
1003 }
1004}
1005
1006static SERIALIZE_COMMANDS_AS_STRINGS: AtomicBool = AtomicBool::new(false);
1008
1009fn is_command_blocks_empty(command_blocks: &[CommandsBlock]) -> bool {
1010 command_blocks.is_empty()
1011}
1012
1013fn should_persist_command(command: &Commands) -> bool {
1014 !matches!(
1015 command,
1016 Commands::Quit(_) | Commands::StartCommandsBlock(_) | Commands::FinishCommandsBlock
1017 )
1018}
1019
1020pub fn set_serialize_commands_as_strings(value: bool) {
1022 SERIALIZE_COMMANDS_AS_STRINGS.store(value, std::sync::atomic::Ordering::Relaxed);
1023}
1024
1025pub fn get_serialize_commands_as_strings() -> bool {
1027 SERIALIZE_COMMANDS_AS_STRINGS.load(std::sync::atomic::Ordering::Relaxed)
1028}
1029
1030pub struct SerializeCommandsAsStringsGuard {
1031 previous: bool,
1032}
1033
1034impl SerializeCommandsAsStringsGuard {
1035 pub fn new(value: bool) -> Self {
1036 let previous = get_serialize_commands_as_strings();
1037 set_serialize_commands_as_strings(value);
1038 Self { previous }
1039 }
1040}
1041
1042impl Drop for SerializeCommandsAsStringsGuard {
1043 fn drop(&mut self) {
1044 set_serialize_commands_as_strings(self.previous);
1045 }
1046}
1047
1048#[derive(Debug, Clone, PartialEq)]
1055pub struct CommandHistory {
1056 pub command: Commands,
1058 pub raw_string: Option<String>,
1060}
1061
1062impl JsonSchema for CommandHistory {
1063 fn schema_name() -> std::borrow::Cow<'static, str> {
1064 "CommandHistory".into()
1065 }
1066
1067 fn json_schema(generator: &mut schemars::SchemaGenerator) -> Schema {
1068 schemars::json_schema!({
1070 "description": "A raw command string (including scoped templates) or a structured command.",
1071 "anyOf": [
1072 { "type": "string" },
1073 generator.subschema_for::<Commands>()
1074 ]
1075 })
1076 }
1077}
1078
1079impl Serialize for CommandHistory {
1080 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1081 where
1082 S: serde::Serializer,
1083 {
1084 if let Commands::CommandTemplate(raw) = &self.command {
1085 return raw.serialize(serializer);
1086 }
1087
1088 if get_serialize_commands_as_strings() {
1089 if let Some(ref raw_string) = self.raw_string {
1090 raw_string.serialize(serializer)
1091 } else {
1092 self.command.serialize(serializer)
1093 }
1094 } else {
1095 self.command.serialize(serializer)
1096 }
1097 }
1098}
1099
1100impl<'de> Deserialize<'de> for CommandHistory {
1101 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1102 where
1103 D: serde::Deserializer<'de>,
1104 {
1105 use serde::de::{self, Visitor};
1106 use std::fmt;
1107
1108 struct CommandHistoryVisitor;
1109
1110 impl<'de> Visitor<'de> for CommandHistoryVisitor {
1111 type Value = CommandHistory;
1112
1113 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1114 formatter.write_str("a string or a Commands structure")
1115 }
1116
1117 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1118 where
1119 E: de::Error,
1120 {
1121 CommandHistory::from_raw_string(value).map_err(|err| {
1122 E::custom(format!(
1123 "Failed to parse command string '{}': {}",
1124 value, err
1125 ))
1126 })
1127 }
1128
1129 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
1130 where
1131 E: de::Error,
1132 {
1133 self.visit_str(&value)
1134 }
1135
1136 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
1137 where
1138 A: de::SeqAccess<'de>,
1139 {
1140 let command =
1142 Commands::deserialize(de::value::SeqAccessDeserializer::new(&mut seq))?;
1143 Ok(CommandHistory {
1144 command,
1145 raw_string: None,
1146 })
1147 }
1148
1149 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
1150 where
1151 A: de::MapAccess<'de>,
1152 {
1153 let command = Commands::deserialize(de::value::MapAccessDeserializer::new(map))?;
1155 Ok(CommandHistory {
1156 command,
1157 raw_string: None,
1158 })
1159 }
1160 }
1161
1162 deserializer.deserialize_any(CommandHistoryVisitor)
1163 }
1164}
1165
1166impl CommandHistory {
1167 pub fn new(command: Commands) -> Self {
1169 Self {
1170 command,
1171 raw_string: None,
1172 }
1173 }
1174
1175 pub fn new_with_raw(command: Commands, raw_string: String) -> Self {
1177 Self {
1178 command,
1179 raw_string: Some(raw_string),
1180 }
1181 }
1182
1183 pub fn new_template(raw_string: String) -> Self {
1184 Self::new(Commands::CommandTemplate(raw_string))
1185 }
1186
1187 pub fn is_template(&self) -> bool {
1188 matches!(self.command, Commands::CommandTemplate(_))
1189 }
1190
1191 pub fn raw_string(&self) -> Option<&str> {
1192 match &self.command {
1193 Commands::CommandTemplate(raw) => Some(raw),
1194 _ => self.raw_string.as_deref(),
1195 }
1196 }
1197
1198 pub fn semantically_eq(&self, other: &Self) -> bool {
1199 self.command == other.command
1200 }
1201
1202 pub fn from_command(command: Commands) -> Self {
1204 Self::new(command)
1205 }
1206
1207 pub fn from_raw_string(raw_string: &str) -> Result<Self, clap::Error> {
1212 use clap::error::ErrorKind;
1213
1214 let args = split_command_line(raw_string)
1215 .map(normalize_clap_args)
1216 .map_err(|_| {
1217 clap::Error::raw(
1218 ErrorKind::InvalidValue,
1219 "Could not parse command: unmatched quotes or trailing escape",
1220 )
1221 })?;
1222
1223 match Self::from_args_and_raw(args, raw_string.to_string()) {
1224 Ok(command) => Ok(command),
1225 Err(_) if contains_placeholder(raw_string) => Ok(Self::new_template(raw_string.into())),
1226 Err(err) => Err(err),
1227 }
1228 }
1229
1230 pub fn from_args_and_raw(args: Vec<String>, raw_string: String) -> Result<Self, clap::Error> {
1231 use crate::Repl;
1232 use clap::Parser;
1233
1234 let cli = Repl::try_parse_from(
1235 std::iter::once("gammaloop").chain(args.iter().map(String::as_str)),
1236 )?;
1237
1238 Ok(Self::new_with_raw(cli.command, raw_string))
1239 }
1240}
1241
1242#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
1243#[serde(default, deny_unknown_fields)]
1244pub struct RunHistory {
1245 #[serde(skip_serializing_if = "IsDefault::is_default")]
1247 pub default_runtime_settings: RuntimeSettings,
1248
1249 #[serde(skip_serializing_if = "IsDefault::is_default")]
1251 pub cli_settings: CLISettings,
1252
1253 #[serde(skip_serializing_if = "is_command_blocks_empty")]
1255 pub command_blocks: Vec<CommandsBlock>,
1256 pub commands: Vec<CommandHistory>,
1260}
1261
1262#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
1263#[serde(default, deny_unknown_fields)]
1264pub struct CommandsBlock {
1265 pub name: String,
1266 pub commands: Vec<CommandHistory>,
1267}
1268
1269#[derive(Debug, Deserialize, Default)]
1270#[serde(default, deny_unknown_fields)]
1271struct RawRunHistoryToml {
1272 default_runtime_settings: RuntimeSettings,
1273 cli_settings: CLISettings,
1274 command_blocks: Vec<RawCommandsBlockToml>,
1275 commands: Vec<TomlValue>,
1276}
1277
1278#[derive(Debug, Deserialize, Default)]
1279#[serde(default, deny_unknown_fields)]
1280struct RawCommandsBlockToml {
1281 name: String,
1282 commands: Vec<TomlValue>,
1283}
1284
1285impl CommandsBlock {
1286 pub fn semantically_eq(&self, other: &Self) -> bool {
1287 self.name == other.name
1288 && self.commands.len() == other.commands.len()
1289 && self
1290 .commands
1291 .iter()
1292 .zip(other.commands.iter())
1293 .all(|(left, right)| left.semantically_eq(right))
1294 }
1295}
1296
1297impl SmartSerde for RunHistory {
1298 fn has_schema_path(&self, online: bool) -> Option<Result<PathBuf>> {
1299 Some(get_schema_folder(online).map(|f| f.join("runhistory.json")))
1300 }
1301}
1302
1303impl RunHistory {
1304 pub fn freeze_boot_settings_from(&mut self, boot_run_history: &RunHistory) {
1305 self.cli_settings.global = boot_run_history.cli_settings.global.clone();
1306 self.default_runtime_settings = boot_run_history.default_runtime_settings.clone();
1307 }
1308
1309 pub fn frozen_boot_settings_match(&self, boot_run_history: &RunHistory) -> bool {
1310 self.cli_settings.global == boot_run_history.cli_settings.global
1311 && self.default_runtime_settings == boot_run_history.default_runtime_settings
1312 }
1313
1314 pub fn push(&mut self, command: Commands) {
1316 self.push_with_raw(command, None);
1317 }
1318
1319 pub fn push_with_raw(&mut self, command: Commands, raw_string: Option<String>) {
1324 if should_persist_command(&command) {
1325 self.commands.push(CommandHistory {
1326 command,
1327 raw_string,
1328 });
1329 }
1330 }
1331
1332 pub fn schema() -> Schema {
1333 schema_for!(RunHistory)
1334 }
1335
1336 pub fn validate(&self) -> Result<()> {
1337 let mut seen_names = HashSet::with_capacity(self.command_blocks.len());
1338 for block in &self.command_blocks {
1339 if block.name.trim().is_empty() {
1340 return Err(eyre!(
1341 "Run card `command_blocks` contains a block with an empty name"
1342 ));
1343 }
1344 if !seen_names.insert(block.name.clone()) {
1345 return Err(eyre!(
1346 "Run card `command_blocks` contains duplicate block name '{}'",
1347 block.name
1348 ));
1349 }
1350 }
1351 Ok(())
1352 }
1353
1354 pub fn command_block(&self, name: &str) -> Option<&CommandsBlock> {
1355 self.command_blocks.iter().find(|block| block.name == name)
1356 }
1357
1358 pub fn command_block_placeholder_names(&self, name: &str) -> BTreeSet<String> {
1359 self.command_block_placeholder_specs(name)
1360 .into_iter()
1361 .map(|spec| spec.name)
1362 .collect()
1363 }
1364
1365 pub fn command_block_placeholder_specs(&self, name: &str) -> BTreeSet<PlaceholderSpec> {
1366 let mut placeholders = BTreeSet::new();
1367 let mut visited = HashSet::new();
1368 self.collect_command_block_placeholder_specs(name, &mut visited, &mut placeholders);
1369 placeholders
1370 }
1371
1372 fn collect_command_block_placeholder_specs(
1373 &self,
1374 name: &str,
1375 visited: &mut HashSet<String>,
1376 placeholders: &mut BTreeSet<PlaceholderSpec>,
1377 ) {
1378 if !visited.insert(name.to_string()) {
1379 return;
1380 }
1381
1382 let Some(block) = self.command_block(name) else {
1383 return;
1384 };
1385
1386 for command in &block.commands {
1387 if let Some(raw) = command.raw_string() {
1388 placeholders.extend(placeholder_specs(raw));
1389 }
1390 let Commands::Run(run) = &command.command else {
1391 continue;
1392 };
1393 for nested_name in run.selected_block_names() {
1394 let mut nested_placeholders = BTreeSet::new();
1395 self.collect_command_block_placeholder_specs(
1396 nested_name,
1397 visited,
1398 &mut nested_placeholders,
1399 );
1400 for defined in &run.defines {
1401 nested_placeholders.retain(|spec| spec.name.as_str() != defined.key.as_str());
1402 }
1403 placeholders.extend(nested_placeholders);
1404 }
1405 }
1406 visited.remove(name);
1407 }
1408
1409 pub fn select_command_blocks(
1410 &self,
1411 selected_block_names: &[String],
1412 ) -> Result<Vec<CommandsBlock>> {
1413 let mut selected = Vec::with_capacity(selected_block_names.len());
1414 for name in selected_block_names {
1415 let block = self.command_block(name).ok_or_else(|| {
1416 eyre!(
1417 "Unknown command block '{}'. Available command blocks: {}",
1418 name,
1419 self.command_blocks
1420 .iter()
1421 .map(|block| block.name.as_str())
1422 .collect::<Vec<_>>()
1423 .join(", ")
1424 )
1425 })?;
1426 selected.push(block.clone());
1427 }
1428 Ok(selected)
1429 }
1430
1431 pub fn conflicting_command_block_names(&self, command_blocks: &[CommandsBlock]) -> Vec<String> {
1432 command_blocks
1433 .iter()
1434 .filter_map(|new_block| match self.command_block(&new_block.name) {
1435 Some(existing_block) if !existing_block.semantically_eq(new_block) => {
1436 Some(new_block.name.clone())
1437 }
1438 _ => None,
1439 })
1440 .collect()
1441 }
1442
1443 pub fn merge_command_blocks_with_overwrite(
1444 &mut self,
1445 command_blocks: &[CommandsBlock],
1446 overwrite_conflicts: bool,
1447 ) -> Result<()> {
1448 for new_block in command_blocks {
1449 match self
1450 .command_blocks
1451 .iter()
1452 .position(|existing| existing.name == new_block.name)
1453 {
1454 Some(index) if self.command_blocks[index].semantically_eq(new_block) => {}
1455 Some(index) if overwrite_conflicts => {
1456 self.command_blocks[index] = new_block.clone();
1457 }
1458 Some(_) => {
1459 return Err(eyre!(
1460 "Run card command block '{}' redefines an existing block with different commands",
1461 new_block.name
1462 ));
1463 }
1464 None => self.command_blocks.push(new_block.clone()),
1465 }
1466 }
1467 self.validate()
1468 }
1469
1470 pub fn merge_command_blocks(&mut self, command_blocks: &[CommandsBlock]) -> Result<()> {
1471 self.merge_command_blocks_with_overwrite(command_blocks, false)
1472 }
1473
1474 pub fn apply_session_settings(
1475 &self,
1476 global_settings: &mut CLISettings,
1477 default_runtime_settings: &mut RuntimeSettings,
1478 ) -> Result<()> {
1479 if self.cli_settings.global != GlobalSettings::default() {
1480 global_settings.global = self.cli_settings.global.clone();
1481 global_settings.sync_settings()?;
1482 }
1483 if self.default_runtime_settings != RuntimeSettings::default() {
1484 *default_runtime_settings = self.default_runtime_settings.clone();
1485 }
1486 Ok(())
1487 }
1488
1489 pub fn run(
1490 &mut self,
1491 state: &mut State,
1492 global_settings: &mut CLISettings,
1493 default_runtime_settings: &mut RuntimeSettings,
1494 ) -> Result<ControlFlow<SaveState>> {
1495 let mut session_state = crate::session::CliSessionState::default();
1496 let mut session = crate::session::CliSession::new(
1497 state,
1498 self,
1499 global_settings,
1500 default_runtime_settings,
1501 &mut session_state,
1502 );
1503 session.replay_run_history()
1504 }
1505
1506 pub(crate) fn filtered_for_save(&self) -> Self {
1507 let mut filtered = self.clone();
1508 filtered.commands.retain(|command_history| {
1509 command_history.is_template() || should_persist_command(&command_history.command)
1510 });
1511 filtered
1512 }
1513
1514 pub(crate) fn to_toml_string(&self, serialize_commands_as_strings: bool) -> Result<String> {
1515 let _serialize_commands_guard =
1516 SerializeCommandsAsStringsGuard::new(serialize_commands_as_strings);
1517 render_smart_toml(&self.filtered_for_save())
1518 }
1519
1520 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
1521 let path = path.as_ref();
1522 debug!("Loaded run history from file {}", path.display());
1523
1524 let runhistory = match path.extension().and_then(|ext| ext.to_str()) {
1525 Some("toml") => Self::load_toml(path)?,
1526 _ => Self::from_file(path, "run history")?,
1527 };
1528 runhistory.validate()?;
1529 Ok(runhistory)
1530 }
1531
1532 pub fn save_toml(
1533 &self,
1534 root_folder: &Path,
1535 override_state_file: bool,
1536 _strict: bool,
1537 ) -> Result<()> {
1538 self.filtered_for_save()
1539 .to_file(root_folder.join("run.toml"), override_state_file)?;
1540
1541 Ok(())
1543 }
1544
1545 pub fn save_yaml(
1546 &self,
1547 root_folder: &Path,
1548 override_state_file: bool,
1549 _strict: bool,
1550 ) -> Result<()> {
1551 self.filtered_for_save()
1552 .to_file(root_folder.join("run.yaml"), override_state_file)?;
1553 Ok(())
1555 }
1556
1557 fn load_toml(path: &Path) -> Result<Self> {
1558 let raw = fs::read_to_string(path)
1559 .with_context(|| format!("Could not open run history file {}", path.display()))?;
1560 let raw_run_history: RawRunHistoryToml =
1561 toml::from_str(&raw).wrap_err("Error parsing run history toml")?;
1562
1563 let commands = raw_run_history
1564 .commands
1565 .into_iter()
1566 .enumerate()
1567 .map(|(index, value)| {
1568 parse_toml_command_history(value, &format!("top-level command #{}", index + 1))
1569 })
1570 .collect::<Result<Vec<_>>>()?;
1571
1572 let command_blocks = raw_run_history
1573 .command_blocks
1574 .into_iter()
1575 .map(|block| {
1576 let RawCommandsBlockToml { name, commands } = block;
1577 let commands = commands
1578 .into_iter()
1579 .enumerate()
1580 .map(|(index, value)| {
1581 parse_toml_command_history(
1582 value,
1583 &format!("command block '{}' command #{}", name, index + 1),
1584 )
1585 })
1586 .collect::<Result<Vec<_>>>()?;
1587 Ok(CommandsBlock { name, commands })
1588 })
1589 .collect::<Result<Vec<_>>>()?;
1590
1591 Ok(Self {
1592 default_runtime_settings: raw_run_history.default_runtime_settings,
1593 cli_settings: raw_run_history.cli_settings,
1594 command_blocks,
1595 commands,
1596 })
1597 }
1598}
1599
1600fn parse_toml_command_history(value: TomlValue, context: &str) -> Result<CommandHistory> {
1601 match value {
1602 TomlValue::String(raw) => CommandHistory::from_raw_string(&raw)
1603 .map_err(|err| eyre!("Failed to parse {} '{}': {}", context, raw, err)),
1604 other => other
1605 .try_into::<CommandHistory>()
1606 .map_err(|err| eyre!("Failed to parse {}: {}", context, err)),
1607 }
1608}
1609
1610#[cfg_attr(
1611 feature = "python",
1612 pyo3::pyclass(from_py_object, unsendable, name = "GammaLoopState")
1613)]
1614#[derive(Clone)]
1615pub struct State {
1616 pub model: Model,
1618 pub model_parameters: InputParamCard<F<f64>>,
1620 pub process_list: ProcessList,
1622 pub generation_summaries: BTreeMap<IntegrandGenerationSummaryKey, IntegrandGenerationSummary>,
1624}
1625
1626const STATE_MANIFEST_FILE: &str = "state_manifest.toml";
1627const INTEGRAND_GENERATION_SUMMARY_FILE: &str = "generation_summary.json";
1628const CURRENT_STATE_MANIFEST_VERSION: u32 = 15;
1658const GENERATION_THREAD_STACK_SIZE_BYTES: usize = 32 * 1024 * 1024;
1659
1660#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1661#[serde(deny_unknown_fields)]
1662struct StateManifest {
1663 version: u32,
1664 #[serde(default)]
1665 ufo_print_symbols: Vec<String>,
1666 #[serde(default)]
1667 linnet_subgraph_print_labels: Vec<String>,
1668}
1669
1670impl Default for StateManifest {
1671 fn default() -> Self {
1672 Self {
1673 version: CURRENT_STATE_MANIFEST_VERSION,
1674 ufo_print_symbols: Vec::new(),
1675 linnet_subgraph_print_labels: Vec::new(),
1676 }
1677 }
1678}
1679
1680fn ensure_supported_state_manifest_version(manifest: &StateManifest) -> Result<()> {
1681 if manifest.version < CURRENT_STATE_MANIFEST_VERSION {
1682 return Err(eyre!(
1683 "State version {} predates the supported version {} and cannot be migrated. Please regenerate the saved state with this gammaloop binary.",
1684 manifest.version,
1685 CURRENT_STATE_MANIFEST_VERSION
1686 ));
1687 }
1688 if manifest.version > CURRENT_STATE_MANIFEST_VERSION {
1689 return Err(eyre!(
1690 "State version {} is newer than this binary supports (max {}). Please upgrade gammaloop.",
1691 manifest.version,
1692 CURRENT_STATE_MANIFEST_VERSION
1693 ));
1694 }
1695
1696 Ok(())
1697}
1698
1699fn validate_state_layout(manifest: &StateManifest, save_path: &Path) -> Result<()> {
1700 ensure_supported_state_manifest_version(manifest)?;
1701
1702 match manifest.version {
1703 CURRENT_STATE_MANIFEST_VERSION => {
1704 if !save_path.join("symbolica_state.bin").exists() {
1705 return Err(eyre!(
1706 "Saved state at '{}' is missing required file symbolica_state.bin",
1707 save_path.display()
1708 ));
1709 }
1710 if !save_path.join("processes").exists() {
1711 return Err(eyre!(
1712 "Saved state at '{}' is missing required folder processes/",
1713 save_path.display()
1714 ));
1715 }
1716 if !save_path.join("model.json").exists() {
1717 return Err(eyre!(
1718 "Saved state at '{}' is missing required file model.json",
1719 save_path.display()
1720 ));
1721 }
1722 Ok(())
1723 }
1724 _ => Err(eyre!(
1725 "State version {} is not supported by this binary.",
1726 manifest.version
1727 )),
1728 }
1729}
1730
1731#[derive(Debug, Clone, PartialEq, Eq)]
1732pub enum StateFolderKind {
1733 Missing,
1735 Scratch,
1737 Unmanifested,
1739 Saved,
1741 Invalid(String),
1743}
1744
1745fn is_scratch_state_entry(entry: &fs::DirEntry) -> bool {
1746 entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
1747 && entry.file_name().to_string_lossy() == "logs"
1748}
1749
1750pub fn classify_state_folder(save_path: &Path) -> Result<StateFolderKind> {
1751 if !save_path.exists() {
1752 return Ok(StateFolderKind::Missing);
1753 }
1754 if !save_path.is_dir() {
1755 return Ok(StateFolderKind::Invalid(format!(
1756 "'{}' exists but is not a directory",
1757 save_path.display()
1758 )));
1759 }
1760
1761 let manifest_path = save_path.join(STATE_MANIFEST_FILE);
1762 if manifest_path.exists() {
1763 let manifest = load_state_manifest(save_path)?;
1764 return Ok(match validate_state_layout(&manifest, save_path) {
1765 Ok(()) => StateFolderKind::Saved,
1766 Err(err) => StateFolderKind::Invalid(err.to_string()),
1767 });
1768 }
1769
1770 let mut entries = fs::read_dir(save_path)
1771 .with_context(|| format!("Trying to read state folder '{}'", save_path.display()))?;
1772 if entries.by_ref().all(|entry| {
1773 entry
1774 .map(|entry| is_scratch_state_entry(&entry))
1775 .unwrap_or(false)
1776 }) {
1777 return Ok(StateFolderKind::Scratch);
1778 }
1779
1780 Ok(StateFolderKind::Unmanifested)
1781}
1782
1783fn load_state_manifest(save_path: &Path) -> Result<StateManifest> {
1784 let manifest_path = save_path.join(STATE_MANIFEST_FILE);
1785 let raw_manifest = fs::read_to_string(&manifest_path).with_context(|| {
1786 format!(
1787 "Trying to read state manifest file {}",
1788 manifest_path.display()
1789 )
1790 })?;
1791 let manifest = toml::from_str::<StateManifest>(&raw_manifest).map_err(|error| {
1792 eyre!(
1793 "Trying to parse state manifest file {}: {error}",
1794 manifest_path.display()
1795 )
1796 })?;
1797 ensure_supported_state_manifest_version(&manifest)?;
1798 Ok(manifest)
1799}
1800
1801fn save_state_manifest(save_path: &Path) -> Result<()> {
1802 let mut ufo_print_symbols: Vec<_> = symbolica::state::State::symbol_iter()
1805 .filter(|(symbol, _)| {
1806 symbol.get_namespace() == "UFO" && symbol.get_print_function().is_some()
1807 })
1808 .map(|(symbol, _)| symbol.get_stripped_name().to_owned())
1809 .collect();
1810 ufo_print_symbols.sort_unstable();
1811 let mut linnet_subgraph_print_labels: Vec<_> = symbolica::state::State::symbol_iter()
1812 .filter(|(symbol, _)| {
1813 symbol.get_namespace() == "linnet" && symbol.get_print_function().is_some()
1814 })
1815 .filter_map(|(symbol, _)| {
1816 symbol
1817 .get_stripped_name()
1818 .strip_prefix("S_")
1819 .map(str::to_owned)
1820 })
1821 .collect();
1822 linnet_subgraph_print_labels.sort_unstable();
1823 let manifest = StateManifest {
1824 ufo_print_symbols,
1825 linnet_subgraph_print_labels,
1826 ..StateManifest::default()
1827 };
1828 let raw_manifest =
1829 toml::to_string_pretty(&manifest).context("Trying to serialize state manifest to TOML")?;
1830 fs::write(save_path.join(STATE_MANIFEST_FILE), raw_manifest).with_context(|| {
1831 format!(
1832 "Trying to write state manifest file {}",
1833 save_path.join(STATE_MANIFEST_FILE).display()
1834 )
1835 })?;
1836 Ok(())
1837}
1838
1839fn integrand_generation_summary_path(
1840 root_folder: &Path,
1841 process: &Process,
1842 integrand_name: &str,
1843) -> PathBuf {
1844 let process_kind_folder = match &process.collection {
1845 ProcessCollection::Amplitudes(_) => "amplitudes",
1846 ProcessCollection::CrossSections(_) => "cross_sections",
1847 };
1848
1849 root_folder
1850 .join("processes")
1851 .join(process_kind_folder)
1852 .join(&process.definition.folder_name)
1853 .join(integrand_name)
1854 .join(INTEGRAND_GENERATION_SUMMARY_FILE)
1855}
1856
1857fn process_kind_folder(root_folder: &Path, process: &Process) -> PathBuf {
1858 let process_kind_folder = match &process.collection {
1859 ProcessCollection::Amplitudes(_) => "amplitudes",
1860 ProcessCollection::CrossSections(_) => "cross_sections",
1861 };
1862
1863 root_folder.join("processes").join(process_kind_folder)
1864}
1865
1866fn process_artifact_folder(root_folder: &Path, process: &Process) -> PathBuf {
1867 process_kind_folder(root_folder, process).join(&process.definition.folder_name)
1868}
1869
1870fn integrand_artifact_folder(
1871 root_folder: &Path,
1872 process: &Process,
1873 integrand_name: &str,
1874) -> PathBuf {
1875 process_artifact_folder(root_folder, process).join(integrand_name)
1876}
1877
1878fn generated_integrand_artifact_path(
1879 root_folder: &Path,
1880 process: &Process,
1881 integrand_name: &str,
1882) -> PathBuf {
1883 integrand_artifact_folder(root_folder, process, integrand_name).join("integrand")
1884}
1885
1886fn ensure_existing_path_under_root(path: &Path, root: &Path, description: &str) -> Result<PathBuf> {
1887 let canonical_root = root
1888 .canonicalize()
1889 .with_context(|| format!("Trying to resolve artifact cleanup root {}", root.display()))?;
1890 let canonical_path = path.canonicalize().with_context(|| {
1891 format!(
1892 "Trying to resolve {} path {} before removal",
1893 description,
1894 path.display()
1895 )
1896 })?;
1897 if !canonical_path.starts_with(&canonical_root) {
1898 return Err(eyre!(
1899 "Refusing to remove {} path {} because it resolves outside {}",
1900 description,
1901 canonical_path.display(),
1902 canonical_root.display()
1903 ));
1904 }
1905 Ok(canonical_path)
1906}
1907
1908fn remove_file_if_exists_under_root(path: &Path, root: &Path, description: &str) -> Result<bool> {
1909 if !path.try_exists().with_context(|| {
1910 format!(
1911 "Trying to check whether {} path {} exists",
1912 description,
1913 path.display()
1914 )
1915 })? {
1916 return Ok(false);
1917 }
1918 let canonical_path = ensure_existing_path_under_root(path, root, description)?;
1919 fs::remove_file(path).with_context(|| {
1920 format!(
1921 "Trying to remove {} path {}",
1922 description,
1923 canonical_path.display()
1924 )
1925 })?;
1926 Ok(true)
1927}
1928
1929fn remove_dir_if_exists_under_root(path: &Path, root: &Path, description: &str) -> Result<bool> {
1930 if !path.try_exists().with_context(|| {
1931 format!(
1932 "Trying to check whether {} path {} exists",
1933 description,
1934 path.display()
1935 )
1936 })? {
1937 return Ok(false);
1938 }
1939 let canonical_path = ensure_existing_path_under_root(path, root, description)?;
1940 fs::remove_dir_all(path).with_context(|| {
1941 format!(
1942 "Trying to remove {} path {}",
1943 description,
1944 canonical_path.display()
1945 )
1946 })?;
1947 Ok(true)
1948}
1949
1950fn remove_saved_process_artifacts(root_folder: &Path, process: &Process) -> Result<bool> {
1951 let root = process_kind_folder(root_folder, process);
1952 let path = process_artifact_folder(root_folder, process);
1953 remove_dir_if_exists_under_root(&path, &root, "saved process artifacts")
1954}
1955
1956fn remove_saved_integrand_artifacts(
1957 root_folder: &Path,
1958 process: &Process,
1959 integrand_name: &str,
1960) -> Result<bool> {
1961 let root = process_artifact_folder(root_folder, process);
1962 let path = integrand_artifact_folder(root_folder, process, integrand_name);
1963 remove_dir_if_exists_under_root(&path, &root, "saved integrand artifacts")
1964}
1965
1966fn validate_output_name(value: &str, flag_name: &str) -> Result<()> {
1967 if value.trim().is_empty() {
1968 return Err(eyre!("{flag_name} must not be empty"));
1969 }
1970 Ok(())
1971}
1972
1973fn rename_process_integrand(integrand: Option<&mut ProcessIntegrand>, new_name: &str) {
1974 let Some(integrand) = integrand else {
1975 return;
1976 };
1977 let _ = integrand.get_mut_settings();
1978 match integrand {
1979 ProcessIntegrand::Amplitude(amplitude) => {
1980 amplitude.data.name = new_name.to_string();
1981 }
1982 ProcessIntegrand::CrossSection(cross_section) => {
1983 cross_section.data.name = new_name.to_string();
1984 }
1985 }
1986}
1987
1988fn load_integrand_generation_summaries(
1989 root_folder: &Path,
1990 process_list: &ProcessList,
1991) -> Result<BTreeMap<IntegrandGenerationSummaryKey, IntegrandGenerationSummary>> {
1992 let mut summaries = BTreeMap::new();
1993
1994 for (process_id, process) in process_list.processes.iter().enumerate() {
1995 for integrand_name in process.collection.get_integrand_names() {
1996 let summary_path =
1997 integrand_generation_summary_path(root_folder, process, integrand_name);
1998 if !summary_path.exists() {
1999 continue;
2000 }
2001
2002 let raw_summary = fs::read_to_string(&summary_path).with_context(|| {
2003 format!(
2004 "Trying to read integrand generation summary {}",
2005 summary_path.display()
2006 )
2007 })?;
2008 let mut summary: IntegrandGenerationSummary = serde_json::from_str(&raw_summary)
2009 .with_context(|| {
2010 format!(
2011 "Trying to parse integrand generation summary {}",
2012 summary_path.display()
2013 )
2014 })?;
2015 for report in &mut summary.reports {
2019 report.process_id = process_id;
2020 }
2021 summaries.insert(
2022 IntegrandGenerationSummaryKey {
2023 process_id,
2024 integrand_name: integrand_name.to_string(),
2025 },
2026 summary,
2027 );
2028 }
2029 }
2030
2031 Ok(summaries)
2032}
2033
2034impl State {
2035 fn overridable_model_parameter_names(&self) -> Vec<String> {
2036 let mut names = self
2037 .model_parameters
2038 .keys()
2039 .map(|symbol| symbol.to_string())
2040 .collect::<Vec<_>>();
2041 names.sort();
2042 names
2043 }
2044
2045 fn resolve_overridable_model_parameter_type(
2046 &self,
2047 parameter_name: &str,
2048 ) -> Result<gammalooprs::model::ParameterType> {
2049 let possibilities = self.overridable_model_parameter_names();
2050 let parameter_type = external_model_parameter_type(&self.model, parameter_name)
2051 .ok_or_else(|| eyre!("No model parameter named '{parameter_name}'"))
2052 .with_note(|| {
2053 format!(
2054 "Possible model parameters are: {}",
2055 possibilities.join(", ")
2056 )
2057 })?;
2058
2059 let symbol = UFOSymbol::from(parameter_name);
2060 if !self.model_parameters.contains_key(&symbol) {
2061 return Err(eyre!(
2062 "Model parameter '{parameter_name}' cannot be overridden because it is not present in the shared top-level model_parameters.json"
2063 ))
2064 .with_note(|| format!("Possible model parameters are: {}", possibilities.join(", ")));
2065 }
2066
2067 Ok(parameter_type)
2068 }
2069
2070 pub fn find_generated_integrand_ref_by_name(
2071 &self,
2072 integrand_name: &str,
2073 ) -> Result<(usize, String)> {
2074 let matches = self
2075 .process_list
2076 .processes
2077 .iter()
2078 .enumerate()
2079 .filter_map(|(process_id, process)| {
2080 let found = match &process.collection {
2081 ProcessCollection::Amplitudes(amplitudes) => amplitudes
2082 .get(integrand_name)
2083 .filter(|amplitude| amplitude.integrand.is_some())
2084 .map(|_| (process_id, process.definition.folder_name.clone())),
2085 ProcessCollection::CrossSections(cross_sections) => cross_sections
2086 .get(integrand_name)
2087 .filter(|cross_section| cross_section.integrand.is_some())
2088 .map(|_| (process_id, process.definition.folder_name.clone())),
2089 };
2090 found.map(|(process_id, process_name)| {
2091 (process_id, process_name, integrand_name.to_string())
2092 })
2093 })
2094 .collect::<Vec<_>>();
2095
2096 match matches.as_slice() {
2097 [(process_id, _, canonical_name)] => Ok((*process_id, canonical_name.clone())),
2098 [] => Err(eyre!(
2099 "No generated integrand named '{integrand_name}' was found. Per-integrand model parameters are only supported for generated integrands."
2100 )),
2101 _ => {
2102 let names = matches
2103 .iter()
2104 .map(|(process_id, process_name, _)| format!("#{process_id} ({process_name})"))
2105 .collect::<Vec<_>>();
2106 Err(eyre!(
2107 "Integrand name '{integrand_name}' is ambiguous across generated integrands"
2108 ))
2109 .with_note(|| format!("Matching processes: {}", names.join(", ")))
2110 }
2111 }
2112 }
2113
2114 pub fn import_model(&mut self, path: impl AsRef<Path>) -> Result<()> {
2115 self.model = Model::from_file(path)?;
2116 Ok(())
2117 }
2118
2119 fn matching_process_ids(&self, process: Option<&ProcessRef>) -> Result<Vec<usize>> {
2120 let Some(process) = process else {
2121 return Ok((0..self.process_list.processes.len()).collect());
2122 };
2123
2124 match process {
2125 ProcessRef::Id(id) => Ok((*id < self.process_list.processes.len())
2126 .then_some(*id)
2127 .into_iter()
2128 .collect()),
2129 ProcessRef::Name(name) => Ok(self
2130 .process_list
2131 .processes
2132 .iter()
2133 .position(|p| p.definition.folder_name == *name)
2134 .into_iter()
2135 .collect()),
2136 ProcessRef::Unqualified(value) => {
2137 let name_match = self
2138 .process_list
2139 .processes
2140 .iter()
2141 .position(|p| p.definition.folder_name == *value);
2142 if let Ok(id) = value.parse::<usize>() {
2143 let id_valid = id < self.process_list.processes.len();
2144 match (id_valid, name_match) {
2145 (true, Some(_)) => Err(eyre!(
2146 "Ambiguous process reference '{}'. Use '#{}' or 'name:{}' to disambiguate.",
2147 value,
2148 id,
2149 value
2150 )),
2151 (true, None) => Ok(vec![id]),
2152 (false, Some(index)) => Ok(vec![index]),
2153 (false, None) => Ok(vec![]),
2154 }
2155 } else {
2156 Ok(name_match.into_iter().collect())
2157 }
2158 }
2159 }
2160 }
2161
2162 pub fn remove_selected_integrands(
2163 &mut self,
2164 process: Option<&ProcessRef>,
2165 integrand_name: Option<&str>,
2166 ) -> Result<Vec<RemovedIntegrand>> {
2167 let process_ids = self.matching_process_ids(process)?;
2168 let mut removed = Vec::new();
2169
2170 for process_id in process_ids.into_iter().rev() {
2171 let process_name = self.process_list.processes[process_id]
2172 .definition
2173 .folder_name
2174 .clone();
2175 let integrand_names = {
2176 let collection = &self.process_list.processes[process_id].collection;
2177 match integrand_name {
2178 Some(name) => collection
2179 .get_integrand_names()
2180 .into_iter()
2181 .find(|candidate| *candidate == name)
2182 .map(|name| vec![name.to_string()])
2183 .unwrap_or_default(),
2184 None => collection
2185 .get_integrand_names()
2186 .into_iter()
2187 .map(str::to_string)
2188 .collect(),
2189 }
2190 };
2191
2192 if integrand_names.is_empty() {
2193 continue;
2194 }
2195
2196 {
2197 let process_entry = &mut self.process_list.processes[process_id];
2198 for integrand_name in &integrand_names {
2199 process_entry.collection.remove_integrand(integrand_name)?;
2200 }
2201 }
2202 for integrand_name in &integrand_names {
2203 self.generation_summaries
2204 .remove(&IntegrandGenerationSummaryKey {
2205 process_id,
2206 integrand_name: integrand_name.clone(),
2207 });
2208 }
2209
2210 let removed_empty_process = self.process_list.processes[process_id]
2211 .collection
2212 .get_integrand_names()
2213 .is_empty();
2214 if removed_empty_process {
2215 self.process_list.processes.remove(process_id);
2216 self.remove_generation_summaries_for_process(process_id);
2217 }
2218
2219 for integrand_name in integrand_names {
2220 removed.push(RemovedIntegrand {
2221 process_id,
2222 process_name: process_name.clone(),
2223 integrand_name,
2224 removed_empty_process,
2225 });
2226 }
2227 }
2228
2229 removed.reverse();
2230 Ok(removed)
2231 }
2232
2233 pub fn remove_process(&mut self, process: Option<&ProcessRef>) -> Result<RemovedProcess> {
2234 let process_id = self.resolve_process_ref(process)?;
2235 let removed = self.process_list.processes.remove(process_id);
2236 self.remove_generation_summaries_for_process(process_id);
2237 Ok(RemovedProcess {
2238 process_id,
2239 process_name: removed.definition.folder_name,
2240 })
2241 }
2242
2243 pub fn remove_integrand(
2244 &mut self,
2245 process: &ProcessRef,
2246 integrand_name: &str,
2247 ) -> Result<RemovedIntegrand> {
2248 let process_id = process.resolve(&self.process_list)?;
2249 let (process_name, canonical_integrand_name, removed_empty_process) = {
2250 let process_entry = &mut self.process_list.processes[process_id];
2251 let process_name = process_entry.definition.folder_name.clone();
2252 let canonical_integrand_name = process_entry
2253 .collection
2254 .find_integrand(Some(integrand_name.to_string()))?;
2255 process_entry
2256 .collection
2257 .remove_integrand(&canonical_integrand_name)?;
2258 let removed_empty_process = process_entry.collection.get_integrand_names().is_empty();
2259 (
2260 process_name,
2261 canonical_integrand_name,
2262 removed_empty_process,
2263 )
2264 };
2265 self.generation_summaries
2266 .remove(&IntegrandGenerationSummaryKey {
2267 process_id,
2268 integrand_name: canonical_integrand_name.clone(),
2269 });
2270
2271 if removed_empty_process {
2272 self.process_list.processes.remove(process_id);
2273 self.remove_generation_summaries_for_process(process_id);
2274 }
2275
2276 Ok(RemovedIntegrand {
2277 process_id,
2278 process_name,
2279 integrand_name: canonical_integrand_name,
2280 removed_empty_process,
2281 })
2282 }
2283
2284 pub fn resolve_process_ref(&self, process: Option<&ProcessRef>) -> Result<usize> {
2285 match process {
2286 Some(process_ref) => process_ref.resolve(&self.process_list),
2287 None => self.process_list.find_process(None),
2288 }
2289 }
2290
2291 pub fn find_integrand_ref(
2292 &self,
2293 process: Option<&ProcessRef>,
2294 integrand_name: Option<&String>,
2295 ) -> Result<(usize, String)> {
2296 let process_id = self.resolve_process_ref(process)?;
2297 let integrand_name = self.process_list.processes[process_id]
2298 .collection
2299 .find_integrand(integrand_name.cloned())
2300 .with_note(|| format!("in process id {process_id}"))?;
2301 Ok((process_id, integrand_name))
2302 }
2303
2304 pub fn get_integrand_info(
2305 &self,
2306 process: Option<&ProcessRef>,
2307 integrand_name: Option<&String>,
2308 ) -> Result<IntegrandInfo> {
2309 let (process_id, integrand_name) = self.find_integrand_ref(process, integrand_name)?;
2310 collect_integrand_info(self, process_id, &integrand_name)
2311 }
2312
2313 pub fn duplicate_integrand(
2314 &mut self,
2315 process: Option<&ProcessRef>,
2316 integrand_name: Option<&String>,
2317 output_process_name: &str,
2318 output_integrand_name: &str,
2319 ) -> Result<()> {
2320 validate_output_name(output_process_name, "--output_process_name")?;
2321 validate_output_name(output_integrand_name, "--output_integrand_name")?;
2322
2323 let (source_process_id, source_integrand_name) =
2324 self.find_integrand_ref(process, integrand_name)?;
2325 let mut payload =
2326 self.cloned_integrand_payload(source_process_id, &source_integrand_name)?;
2327 payload.rename(output_integrand_name);
2328 self.insert_integrand_copy(
2329 source_process_id,
2330 output_process_name,
2331 output_integrand_name,
2332 payload,
2333 false,
2334 false,
2335 None,
2336 false,
2337 )?;
2338 Ok(())
2339 }
2340
2341 pub fn select_integrand_graph_groups(
2342 &mut self,
2343 process: Option<&ProcessRef>,
2344 integrand_name: Option<&String>,
2345 selection: &GraphGroupSelectionSpec,
2346 target: &GraphGroupSelectionTarget,
2347 context: GraphGroupSelectionContext<'_>,
2348 ) -> Result<SelectedGraphGroups> {
2349 if target.clear_existing_processes && !target.is_copy_mode() {
2350 return Err(eyre!(
2351 "--clear-existing-processes requires --output_process or --output_integrand for `select`"
2352 ));
2353 }
2354
2355 let (source_process_id, source_integrand_name) =
2356 self.find_integrand_ref(process, integrand_name)?;
2357 let (source_process_name, plan, discarded_generated_integrand) = {
2358 let process_entry = &self.process_list.processes[source_process_id];
2359 let process_name = process_entry.definition.folder_name.clone();
2360 match &process_entry.collection {
2361 ProcessCollection::Amplitudes(amplitudes) => {
2362 if selection.has_raised_cut_rules() {
2363 return Err(eyre!(
2364 "Raised-cut signature selection can only be used with cross-section integrands."
2365 ));
2366 }
2367 if selection.mode() == GraphGroupSelectionMode::CrossSectionAmplitudeGraphs {
2368 return Err(eyre!(
2369 "`select --amplitude-graphs` can only be used with cross-section integrands."
2370 ));
2371 }
2372 let amplitude = amplitudes.get(&source_integrand_name).ok_or_else(|| {
2373 eyre!(
2374 "No amplitude named '{}' in process '{}'",
2375 source_integrand_name,
2376 process_name
2377 )
2378 })?;
2379 let plan = amplitude.plan_graph_group_selection(selection)?;
2380 amplitude.validate_graph_group_selection_plan(&plan)?;
2381 (process_name, plan, amplitude.integrand.is_some())
2382 }
2383 ProcessCollection::CrossSections(cross_sections) => {
2384 let cross_section =
2385 cross_sections.get(&source_integrand_name).ok_or_else(|| {
2386 eyre!(
2387 "No cross section named '{}' in process '{}'",
2388 source_integrand_name,
2389 process_name
2390 )
2391 })?;
2392 let plan = cross_section.plan_graph_group_selection_with_context(
2393 selection,
2394 &self.model,
2395 &process_entry.definition,
2396 context.generation_settings,
2397 )?;
2398 cross_section.validate_graph_group_selection_plan(&plan)?;
2399 (process_name, plan, cross_section.integrand.is_some())
2400 }
2401 }
2402 };
2403 let report = plan.report().clone();
2404 let source = SelectionSource {
2405 process_id: source_process_id,
2406 process_name: source_process_name,
2407 integrand_name: source_integrand_name,
2408 };
2409
2410 if target.is_copy_mode() {
2411 return self.select_integrand_graph_groups_to_output(
2412 &source,
2413 &plan,
2414 report,
2415 target,
2416 context.state_folder,
2417 context.read_only_state,
2418 );
2419 }
2420
2421 let mut removed_generated_artifacts = false;
2422 let mut removed_generation_summary = false;
2423 if discarded_generated_integrand {
2424 if context.read_only_state {
2425 return Err(eyre!(
2426 "Cannot select graph groups for generated integrand '{}' in process '{}' because this session was started with --read-only-state. Restart without --read-only-state or select before generation.",
2427 source.integrand_name,
2428 source.process_name
2429 ));
2430 }
2431
2432 let process_entry = &self.process_list.processes[source.process_id];
2433 let integrand_artifact_root = integrand_artifact_folder(
2434 context.state_folder,
2435 process_entry,
2436 &source.integrand_name,
2437 );
2438 let generated_artifact_path = generated_integrand_artifact_path(
2439 context.state_folder,
2440 process_entry,
2441 &source.integrand_name,
2442 );
2443 removed_generated_artifacts = remove_dir_if_exists_under_root(
2444 &generated_artifact_path,
2445 &integrand_artifact_root,
2446 "generated integrand artifact",
2447 )?;
2448 let generation_summary_path = integrand_generation_summary_path(
2449 context.state_folder,
2450 process_entry,
2451 &source.integrand_name,
2452 );
2453 removed_generation_summary = remove_file_if_exists_under_root(
2454 &generation_summary_path,
2455 &integrand_artifact_root,
2456 "integrand generation summary",
2457 )?;
2458 }
2459
2460 {
2461 let process_entry = &mut self.process_list.processes[source.process_id];
2462 match &mut process_entry.collection {
2463 ProcessCollection::Amplitudes(amplitudes) => {
2464 let amplitude =
2465 amplitudes.get_mut(&source.integrand_name).ok_or_else(|| {
2466 eyre!(
2467 "No amplitude named '{}' in process '{}'",
2468 source.integrand_name,
2469 source.process_name
2470 )
2471 })?;
2472 amplitude.apply_graph_group_selection(&plan)?;
2473 }
2474 ProcessCollection::CrossSections(cross_sections) => {
2475 let cross_section =
2476 cross_sections
2477 .get_mut(&source.integrand_name)
2478 .ok_or_else(|| {
2479 eyre!(
2480 "No cross section named '{}' in process '{}'",
2481 source.integrand_name,
2482 source.process_name
2483 )
2484 })?;
2485 cross_section.apply_graph_group_selection(&plan)?;
2486 }
2487 }
2488 }
2489
2490 if discarded_generated_integrand {
2491 self.generation_summaries
2492 .remove(&IntegrandGenerationSummaryKey {
2493 process_id: source.process_id,
2494 integrand_name: source.integrand_name.clone(),
2495 });
2496 }
2497
2498 Ok(SelectedGraphGroups {
2499 source_process_id: source.process_id,
2500 source_process_name: source.process_name.clone(),
2501 source_integrand_name: source.integrand_name.clone(),
2502 process_id: source.process_id,
2503 process_name: source.process_name,
2504 integrand_name: source.integrand_name,
2505 report,
2506 copied_to_output: false,
2507 replaced_existing_target: false,
2508 removed_target_artifacts: false,
2509 discarded_generated_integrand,
2510 removed_generated_artifacts,
2511 removed_generation_summary,
2512 })
2513 }
2514
2515 fn select_integrand_graph_groups_to_output(
2516 &mut self,
2517 source: &SelectionSource,
2518 plan: &GraphGroupSelectionPlan,
2519 report: GraphGroupSelectionReport,
2520 target: &GraphGroupSelectionTarget,
2521 state_folder: &Path,
2522 read_only_state: bool,
2523 ) -> Result<SelectedGraphGroups> {
2524 let output_process_name = target
2525 .output_process_name
2526 .as_deref()
2527 .unwrap_or(&source.process_name);
2528 let output_integrand_name = target
2529 .output_integrand_name
2530 .as_deref()
2531 .unwrap_or(&source.integrand_name);
2532
2533 validate_output_name(output_process_name, "--output_process")?;
2534 validate_output_name(output_integrand_name, "--output_integrand")?;
2535
2536 if output_process_name == source.process_name
2537 && output_integrand_name == source.integrand_name
2538 {
2539 return Err(eyre!(
2540 "Copy-mode select target '{} / {}' is the selected source integrand. Omit --output_process/--output_integrand for in-place selection or choose a different output target.",
2541 output_process_name,
2542 output_integrand_name
2543 ));
2544 }
2545
2546 let target_process_id = self
2547 .process_list
2548 .processes
2549 .iter()
2550 .position(|process| process.definition.folder_name == output_process_name);
2551 let replace_existing_process = target.output_process_name.is_some()
2552 && target.clear_existing_processes
2553 && target_process_id.is_some_and(|process_id| process_id != source.process_id);
2554
2555 let mut payload =
2556 self.cloned_integrand_payload(source.process_id, &source.integrand_name)?;
2557 payload.rename(output_integrand_name);
2558 payload.apply_graph_group_selection(plan)?;
2559
2560 let insertion = self.insert_integrand_copy(
2561 source.process_id,
2562 output_process_name,
2563 output_integrand_name,
2564 payload,
2565 target.clear_existing_processes,
2566 replace_existing_process,
2567 Some(state_folder),
2568 read_only_state,
2569 )?;
2570
2571 Ok(SelectedGraphGroups {
2572 source_process_id: source.process_id,
2573 source_process_name: source.process_name.clone(),
2574 source_integrand_name: source.integrand_name.clone(),
2575 process_id: insertion.process_id,
2576 process_name: insertion.process_name,
2577 integrand_name: insertion.integrand_name,
2578 report,
2579 copied_to_output: true,
2580 replaced_existing_target: insertion.replaced_existing_target,
2581 removed_target_artifacts: insertion.removed_target_artifacts,
2582 discarded_generated_integrand: false,
2583 removed_generated_artifacts: false,
2584 removed_generation_summary: false,
2585 })
2586 }
2587
2588 fn cloned_integrand_payload(
2589 &self,
2590 source_process_id: usize,
2591 source_integrand_name: &str,
2592 ) -> Result<IntegrandCopyPayload> {
2593 let source_process = &self.process_list.processes[source_process_id];
2594 match &source_process.collection {
2595 ProcessCollection::Amplitudes(amplitudes) => amplitudes
2596 .get(source_integrand_name)
2597 .cloned()
2598 .map(IntegrandCopyPayload::Amplitude)
2599 .ok_or_else(|| eyre!("Missing source amplitude '{}'", source_integrand_name)),
2600 ProcessCollection::CrossSections(cross_sections) => cross_sections
2601 .get(source_integrand_name)
2602 .cloned()
2603 .map(IntegrandCopyPayload::CrossSection)
2604 .ok_or_else(|| eyre!("Missing source cross section '{}'", source_integrand_name)),
2605 }
2606 }
2607
2608 #[allow(clippy::too_many_arguments)]
2609 fn insert_integrand_copy(
2610 &mut self,
2611 source_process_id: usize,
2612 output_process_name: &str,
2613 output_integrand_name: &str,
2614 payload: IntegrandCopyPayload,
2615 clear_existing_processes: bool,
2616 replace_existing_process: bool,
2617 state_folder: Option<&Path>,
2618 read_only_state: bool,
2619 ) -> Result<IntegrandCopyInsertion> {
2620 if let Some(destination_process_id) = self
2621 .process_list
2622 .processes
2623 .iter()
2624 .position(|process| process.definition.folder_name == output_process_name)
2625 {
2626 if replace_existing_process {
2627 if read_only_state {
2628 return Err(eyre!(
2629 "Cannot overwrite output process '{}' because this session was started with --read-only-state.",
2630 output_process_name
2631 ));
2632 }
2633 let removed_target_artifacts = if let Some(state_folder) = state_folder {
2634 remove_saved_process_artifacts(
2635 state_folder,
2636 &self.process_list.processes[destination_process_id],
2637 )?
2638 } else {
2639 false
2640 };
2641 self.remove_generation_summaries_for_process_without_shifting(
2642 destination_process_id,
2643 );
2644 let process = self.single_integrand_process(
2645 source_process_id,
2646 destination_process_id,
2647 output_process_name,
2648 payload,
2649 );
2650 self.process_list.processes[destination_process_id] = process;
2651 return Ok(IntegrandCopyInsertion {
2652 process_id: destination_process_id,
2653 process_name: output_process_name.to_string(),
2654 integrand_name: output_integrand_name.to_string(),
2655 replaced_existing_target: true,
2656 removed_target_artifacts,
2657 });
2658 }
2659
2660 let target_exists = self.process_list.processes[destination_process_id]
2661 .collection
2662 .get_integrand_names()
2663 .contains(&output_integrand_name);
2664 if target_exists {
2665 if !clear_existing_processes {
2666 return Err(eyre!(
2667 "An integrand '{}' already exists in process '{}'",
2668 output_integrand_name,
2669 output_process_name
2670 ));
2671 }
2672 if read_only_state {
2673 return Err(eyre!(
2674 "Cannot overwrite output integrand '{}' in process '{}' because this session was started with --read-only-state.",
2675 output_integrand_name,
2676 output_process_name
2677 ));
2678 }
2679 }
2680
2681 if !payload
2682 .is_compatible_with(&self.process_list.processes[destination_process_id].collection)
2683 {
2684 return Err(eyre!(
2685 "Destination process '{}' exists but does not contain {}",
2686 output_process_name,
2687 payload.kind_name()
2688 ));
2689 }
2690
2691 let removed_target_artifacts = if target_exists {
2692 if let Some(state_folder) = state_folder {
2693 remove_saved_integrand_artifacts(
2694 state_folder,
2695 &self.process_list.processes[destination_process_id],
2696 output_integrand_name,
2697 )?
2698 } else {
2699 false
2700 }
2701 } else {
2702 false
2703 };
2704
2705 if target_exists {
2706 self.generation_summaries
2707 .remove(&IntegrandGenerationSummaryKey {
2708 process_id: destination_process_id,
2709 integrand_name: output_integrand_name.to_string(),
2710 });
2711 }
2712
2713 payload.insert_into_process(
2714 &mut self.process_list.processes[destination_process_id],
2715 output_process_name,
2716 )?;
2717 Ok(IntegrandCopyInsertion {
2718 process_id: destination_process_id,
2719 process_name: output_process_name.to_string(),
2720 integrand_name: output_integrand_name.to_string(),
2721 replaced_existing_target: target_exists,
2722 removed_target_artifacts,
2723 })
2724 } else {
2725 let process_id = self.process_list.processes.len();
2726 let process = self.single_integrand_process(
2727 source_process_id,
2728 process_id,
2729 output_process_name,
2730 payload,
2731 );
2732 self.process_list.add_process(process);
2733 Ok(IntegrandCopyInsertion {
2734 process_id,
2735 process_name: output_process_name.to_string(),
2736 integrand_name: output_integrand_name.to_string(),
2737 replaced_existing_target: false,
2738 removed_target_artifacts: false,
2739 })
2740 }
2741 }
2742
2743 fn single_integrand_process(
2744 &self,
2745 source_process_id: usize,
2746 process_id: usize,
2747 process_name: &str,
2748 payload: IntegrandCopyPayload,
2749 ) -> Process {
2750 let source_process = &self.process_list.processes[source_process_id];
2751 let mut definition = source_process.definition.clone();
2752 definition.folder_name = process_name.to_string();
2753 definition.process_id = process_id;
2754 Process {
2755 definition,
2756 settings_history: source_process.settings_history.clone(),
2757 collection: payload.into_collection(),
2758 }
2759 }
2760
2761 pub fn resolve_effective_model_parameter_card_for_settings(
2762 &self,
2763 settings: &RuntimeSettings,
2764 ) -> Result<InputParamCard<F<f64>>> {
2765 let mut card = self.model_parameters.clone();
2766
2767 for (parameter_name, value) in &settings.model.external_parameters {
2768 let parameter_type = self.resolve_overridable_model_parameter_type(parameter_name)?;
2769 let value = Complex::new(value.0, value.1);
2770 validate_model_parameter_type(parameter_name, parameter_type, &value)?;
2771
2772 let parameter = card
2773 .get_mut(&UFOSymbol::from(parameter_name.as_str()))
2774 .ok_or_else(|| {
2775 eyre!(
2776 "Model parameter '{parameter_name}' is missing from the shared top-level model_parameters.json"
2777 )
2778 })?;
2779 *parameter = value;
2780 }
2781
2782 Ok(card)
2783 }
2784
2785 pub fn resolve_serializable_model_parameter_card_for_settings(
2786 &self,
2787 settings: &RuntimeSettings,
2788 ) -> Result<SerializableInputParamCard<F<f64>>> {
2789 Ok(self
2790 .resolve_effective_model_parameter_card_for_settings(settings)?
2791 .to_serializable())
2792 }
2793
2794 pub fn resolve_effective_model_parameter_card_for_integrand(
2795 &self,
2796 process_id: usize,
2797 integrand_name: &str,
2798 ) -> Result<SerializableInputParamCard<F<f64>>> {
2799 let resolved = self
2800 .process_list
2801 .get_integrand(process_id, integrand_name)?;
2802 match resolved.get_settings() {
2803 Some(settings) => self.resolve_serializable_model_parameter_card_for_settings(settings),
2804 None => Ok(self.model_parameters.to_serializable()),
2805 }
2806 }
2807
2808 pub fn resolve_model_for_settings(&self, settings: &RuntimeSettings) -> Result<Model> {
2809 let mut model = self.model.clone();
2810 self.resolve_effective_model_parameter_card_for_settings(settings)?
2811 .apply_to_model(&mut model)?;
2812 Ok(model)
2813 }
2814
2815 pub fn resolve_model_for_integrand(
2816 &self,
2817 process_id: usize,
2818 integrand_name: &str,
2819 ) -> Result<Model> {
2820 let resolved = self
2821 .process_list
2822 .get_integrand(process_id, integrand_name)?;
2823 match resolved.get_settings() {
2824 Some(settings) => self.resolve_model_for_settings(settings),
2825 None => Ok(self.model.clone()),
2826 }
2827 }
2828
2829 pub fn generate_integrands(
2830 &mut self,
2831 global_settings: &GlobalSettings,
2832 runtime_default: LockedRuntimeSettings,
2833 ) -> Result<GenerationReports> {
2834 self.run_generation_with_monitor(global_settings, move |state, generation_pool| {
2835 let mut reports = state.process_list.preprocess(
2836 &state.model,
2837 global_settings,
2838 &runtime_default,
2839 generation_pool,
2840 )?;
2841 merge_generated_graph_reports(
2842 &mut reports,
2843 state.process_list.generate_integrands(
2844 &state.model,
2845 global_settings,
2846 runtime_default,
2847 generation_pool,
2848 )?,
2849 );
2850 Ok(reports)
2851 })
2852 }
2853
2854 fn attach_process_id_to_named_reports(
2855 process_id: usize,
2856 reports: Vec<NamedGraphGenerationReport>,
2857 ) -> Vec<GeneratedGraphReport> {
2858 reports
2859 .into_iter()
2860 .map(|report| GeneratedGraphReport {
2861 process_id,
2862 integrand_name: report.integrand_name,
2863 graph_name: report.graph_name,
2864 stats: report.stats,
2865 })
2866 .collect()
2867 }
2868
2869 pub fn generate_integrand(
2870 &mut self,
2871 global_settings: &GlobalSettings,
2872 runtime_default: LockedRuntimeSettings,
2873 process_id: usize,
2874 integrand_name: Option<String>,
2875 ) -> Result<GenerationReports> {
2876 self.run_generation_with_monitor(global_settings, move |state, generation_pool| {
2877 let p = &mut state.process_list.processes[process_id];
2878 let process_name = p.definition.folder_name.clone();
2879 if let Some(name) = &integrand_name {
2880 let _ = p.get_integrand(name)?;
2881 for other in p.get_integrand_names() {
2884 if other != name.as_str()
2885 && p.get_integrand(other)?.integrand.is_some()
2886 && p.settings_history.as_ref().is_none_or(|history| {
2887 history.generation != global_settings.generation
2888 })
2889 {
2890 return Err(eyre!(
2891 "Cannot generate only integrand '{}' in process '{}' with absent or different shared generation settings while sibling '{}' is already generated; regenerate the whole process without --integrand-name",
2892 name, process_name, other
2893 ));
2894 }
2895 }
2896 let mut reports = Vec::new();
2897 match &mut p.collection {
2898 ProcessCollection::Amplitudes(a) => {
2899 if let Some(a) = a.get_mut(name) {
2900 begin_phase(
2901 GenerationProgressPhase::GraphPreprocessing,
2902 GenerationProcessKind::Amplitude,
2903 &process_name,
2904 &a.name,
2905 a.graphs.len(),
2906 None,
2907 );
2908 merge_generated_graph_reports(
2909 &mut reports,
2910 Self::attach_process_id_to_named_reports(
2911 process_id,
2912 a.preprocess(
2913 &state.model,
2914 &global_settings.generation,
2915 &runtime_default,
2916 generation_pool,
2917 )?,
2918 ),
2919 );
2920 merge_generated_graph_reports(
2921 &mut reports,
2922 Self::attach_process_id_to_named_reports(
2923 process_id,
2924 a.build_integrand(
2925 &state.model,
2926 &process_name,
2927 global_settings,
2928 runtime_default,
2929 generation_pool,
2930 )?,
2931 ),
2932 );
2933 } else {
2934 return Err(eyre!(
2935 "No amplitude named '{}' in process id {}",
2936 name,
2937 process_id
2938 ));
2939 }
2940 }
2941 ProcessCollection::CrossSections(cs) => {
2942 if let Some(cs) = cs.get_mut(name) {
2943 merge_generated_graph_reports(
2944 &mut reports,
2945 Self::attach_process_id_to_named_reports(
2946 process_id,
2947 cs.preprocess(
2948 &state.model,
2949 &p.definition,
2950 &global_settings.generation,
2951 runtime_default,
2952 generation_pool,
2953 )?,
2954 ),
2955 );
2956 merge_generated_graph_reports(
2957 &mut reports,
2958 Self::attach_process_id_to_named_reports(
2959 process_id,
2960 cs.build_integrand(
2961 &state.model,
2962 &p.definition,
2963 global_settings,
2964 runtime_default,
2965 generation_pool,
2966 )?,
2967 ),
2968 );
2969 } else {
2970 return Err(eyre!(
2971 "No cross section named '{}' in process id {}",
2972 name,
2973 process_id
2974 ));
2975 }
2976 }
2977 }
2978 p.settings_history = Some(global_settings.clone());
2981 Ok(reports)
2982 } else {
2983 let mut reports = p.preprocess(
2984 &state.model,
2985 global_settings,
2986 &runtime_default,
2987 generation_pool,
2988 )?;
2989 merge_generated_graph_reports(
2990 &mut reports,
2991 p.generate_integrands(
2992 &state.model,
2993 global_settings,
2994 runtime_default,
2995 generation_pool,
2996 )?,
2997 );
2998 Ok(reports)
2999 }
3000 })
3001 }
3002
3003 fn run_generation_with_monitor<F>(
3004 &mut self,
3005 global_settings: &GlobalSettings,
3006 generation: F,
3007 ) -> Result<GenerationReports>
3008 where
3009 F: FnOnce(&mut Self, &rayon::ThreadPool) -> Result<Vec<GeneratedGraphReport>>,
3010 {
3011 let generation_pool = rayon::ThreadPoolBuilder::new()
3012 .num_threads(global_settings.n_cores.generate)
3013 .stack_size(GENERATION_THREAD_STACK_SIZE_BYTES)
3014 .build()?;
3015 let generation_cores = generation_pool.current_num_threads();
3016 clear_interrupt_request();
3017 let mut monitor = GenerationMonitor::start()?;
3018 let stderr_is_terminal = io::stderr().is_terminal();
3019 let progress_mode = if generation_cores == 1 && stderr_is_terminal {
3020 GenerationProgressMode::Detailed
3021 } else {
3022 GenerationProgressMode::Aggregate
3023 };
3024 let _progress_mode_guard = GenerationProgressModeGuard::set(progress_mode);
3025 let _progress_observer_guard = if generation_cores > 1 && stderr_is_terminal {
3026 let reporter = AggregateGenerationProgressReporter::new(
3027 monitor.current_ram_bytes(),
3028 monitor.peak_ram_bytes(),
3029 generation_cores as u64,
3030 );
3031 Some(GenerationProgressObserverGuard::set(reporter))
3032 } else {
3033 None
3034 };
3035 let generation_result = generation(self, &generation_pool);
3036 let peak_ram_bytes = monitor.finish();
3037 clear_interrupt_request();
3038
3039 generation_result.map(|reports| GenerationReports {
3040 reports,
3041 resources: GenerationResourceSummary {
3042 peak_ram_bytes,
3043 generation_cores,
3044 },
3045 })
3046 }
3047
3048 pub fn compile_integrands(
3049 &mut self,
3050 folder: impl AsRef<Path>,
3051 override_existing: bool,
3052 global_settings: &GlobalSettings,
3053 process_id: Option<usize>,
3054 integrand_name: Option<String>,
3055 ) -> Result<Vec<GeneratedGraphReport>> {
3056 let compile_pool = rayon::ThreadPoolBuilder::new()
3057 .num_threads(global_settings.n_cores.compile)
3058 .build()?;
3059 self.process_list.compile(
3060 folder,
3061 override_existing,
3062 process_id,
3063 integrand_name,
3064 &compile_pool,
3065 )
3066 }
3067
3068 pub fn generation_summary(
3069 &self,
3070 process_id: usize,
3071 integrand_name: &str,
3072 ) -> Option<&IntegrandGenerationSummary> {
3073 self.generation_summaries
3074 .get(&IntegrandGenerationSummaryKey {
3075 process_id,
3076 integrand_name: integrand_name.to_string(),
3077 })
3078 }
3079
3080 pub fn record_generation_summary(
3081 &mut self,
3082 reports: &[GeneratedGraphReport],
3083 resources: GenerationResourceSummary,
3084 ) {
3085 let mut reports_by_integrand: BTreeMap<
3086 IntegrandGenerationSummaryKey,
3087 Vec<GeneratedGraphReport>,
3088 > = BTreeMap::new();
3089
3090 for report in reports {
3091 let Some(process) = self.process_list.processes.get(report.process_id) else {
3092 continue;
3093 };
3094 if !process
3095 .collection
3096 .get_integrand_names()
3097 .contains(&report.integrand_name.as_str())
3098 {
3099 continue;
3100 }
3101
3102 reports_by_integrand
3103 .entry(IntegrandGenerationSummaryKey {
3104 process_id: report.process_id,
3105 integrand_name: report.integrand_name.clone(),
3106 })
3107 .or_default()
3108 .push(report.clone());
3109 }
3110
3111 for (key, reports) in reports_by_integrand {
3112 self.generation_summaries.insert(
3113 key,
3114 IntegrandGenerationSummary {
3115 peak_ram_bytes: resources.peak_ram_bytes,
3116 reports,
3117 },
3118 );
3119 }
3120 }
3121
3122 fn remove_generation_summaries_for_process(&mut self, process_id: usize) {
3123 let mut updated = BTreeMap::new();
3124 for (mut key, mut summary) in std::mem::take(&mut self.generation_summaries) {
3125 if key.process_id == process_id {
3126 continue;
3127 }
3128 if key.process_id > process_id {
3129 key.process_id -= 1;
3130 for report in &mut summary.reports {
3131 if report.process_id > process_id {
3132 report.process_id -= 1;
3133 }
3134 }
3135 }
3136 updated.insert(key, summary);
3137 }
3138 self.generation_summaries = updated;
3139 }
3140
3141 fn remove_generation_summaries_for_process_without_shifting(&mut self, process_id: usize) {
3142 self.generation_summaries
3143 .retain(|key, _| key.process_id != process_id);
3144 }
3145
3146 pub fn export_dots(
3147 &mut self,
3148 path: impl AsRef<Path>,
3149 settings: &DotExportSettings,
3150 ) -> Result<()> {
3151 self.process_list.export_dot(path, settings)?;
3152 Ok(())
3153 }
3154
3155 pub fn import_graphs(&mut self, graphs: Vec<Graph>, options: GraphImportOptions) -> Result<()> {
3156 let GraphImportOptions {
3157 process_name,
3158 process_id,
3159 process_definition,
3160 integrand_name,
3161 overwrite,
3162 append,
3163 } = options;
3164 let generation_type = Self::infer_graph_list_generation_type(&graphs)?;
3165 if let Some(definition) = &process_definition {
3166 if definition.generation_type != generation_type {
3167 return Err(eyre!(
3168 "--process-spec describes a {} process, but the imported graph list is {}",
3169 definition.generation_type,
3170 generation_type
3171 ));
3172 }
3173 }
3174
3175 let integrand_base_name = integrand_name.clone().unwrap_or("default".to_string());
3176 let process = if let Some(proc_id) = process_id {
3177 if proc_id >= self.process_list.processes.len() {
3178 return Err(eyre!(
3179 "Process ID {} invalid, only {} processes available",
3180 proc_id,
3181 self.process_list.processes.len()
3182 ));
3183 }
3184 Some(&mut self.process_list.processes[proc_id])
3185 } else {
3186 let p_name = match process_name {
3187 Some(n) => n,
3188 None => {
3189 return Err(eyre!(
3190 "Either process ID or process name must be provided when importing graphs"
3191 ));
3192 }
3193 };
3194 if let Some(existing_proc) = self
3195 .process_list
3196 .processes
3197 .iter_mut()
3198 .find(|p| p.definition.folder_name == p_name)
3199 {
3200 Some(existing_proc)
3201 } else {
3202 let mut process_definition = match process_definition.clone() {
3203 Some(definition) => definition,
3204 None => {
3205 ProcessDefinition::from_graph_list(&graphs, generation_type, &self.model)?
3206 }
3207 };
3208 process_definition.process_id = self.process_list.processes.len();
3209 let process = Process::from_graph_list(
3210 p_name,
3211 integrand_base_name.clone(),
3212 graphs.clone(),
3214 generation_type,
3215 Some(process_definition),
3216 None,
3217 &self.model,
3218 )?;
3219
3220 self.process_list.add_process(process);
3221 None
3222 }
3223 };
3224 if let Some(p) = process {
3225 if let Some(mut imported_definition) = process_definition {
3226 imported_definition.folder_name = p.definition.folder_name.clone();
3227 imported_definition.process_id = p.definition.process_id;
3228 if imported_definition != p.definition {
3229 return Err(eyre!(
3230 "--process-spec does not match existing process '{}'. Import the graphs into a new process or use the same process specification that created this process.",
3231 p.definition.folder_name
3232 ));
3233 }
3234 }
3235
3236 let existing_names = p.get_integrand_names();
3237 let integrand_name = if existing_names.contains(&integrand_base_name.as_str()) {
3238 if append {
3239 let mut integrand_i = 0;
3240 while existing_names
3241 .iter()
3242 .any(|ce| *ce == format!("{}_{}", integrand_base_name, integrand_i))
3243 {
3244 integrand_i += 1;
3245 }
3246 format!("{}_{}", integrand_base_name, integrand_i)
3247 } else if overwrite {
3248 p.collection.remove_integrand(&integrand_base_name)?;
3249 integrand_base_name.clone()
3250 } else {
3251 return Err(eyre!(
3252 "Integrand name '{}' already exists in process '{}', use either --overwrite or --append flag when loading graphs",
3253 integrand_base_name,
3254 p.definition.folder_name
3255 ));
3256 }
3257 } else {
3258 integrand_base_name.clone()
3259 };
3260
3261 match generation_type {
3262 GenerationType::Amplitude => p
3263 .collection
3264 .add_amplitude(Amplitude::from_graph_list(integrand_name.clone(), graphs)?),
3265 GenerationType::CrossSection => {
3266 p.collection
3267 .add_cross_section(CrossSection::from_graph_list(
3268 integrand_name.clone(),
3269 graphs,
3270 &self.model,
3271 )?)
3272 }
3273 }
3274 }
3275
3276 Ok(())
3277 }
3278
3279 pub(crate) fn infer_graph_list_generation_type(graphs: &[Graph]) -> Result<GenerationType> {
3280 if graphs.iter().all(|g| g.initial_state_cut.nedges(g) == 0) {
3281 Ok(GenerationType::Amplitude)
3282 } else if graphs.iter().all(|g| g.initial_state_cut.nedges(g) > 0) {
3283 Ok(GenerationType::CrossSection)
3284 } else {
3285 Err(eyre!(
3286 "Mix of amplitude and cross section graphs in the same file is not supported"
3287 ))
3288 }
3289 }
3290
3291 pub fn new(log_dir: impl AsRef<Path>, log_file_name: Option<String>) -> Self {
3292 super::tracing::init_tracing(log_dir.as_ref().join("logs"), log_file_name);
3293 let _ = initialise();
3294
3295 Self {
3296 model: Model::default(),
3297 process_list: ProcessList::default(),
3298 model_parameters: InputParamCard::default(),
3299 generation_summaries: BTreeMap::new(),
3300 }
3301 }
3302
3303 pub fn new_test() -> Self {
3304 init_test_tracing();
3305
3306 Self {
3307 model: Model::default(),
3308 process_list: ProcessList::default(),
3309 model_parameters: InputParamCard::default(),
3310 generation_summaries: BTreeMap::new(),
3311 }
3312 }
3313
3314 pub fn new_bench() -> Self {
3315 init_bench_tracing();
3316
3317 Self {
3318 model: Model::default(),
3319 process_list: ProcessList::default(),
3320 model_parameters: InputParamCard::default(),
3321 generation_summaries: BTreeMap::new(),
3322 }
3323 }
3324}
3325
3326#[derive(Debug, Clone, PartialEq, Eq)]
3327pub struct RemovedProcess {
3328 pub process_id: usize,
3329 pub process_name: String,
3330}
3331
3332#[derive(Debug, Clone, PartialEq, Eq)]
3333pub struct RemovedIntegrand {
3334 pub process_id: usize,
3335 pub process_name: String,
3336 pub integrand_name: String,
3337 pub removed_empty_process: bool,
3338}
3339
3340impl State {
3341 pub fn load(
3342 save_path: PathBuf,
3343 model_path: Option<PathBuf>,
3344 trace_logs_filename: Option<String>,
3345 ) -> Result<Self> {
3346 Self::load_with_selection(save_path, model_path, trace_logs_filename, None)
3347 }
3348
3349 pub fn load_with_selection(
3350 save_path: PathBuf,
3351 model_path: Option<PathBuf>,
3352 trace_logs_filename: Option<String>,
3353 selection: Option<&ProcessLoadSelection>,
3354 ) -> Result<Self> {
3355 let manifest = load_state_manifest(&save_path)?;
3357 validate_state_layout(&manifest, &save_path)?;
3358 let mut loaded_state = State::new(&save_path, trace_logs_filename);
3362 debug!("Loading state manifest version {}", manifest.version);
3363
3364 for name in &manifest.ufo_print_symbols {
3367 let _ = UFOSymbol::from(name);
3368 }
3369 for label in &manifest.linnet_subgraph_print_labels {
3370 let _ = SuBitGraph::symbol_from_label(label.clone());
3371 }
3372
3373 let mut model = if let Some(model_path) = &model_path {
3376 info!("Loading model from {}", model_path.display());
3377 Model::from_file(model_path)?
3378 } else {
3379 let model_dir = save_path.join("model.json");
3380 info!(
3381 "Loading model from default location: {}",
3382 model_dir.display()
3383 );
3384 Model::from_file(model_dir)?
3385 };
3386
3387 debug!("Loaded model: {}", model.name);
3388
3389 let input_param_card = if save_path.join("model_parameters.json").exists() {
3390 let a = InputParamCard::from_file(save_path.join("model_parameters.json"))?;
3391
3392 let _ = model.apply_param_card(&a);
3393 a
3394 } else {
3395 InputParamCard::default_from_model(&model)
3396 };
3397
3398 let symbolica_state = symbolica::state::State::import(
3399 &mut fs::File::open(save_path.join("symbolica_state.bin"))
3400 .context("Trying to open symbolica state binary")?,
3401 None,
3402 )?;
3403
3404 let context: GammaLoopContextContainer<'_> = GammaLoopContextContainer {
3405 state_map: &symbolica_state,
3406 model: &model,
3407 };
3408
3409 let process_list = ProcessList::load_with_selection(&save_path, context, selection)
3410 .context("Trying to load processList")?;
3411
3412 loaded_state.process_list = process_list;
3413 loaded_state.model = model;
3414 loaded_state.model_parameters = input_param_card;
3415 loaded_state.generation_summaries =
3416 load_integrand_generation_summaries(&save_path, &loaded_state.process_list)?;
3417 Ok(loaded_state)
3418 }
3419
3420 pub fn activate_loaded_integrand_backends(
3421 &mut self,
3422 allow_symjit_fallback: bool,
3423 ) -> Result<()> {
3424 self.process_list
3425 .activate_loaded_integrand_backends(allow_symjit_fallback)
3426 }
3427
3428 pub fn compile(
3429 &mut self,
3430 root_folder: &Path,
3431 override_compiled: bool,
3432 settings: &GlobalSettings,
3433 ) -> Result<()> {
3434 fs::create_dir_all(root_folder)?;
3435
3436 let compile_pool = rayon::ThreadPoolBuilder::new()
3437 .num_threads(settings.n_cores.compile)
3438 .build()?;
3439 self.process_list
3440 .compile(root_folder, override_compiled, None, None, &compile_pool)?;
3441 Ok(())
3442 }
3443
3444 fn save_generation_summaries(&self, root_folder: &Path) -> Result<()> {
3445 for (process_id, process) in self.process_list.processes.iter().enumerate() {
3446 for integrand_name in process.collection.get_integrand_names() {
3447 let summary_path =
3448 integrand_generation_summary_path(root_folder, process, integrand_name);
3449 let key = IntegrandGenerationSummaryKey {
3450 process_id,
3451 integrand_name: integrand_name.to_string(),
3452 };
3453 if let Some(summary) = self.generation_summaries.get(&key) {
3454 if let Some(parent) = summary_path.parent() {
3455 fs::create_dir_all(parent)?;
3456 }
3457 let raw_summary = serde_json::to_string_pretty(summary).with_context(|| {
3458 format!(
3459 "Trying to serialize integrand generation summary {}",
3460 summary_path.display()
3461 )
3462 })?;
3463 fs::write(&summary_path, raw_summary).with_context(|| {
3464 format!(
3465 "Trying to write integrand generation summary {}",
3466 summary_path.display()
3467 )
3468 })?;
3469 } else if summary_path.exists() {
3470 fs::remove_file(&summary_path).with_context(|| {
3471 format!(
3472 "Trying to remove stale integrand generation summary {}",
3473 summary_path.display()
3474 )
3475 })?;
3476 }
3477 }
3478 }
3479
3480 Ok(())
3481 }
3482
3483 pub fn save(
3484 &mut self,
3485 root_folder: &Path,
3486 override_state_file: bool,
3487 strict: bool,
3488 ) -> Result<()> {
3489 let mut selected_root_folder = PathBuf::from(root_folder);
3493 let mut user_input = String::new();
3494 if !root_folder.exists() {
3495 fs::create_dir_all(root_folder)?;
3496 } else {
3497 if strict {
3498 return Err(eyre!(
3499 "Export root already exists, please choose a different path or remove the existing directory",
3500 ));
3501 }
3502
3503 if !override_state_file {
3504 while selected_root_folder.exists() {
3505 println!(
3506 "Gammaloop export root {} already exists. Specify 'o' for overwriting, 'n' for not saving, or '<NEW_PATH>' to specify where to save current state to:",
3507 selected_root_folder.display()
3508 );
3509 user_input.clear();
3510 io::stdin()
3511 .read_line(&mut user_input)
3512 .expect("Could not read user-specified gammaloop state export destination");
3513 match user_input.trim() {
3515 "o" => break,
3516 "n" => {
3517 return Ok(());
3518 }
3519 new_path => {
3520 selected_root_folder = PathBuf::from(new_path);
3521 continue;
3522 }
3523 }
3524 }
3525 }
3526 }
3527
3528 if selected_root_folder.join(STATE_MANIFEST_FILE).exists() {
3529 load_state_manifest(&selected_root_folder)?;
3530 }
3531 fs::create_dir_all(&selected_root_folder)?;
3532
3533 let mut state_file =
3534 fs::File::create(selected_root_folder.join("symbolica_state.bin"))?;
3536
3537 symbolica::state::State::export(&mut state_file)?;
3538 self.process_list
3539 .save(&selected_root_folder, override_state_file)?;
3540 self.save_generation_summaries(&selected_root_folder)?;
3541
3542 self.model
3545 .to_serializable()
3546 .to_file(selected_root_folder.join("model.json"), override_state_file)?;
3547 self.model_parameters.to_file(
3548 selected_root_folder.join("model_parameters.json"),
3549 override_state_file,
3550 )?;
3551 save_state_manifest(&selected_root_folder)?;
3552 Ok(())
3553 }
3554}
3555
3556#[cfg(test)]
3557mod tests {
3558 use std::{fs, process::Command};
3559
3560 use gammalooprs::{
3561 graph::Graph,
3562 initialisation::test_initialise,
3563 integrands::process::{ActiveF64Backend, GaussianReferenceFunction},
3564 model::InputParamCard,
3565 momentum::{Dep, ExternalMomenta, Helicity},
3566 processes::{
3567 process::ProcessCollection, RaisedPropagatorScope, RaisedPropagatorSignature,
3568 SelectionPolarity,
3569 },
3570 settings::global::{CompilationMode, FrozenCompilationMode},
3571 settings::{
3572 runtime::kinematic::{improvement::PhaseSpaceImprovementSettings, Externals},
3573 KinematicsSettings, RuntimeSettings,
3574 },
3575 utils::{load_generic_model, serde_utils::SHOWDEFAULTS},
3576 };
3577 use spenso::shadowing::symbolica_utils::SpensoPrintSettings;
3578 use symbolica::atom::{Atom, AtomCore};
3579 use tempfile::tempdir;
3580
3581 use crate::commands::{
3582 display::Display,
3583 save::SaveState,
3584 set::{ProcessSetArgs, Set, SetArgs},
3585 };
3586
3587 use super::*;
3588
3589 const STATE_LOAD_ORDER_CHILD: &str = "GAMMALOOP_STATE_LOAD_ORDER_CHILD";
3590 const STATE_LOAD_ORDER_TEST: &str =
3591 "state::tests::state_load_preserves_ufo_symbols_and_model_values";
3592
3593 #[cfg(target_os = "linux")]
3594 #[test]
3595 fn generation_ram_monitor_parses_resident_bytes() {
3596 assert_eq!(
3597 GenerationMonitor::parse_resident_memory_bytes(
3598 "Name:\tgammaloop\nVmSize:\t9999 kB\nVmRSS:\t 123 kB\nRssAnon:\t100 kB\n"
3599 ),
3600 Some(123 * 1024)
3601 );
3602 assert_eq!(
3603 GenerationMonitor::parse_resident_memory_bytes("VmRSS: 0 kB\n"),
3604 Some(0)
3605 );
3606 for status in [
3607 "VmSize: 123 kB\n",
3608 "VmRSS: invalid kB\n",
3609 "VmRSS: 123\n",
3610 "VmRSS: 123 MB\n",
3611 "VmRSS: 123 kB extra\n",
3612 "VmRSS: 18446744073709551615 kB\n",
3613 ] {
3614 assert_eq!(GenerationMonitor::parse_resident_memory_bytes(status), None);
3615 }
3616 }
3617
3618 #[cfg(target_os = "linux")]
3619 #[test]
3620 fn generation_ram_monitor_records_short_generation() {
3621 let mut monitor = GenerationMonitor::start().unwrap();
3622 let initial_memory = monitor.current_ram_bytes().load(Ordering::Relaxed);
3623 assert!(initial_memory > 0);
3624 let peak_memory = monitor.finish();
3625 assert!(peak_memory >= initial_memory);
3626 assert!(peak_memory >= monitor.current_ram_bytes().load(Ordering::Relaxed));
3627 assert!(monitor.stop_requested.load(Ordering::Relaxed));
3628 assert!(monitor.handle.is_none());
3629 assert_eq!(monitor.finish(), peak_memory);
3630 }
3631
3632 #[test]
3633 fn generation_ram_monitor_wakes_on_finish() {
3634 let stop_requested = Arc::new(AtomicBool::new(false));
3635 let stop_for_thread = Arc::clone(&stop_requested);
3636 let (ready, ready_receiver) = std::sync::mpsc::channel();
3637 let handle = thread::spawn(move || {
3638 ready.send(()).unwrap();
3639 loop {
3640 thread::park();
3641 if stop_for_thread.load(Ordering::Relaxed) {
3642 break;
3643 }
3644 }
3645 });
3646 let mut monitor = GenerationMonitor {
3647 peak_ram_bytes: Arc::new(AtomicU64::new(123)),
3648 current_ram_bytes: Arc::new(AtomicU64::new(123)),
3649 stop_requested,
3650 handle: Some(handle),
3651 };
3652 ready_receiver.recv().unwrap();
3653 assert_eq!(monitor.finish(), 123);
3654 }
3655
3656 #[test]
3657 fn state_load_preserves_ufo_symbols_and_model_values() {
3658 if let Some(state_path) = std::env::var_os(STATE_LOAD_ORDER_CHILD) {
3659 for _ in 0..2 {
3660 let state = State::load(PathBuf::from(&state_path), None, None).unwrap();
3661 let ProcessCollection::Amplitudes(amplitudes) =
3662 &state.process_list.processes[0].collection
3663 else {
3664 panic!("expected an amplitude");
3665 };
3666 let mut params = amplitudes["default"].graphs[0].graph.param_builder.clone();
3667 let saved_values = params.values.clone();
3668 assert_eq!(saved_values.len(), 3);
3669 assert!(!params.pairs.model_parameters.params.is_empty());
3670 params.update_model_values(&state.model).unwrap();
3671 for (actual, expected) in params.values.iter().zip(&saved_values) {
3672 for (actual, expected) in actual.iter().zip(expected) {
3673 for (actual, expected) in
3674 [(actual.re.0, expected.re.0), (actual.im.0, expected.im.0)]
3675 {
3676 assert!(
3677 (actual - expected).abs() <= 1e-13 * expected.abs().max(1.0),
3678 "model value changed after reload: {actual} != {expected}"
3679 );
3680 }
3681 }
3682 }
3683 let mut model = state.model.clone();
3684 model.get_parameter_mut("aS").unwrap().value = Some(Complex::new_re(F(1.0)));
3685 model.recompute_dependents().unwrap();
3686 params.update_model_values(&model).unwrap();
3687 let coupling = model.get_coupling("GC_12");
3688 let position = params.pairs.model_parameters.value_range.start
3689 + params
3690 .pairs
3691 .model_parameters
3692 .params
3693 .iter()
3694 .position(|param| *param == Atom::from(coupling.name))
3695 .unwrap();
3696 for (value_index, values) in params.values.iter().enumerate() {
3697 assert_eq!(
3698 values[position * (value_index + 1)],
3699 coupling.value.unwrap().map(F)
3700 );
3701 }
3702 for name in ["G", "orphan_ufo_printer"] {
3703 let symbol = UFOSymbol::from(name);
3704 assert!(symbol.0.get_print_function().is_some(), "{name}");
3705 let rendered = Atom::from(symbol)
3706 .printer(SpensoPrintSettings::typst_options())
3707 .to_string();
3708 assert_eq!(rendered, format!("\"{name}\""));
3709 }
3710 for label in ["Dd0E4", "1⦻2"] {
3711 let symbol = symbolica::get_symbol!(&format!("linnet::S_{label}")).unwrap();
3712 assert!(symbol.get_print_function().is_some(), "{label}");
3713 assert_eq!(
3714 Atom::var(symbol)
3715 .printer(SpensoPrintSettings::typst_options())
3716 .to_string(),
3717 format!("#S(g,\"{label}\")")
3718 );
3719 }
3720 }
3721 return;
3722 }
3723
3724 let temp = tempdir().unwrap();
3725 let mut state = State::new(temp.path(), None);
3726 let _ = UFOSymbol::from("GC_12");
3728 state.model = load_generic_model("sm");
3729 state.model_parameters = InputParamCard::default_from_model(&state.model);
3730 let mut graphs = Graph::from_string(
3731 "digraph model_values { node [num=1]; edge [num=1, mass=0]; A -> A; }",
3732 &state.model,
3733 )
3734 .unwrap();
3735 graphs[0].param_builder.initialize_duals(3);
3736 state
3737 .import_graphs(
3738 graphs,
3739 GraphImportOptions {
3740 process_name: Some("model_values".into()),
3741 process_id: None,
3742 process_definition: None,
3743 integrand_name: Some("default".into()),
3744 overwrite: false,
3745 append: false,
3746 },
3747 )
3748 .unwrap();
3749 let _ = UFOSymbol::from("orphan_ufo_printer");
3752 for label in ["Dd0E4", "1⦻2"] {
3753 let _ = SuBitGraph::symbol_from_label(label.to_owned());
3754 }
3755 state.save(temp.path(), true, false).unwrap();
3756 let manifest = load_state_manifest(temp.path()).unwrap();
3757 assert!(manifest
3758 .ufo_print_symbols
3759 .contains(&"orphan_ufo_printer".to_owned()));
3760 assert!(!fs::read_to_string(temp.path().join("model.json"))
3761 .unwrap()
3762 .contains("orphan_ufo_printer"));
3763
3764 let output = Command::new(std::env::current_exe().unwrap())
3765 .arg(STATE_LOAD_ORDER_TEST)
3766 .arg("--exact")
3767 .arg("--nocapture")
3768 .env(STATE_LOAD_ORDER_CHILD, temp.path())
3769 .env_remove("GL_ALL_LOG_FILTER")
3770 .env("GL_DISPLAY_FILTER", "warn")
3771 .output()
3772 .unwrap();
3773 let transcript = format!(
3774 "stdout:\n{}\nstderr:\n{}",
3775 String::from_utf8_lossy(&output.stdout),
3776 String::from_utf8_lossy(&output.stderr),
3777 );
3778 assert!(
3779 output.status.success(),
3780 "child failed: {}\n{transcript}",
3781 output.status
3782 );
3783 assert!(
3784 !transcript.contains("Imported symbol UFO::")
3785 && !transcript.contains("Imported symbol linnet::S_"),
3786 "{transcript}"
3787 );
3788 }
3789
3790 fn build_generated_scalar_bubble_state_with_external_backend() -> State {
3791 test_initialise().expect("test initialisation should succeed");
3792 let mut state = State::new_test();
3793 state.model = load_generic_model("scalars");
3794 state.model_parameters = InputParamCard::default_from_model(&state.model);
3795
3796 let graph_path =
3797 crate::test_workspace_root().join("tests/resources/graphs/scalar_bubble.dot");
3798 let graphs = Graph::from_path(&graph_path, &state.model)
3799 .expect("scalar bubble graph fixture should load");
3800
3801 state
3802 .import_graphs(
3803 graphs,
3804 GraphImportOptions {
3805 process_name: Some("scalar_bubble".to_string()),
3806 process_id: None,
3807 process_definition: None,
3808 integrand_name: Some("default".to_string()),
3809 overwrite: false,
3810 append: false,
3811 },
3812 )
3813 .expect("graph import should succeed");
3814
3815 let mut cli_settings = CLISettings::default();
3816 cli_settings.global.generation.evaluator.compile = true;
3817 cli_settings.global.generation.compile.compilation_mode = CompilationMode::Assembly;
3818
3819 let runtime_defaults = RuntimeSettings::default();
3820 state
3821 .generate_integrands(&cli_settings.global, (&runtime_defaults).into())
3822 .expect("integrand generation should succeed");
3823
3824 state
3825 }
3826
3827 #[test]
3828 fn named_generation_records_common_settings_for_persistent_uv_exports() -> Result<()> {
3829 use gammalooprs::{
3830 cff::{expression::AllOrientations, surface::GammaLoopSurfaceCache},
3831 uv::{export::UVForestExportSettings, UVOrchestrator},
3832 };
3833
3834 test_initialise()?;
3835 for (folder, source) in [
3836 (
3837 "amplitudes",
3838 include_str!("../../../tests/resources/graphs/scalar_bubble.dot"),
3839 ),
3840 (
3841 "cross_sections",
3842 include_str!(
3843 "../../../tests/resources/graphs/mass_approach_scalar_self_energy.dot"
3844 ),
3845 ),
3846 ] {
3847 let temp = tempdir()?;
3848 let mut state = State::new_test();
3849 state.model = load_generic_model("scalars");
3850 state.model_parameters = InputParamCard::default_from_model(&state.model);
3851 let mut graphs = Graph::from_string(source, &state.model)?;
3852 graphs.truncate(1);
3853 let graph_name = graphs[0].name.clone();
3854 for name in ["first", "second"] {
3855 state.import_graphs(
3856 graphs.clone(),
3857 GraphImportOptions {
3858 process_name: Some("named_export".into()),
3859 process_id: None,
3860 process_definition: None,
3861 integrand_name: Some(name.into()),
3862 overwrite: false,
3863 append: false,
3864 },
3865 )?;
3866 }
3867 let mut settings = GlobalSettings::default();
3868 settings.n_cores.generate = 1;
3869 settings.generation.uv.orchestrator = UVOrchestrator::HedgePoset;
3870 assert!(!settings.generation.evaluator.compile);
3871 assert!(!settings.generation.evaluator.store_atom);
3872 assert!(settings.generation.uv.subtract_uv);
3873 assert!(settings.generation.uv.generate_integrated);
3874 assert!(settings.generation.uv.softct);
3875 assert!(settings.generation.threshold_subtraction.enable_thresholds);
3876 let runtime = RuntimeSettings::default();
3877 assert!(state.process_list.processes[0].settings_history.is_none());
3878 for name in ["first", "second"] {
3879 state.generate_integrand(&settings, (&runtime).into(), 0, Some(name.into()))?;
3882 assert_eq!(
3883 state.process_list.processes[0].settings_history.as_ref(),
3884 Some(&settings)
3885 );
3886 state.process_list.processes[0]
3887 .get_integrand(name)?
3888 .require_generated()?;
3889 }
3890
3891 let source_expressions = |state: &State| {
3892 ["first", "second"].map(|name| {
3893 let source = match &state.process_list.processes[0].collection {
3894 ProcessCollection::Amplitudes(amplitudes) => amplitudes[name].graphs[0]
3895 .derived_data
3896 .cff_expression
3897 .as_ref()
3898 .unwrap(),
3899 ProcessCollection::CrossSections(cross_sections) => cross_sections[name]
3900 .supergraphs[0]
3901 .derived_data
3902 .global_cff_expression
3903 .as_ref()
3904 .unwrap(),
3905 };
3906 source
3907 .expression
3908 .to_atom(AllOrientations)
3909 .replace_multiple(source.expression.surfaces.get_all_replacements_gs(&[]))
3910 })
3911 };
3912
3913 let mut changed = settings.clone();
3916 changed.generation.explicit_orientation_sum_only = true;
3917 for missing_history in [false, true] {
3918 if missing_history {
3919 state.process_list.processes[0].settings_history = None;
3920 }
3921 let before = source_expressions(&state);
3922 let history_before = state.process_list.processes[0].settings_history.clone();
3923 let error = state
3924 .generate_integrand(
3925 if missing_history { &settings } else { &changed },
3926 (&runtime).into(),
3927 0,
3928 Some("first".into()),
3929 )
3930 .unwrap_err();
3931 assert!(
3932 error.to_string().contains("regenerate the whole process"),
3933 "{error:?}"
3934 );
3935 assert!(
3936 source_expressions(&state)
3937 .into_iter()
3938 .zip(before)
3939 .all(|(actual, expected)| (actual - expected).expand().is_zero()),
3940 "named-generation source expressions changed after rejected generation"
3941 );
3942 assert_eq!(
3943 state.process_list.processes[0].settings_history,
3944 history_before
3945 );
3946 for name in ["first", "second"] {
3947 state.process_list.processes[0]
3948 .get_integrand(name)?
3949 .require_generated()?;
3950 }
3951 state.process_list.processes[0].settings_history = Some(settings.clone());
3952 }
3953
3954 let source_before = source_expressions(&state);
3955 let mut exported_expression: Option<Atom> = None;
3956 for loaded in [false, true] {
3957 let process = &state.process_list.processes[0];
3958 assert_eq!(process.settings_history.as_ref(), Some(&settings));
3959 assert!(
3960 source_expressions(&state)
3961 .into_iter()
3962 .zip(&source_before)
3963 .all(|(actual, expected)| (actual - expected).expand().is_zero()),
3964 "named-generation source expressions changed after persistence"
3965 );
3966 for computed in [false, true] {
3967 let output = temp.path().join(format!("export-{loaded}-{computed}"));
3968 state.process_list.export_uv_forests(
3969 &output,
3970 0,
3971 "first",
3972 &[0],
3973 &UVForestExportSettings { computed },
3974 )?;
3975 let graph_dir = output
3976 .join("processes")
3977 .join(folder)
3978 .join("named_export/first");
3979 assert!(graph_dir.join(format!("{graph_name}.forest.dot")).is_file());
3980 let nodes = graph_dir.join(format!("{graph_name}_nodes"));
3981 if computed {
3982 let mut exported_graph = false;
3983 let mut expression = Atom::Zero;
3984 for forest in fs::read_dir(&nodes)? {
3985 for node in fs::read_dir(forest?.path())? {
3986 let node = node?;
3987 let dot = fs::read_to_string(node.path())?;
3988 assert!(dot.contains("forest_residue_index"));
3989 for graph in Graph::from_string(&dot, &state.model)? {
3990 exported_graph = true;
3991 expression += graph.full_numerator_atom();
3992 }
3993 }
3994 }
3995 assert!(exported_graph, "computed export must contain a graph");
3996 if let Some(expected) = &exported_expression {
3999 assert!(
4000 (&expression - expected).expand().is_zero(),
4001 "computed UV export changed after persistence"
4002 );
4003 } else {
4004 exported_expression = Some(expression);
4005 }
4006 } else {
4007 assert!(!nodes.exists());
4008 }
4009 }
4010 if !loaded {
4011 let saved = temp.path().join("saved");
4012 state.save(&saved, true, false)?;
4013 let history_path = saved
4014 .join("processes")
4015 .join(folder)
4016 .join("named_export/settings_history.toml");
4017 let history: GlobalSettings =
4018 toml::from_str(&fs::read_to_string(history_path)?)?;
4019 assert_eq!(history, settings);
4020 state = State::load(saved, None, None)?;
4021 }
4022 }
4023 }
4024 Ok(())
4025 }
4026
4027 #[test]
4028 fn aggregate_generation_progress_tracks_counts_and_timings() {
4029 let current_ram_bytes = Arc::new(AtomicU64::new(256 * 1024 * 1024));
4030 let peak_ram_bytes = Arc::new(AtomicU64::new(512 * 1024 * 1024));
4031 let reporter =
4032 AggregateGenerationProgressReporter::new_hidden(current_ram_bytes, peak_ram_bytes);
4033
4034 reporter.begin_phase(
4035 GenerationProgressPhase::GraphPreprocessing,
4036 GenerationProcessKind::CrossSection,
4037 "proc",
4038 "itg",
4039 2,
4040 None,
4041 );
4042 reporter.graph_started(GenerationProcessKind::CrossSection, "itg", "GL01", None);
4043 reporter.cuts_discovered("itg", "GL01", 4, 2);
4044 {
4045 let state = reporter
4046 .state
4047 .lock()
4048 .expect("aggregate generation progress state mutex is poisoned");
4049 assert_eq!(state.done_graphs, 0);
4050 assert_eq!(state.discovered_st_cuts, 4);
4051 assert_eq!(state.discovered_valid_cuts, 2);
4052 assert_eq!(
4053 AggregateGenerationProgressReporter::progress_units(&state),
4054 (0, 2)
4055 );
4056 assert_eq!(
4057 AggregateGenerationProgressReporter::graph_progress_counts(&state),
4058 (0, 1, 2)
4059 );
4060 }
4061 reporter.graph_finished(
4062 GenerationProcessKind::CrossSection,
4063 "itg",
4064 "GL01",
4065 &GraphGenerationStats {
4066 total_time: Duration::from_secs(3),
4067 ..GraphGenerationStats::default()
4068 },
4069 None,
4070 );
4071 {
4072 let state = reporter
4073 .state
4074 .lock()
4075 .expect("aggregate generation progress state mutex is poisoned");
4076 assert_eq!(state.done_graphs, 1);
4077 assert_eq!(state.stats.total_time, Duration::from_secs(3));
4078 assert_eq!(
4079 AggregateGenerationProgressReporter::graph_progress_counts(&state),
4080 (1, 0, 2)
4081 );
4082 }
4083
4084 reporter.begin_phase(
4085 GenerationProgressPhase::GraphGeneration,
4086 GenerationProcessKind::CrossSection,
4087 "proc",
4088 "itg",
4089 2,
4090 Some(3),
4091 );
4092 reporter.graph_started(GenerationProcessKind::CrossSection, "itg", "GL01", Some(1));
4093 reporter.graph_started(GenerationProcessKind::CrossSection, "itg", "GL02", Some(2));
4094 {
4095 let state = reporter
4096 .state
4097 .lock()
4098 .expect("aggregate generation progress state mutex is poisoned");
4099 assert_eq!(
4100 AggregateGenerationProgressReporter::graph_progress_counts(&state),
4101 (0, 2, 2)
4102 );
4103 }
4104 reporter.graph_finished(
4105 GenerationProcessKind::CrossSection,
4106 "itg",
4107 "GL01",
4108 &GraphGenerationStats {
4109 evaluator_count: 2,
4110 total_time: Duration::from_secs(4),
4111 evaluator_spenso_time: Duration::from_secs(1),
4112 evaluator_symbolica_time: Duration::from_secs(1),
4113 evaluator_compile_time: Duration::ZERO,
4114 ..GraphGenerationStats::default()
4115 },
4116 None,
4117 );
4118 reporter.cut_finished("itg", "GL01", 1);
4119 reporter.graph_finished(
4120 GenerationProcessKind::CrossSection,
4121 "itg",
4122 "GL02",
4123 &GraphGenerationStats {
4124 evaluator_count: 3,
4125 total_time: Duration::from_secs(6),
4126 evaluator_spenso_time: Duration::from_secs(2),
4127 evaluator_symbolica_time: Duration::ZERO,
4128 evaluator_compile_time: Duration::ZERO,
4129 ..GraphGenerationStats::default()
4130 },
4131 None,
4132 );
4133 reporter.cut_finished("itg", "GL02", 2);
4134
4135 {
4136 let state = reporter
4137 .state
4138 .lock()
4139 .expect("aggregate generation progress state mutex is poisoned");
4140 assert_eq!(state.done_graphs, 2);
4141 assert_eq!(state.done_cuts, 3);
4142 assert!(state.active_graphs.is_empty());
4143 assert_eq!(state.last_graph.as_deref(), Some("GL02"));
4144 assert_eq!(state.stats.evaluator_count, 5);
4145 assert_eq!(state.stats.total_time, Duration::from_secs(10));
4146 assert_eq!(state.stats.expression_build_time(), Duration::from_secs(6));
4147 assert_eq!(state.stats.evaluator_spenso_time, Duration::from_secs(3));
4148 assert_eq!(state.stats.evaluator_symbolica_time, Duration::from_secs(1));
4149 assert_eq!(
4150 AggregateGenerationProgressReporter::progress_units(&state),
4151 (5, 5)
4152 );
4153 assert_eq!(
4154 AggregateGenerationProgressReporter::graph_progress_counts(&state),
4155 (2, 0, 2)
4156 );
4157 }
4158
4159 reporter.backend_started(GenerationProcessKind::CrossSection, "itg", 2);
4160 reporter.backend_finished(
4161 GenerationProcessKind::CrossSection,
4162 "itg",
4163 Duration::from_secs(2),
4164 );
4165
4166 let state = reporter
4167 .state
4168 .lock()
4169 .expect("aggregate generation progress state mutex is poisoned");
4170 assert_eq!(state.phase, Some(GenerationProgressPhase::Backend));
4171 assert_eq!(state.done_graphs, 2);
4172 assert_eq!(state.stats.total_time, Duration::from_secs(12));
4173 assert_eq!(state.stats.evaluator_compile_time, Duration::from_secs(2));
4174 }
4175
4176 #[test]
4177 fn aggregate_generation_progress_tracks_amplitude_preprocessing() {
4178 let reporter = AggregateGenerationProgressReporter::new_hidden(
4179 Arc::new(AtomicU64::new(0)),
4180 Arc::new(AtomicU64::new(0)),
4181 );
4182
4183 reporter.begin_phase(
4184 GenerationProgressPhase::GraphPreprocessing,
4185 GenerationProcessKind::Amplitude,
4186 "proc",
4187 "amp",
4188 3,
4189 None,
4190 );
4191 reporter.graph_started(GenerationProcessKind::Amplitude, "amp", "GL01", None);
4192 reporter.graph_started(GenerationProcessKind::Amplitude, "amp", "GL02", None);
4193
4194 {
4195 let state = reporter
4196 .state
4197 .lock()
4198 .expect("aggregate generation progress state mutex is poisoned");
4199 assert_eq!(state.kind, Some(GenerationProcessKind::Amplitude));
4200 assert_eq!(state.process, "proc");
4201 assert_eq!(state.integrand, "amp");
4202 assert_eq!(
4203 AggregateGenerationProgressReporter::progress_units(&state),
4204 (0, 3)
4205 );
4206 assert_eq!(
4207 AggregateGenerationProgressReporter::graph_progress_counts(&state),
4208 (0, 2, 3)
4209 );
4210 }
4211
4212 reporter.graph_finished(
4213 GenerationProcessKind::Amplitude,
4214 "amp",
4215 "GL01",
4216 &GraphGenerationStats::default(),
4217 None,
4218 );
4219
4220 let state = reporter
4221 .state
4222 .lock()
4223 .expect("aggregate generation progress state mutex is poisoned");
4224 assert_eq!(
4225 AggregateGenerationProgressReporter::progress_units(&state),
4226 (1, 3)
4227 );
4228 assert_eq!(
4229 AggregateGenerationProgressReporter::graph_progress_counts(&state),
4230 (1, 1, 3)
4231 );
4232 assert_eq!(state.last_graph.as_deref(), Some("GL01"));
4233 }
4234
4235 #[test]
4236 fn aggregate_generation_progress_formats_memory_and_time_shares() {
4237 assert_eq!(
4238 AggregateGenerationProgressReporter::progress_memory(512 * 1024 * 1024),
4239 " 512 MiB"
4240 );
4241 assert_eq!(
4242 AggregateGenerationProgressReporter::progress_memory(5 * 1024 * 1024 * 1024),
4243 " 5.00 GiB"
4244 );
4245
4246 assert_eq!(
4247 AggregateGenerationProgressReporter::progress_percent(0.0),
4248 "0%"
4249 );
4250 assert_eq!(
4251 AggregateGenerationProgressReporter::progress_percent(0.009),
4252 "0%"
4253 );
4254 assert_eq!(
4255 AggregateGenerationProgressReporter::progress_percent(0.024),
4256 "0.024%"
4257 );
4258 assert_eq!(
4259 AggregateGenerationProgressReporter::progress_percent(0.24),
4260 "0.24%"
4261 );
4262 assert_eq!(
4263 AggregateGenerationProgressReporter::progress_percent(2.4),
4264 "2.4%"
4265 );
4266 assert_eq!(
4267 AggregateGenerationProgressReporter::progress_percent(54.0),
4268 "54%"
4269 );
4270
4271 assert_eq!(
4272 AggregateGenerationProgressReporter::progress_time_share(
4273 Duration::ZERO,
4274 Duration::ZERO,
4275 ),
4276 "--"
4277 );
4278 assert_eq!(
4279 AggregateGenerationProgressReporter::progress_time_share(
4280 Duration::ZERO,
4281 Duration::from_secs(4),
4282 ),
4283 "0%"
4284 );
4285 assert_eq!(
4286 AggregateGenerationProgressReporter::progress_time_share(
4287 Duration::from_secs(1),
4288 Duration::from_secs(4),
4289 ),
4290 "25%"
4291 );
4292 }
4293
4294 fn build_scalar_bubble_diagram_state() -> State {
4295 test_initialise().expect("test initialisation should succeed");
4296 let mut state = State::new_test();
4297 state.model = load_generic_model("scalars");
4298 state.model_parameters = InputParamCard::default_from_model(&state.model);
4299
4300 let graph_path =
4301 crate::test_workspace_root().join("tests/resources/graphs/scalar_bubble.dot");
4302 let graphs = Graph::from_path(&graph_path, &state.model)
4303 .expect("scalar bubble graph fixture should load");
4304 state
4305 .import_graphs(
4306 graphs,
4307 GraphImportOptions {
4308 process_name: Some("scalar_bubble".to_string()),
4309 process_id: None,
4310 process_definition: None,
4311 integrand_name: Some("default".to_string()),
4312 overwrite: false,
4313 append: false,
4314 },
4315 )
4316 .expect("graph import should succeed");
4317 state
4318 }
4319
4320 fn scalar_bubble_master_graph_name(state: &State, integrand_name: &str) -> String {
4321 let process = &state.process_list.processes[0];
4322 match &process.collection {
4323 ProcessCollection::Amplitudes(amplitudes) => amplitudes
4324 .get(integrand_name)
4325 .expect("amplitude should exist")
4326 .graphs[0]
4327 .graph
4328 .name
4329 .clone(),
4330 ProcessCollection::CrossSections(cross_sections) => cross_sections
4331 .get(integrand_name)
4332 .expect("cross section should exist")
4333 .supergraphs[0]
4334 .graph
4335 .name
4336 .clone(),
4337 }
4338 }
4339
4340 #[test]
4341 fn select_copy_mode_creates_pregenerated_target_without_mutating_source() {
4342 let _guard = crate::LOG_TEST_MUTEX
4343 .lock()
4344 .unwrap_or_else(|err| err.into_inner());
4345 let mut state = build_scalar_bubble_diagram_state();
4346 let temp = tempdir().unwrap();
4347 let graph_name = scalar_bubble_master_graph_name(&state, "default");
4348 let selection = GraphGroupSelectionSpec::new().with_master_graph_names(vec![graph_name]);
4349 let target = GraphGroupSelectionTarget::copy(None, Some("selected".to_string()), false);
4350
4351 let selected = state
4352 .select_integrand_graph_groups(
4353 Some(&ProcessRef::Name("scalar_bubble".to_string())),
4354 Some(&"default".to_string()),
4355 &selection,
4356 &target,
4357 GraphGroupSelectionContext::new(&GenerationSettings::default(), temp.path(), false),
4358 )
4359 .unwrap();
4360
4361 assert!(selected.copied_to_output);
4362 assert_eq!(selected.process_id, 0);
4363 assert_eq!(selected.integrand_name, "selected");
4364 let process = &state.process_list.processes[0];
4365 assert!(process
4366 .collection
4367 .get_integrand_names()
4368 .contains(&"default"));
4369 assert!(process
4370 .collection
4371 .get_integrand_names()
4372 .contains(&"selected"));
4373 match &process.collection {
4374 ProcessCollection::Amplitudes(amplitudes) => {
4375 assert!(amplitudes["default"].integrand.is_none());
4376 assert!(amplitudes["selected"].integrand.is_none());
4377 assert_eq!(amplitudes["default"].graphs.len(), 1);
4378 assert_eq!(amplitudes["selected"].graphs.len(), 1);
4379 }
4380 ProcessCollection::CrossSections(cross_sections) => {
4381 assert!(cross_sections["default"].integrand.is_none());
4382 assert!(cross_sections["selected"].integrand.is_none());
4383 assert_eq!(cross_sections["default"].supergraphs.len(), 1);
4384 assert_eq!(cross_sections["selected"].supergraphs.len(), 1);
4385 }
4386 }
4387 }
4388
4389 #[test]
4390 fn select_copy_mode_rejects_and_clears_existing_target_integrand() {
4391 let _guard = crate::LOG_TEST_MUTEX
4392 .lock()
4393 .unwrap_or_else(|err| err.into_inner());
4394 let mut state = build_scalar_bubble_diagram_state();
4395 let temp = tempdir().unwrap();
4396 let graph_name = scalar_bubble_master_graph_name(&state, "default");
4397 let selection = GraphGroupSelectionSpec::new().with_master_graph_names(vec![graph_name]);
4398 let target = GraphGroupSelectionTarget::copy(None, Some("selected".to_string()), false);
4399
4400 state
4401 .select_integrand_graph_groups(
4402 Some(&ProcessRef::Name("scalar_bubble".to_string())),
4403 Some(&"default".to_string()),
4404 &selection,
4405 &target,
4406 GraphGroupSelectionContext::new(&GenerationSettings::default(), temp.path(), false),
4407 )
4408 .unwrap();
4409 let err = state
4410 .select_integrand_graph_groups(
4411 Some(&ProcessRef::Name("scalar_bubble".to_string())),
4412 Some(&"default".to_string()),
4413 &selection,
4414 &target,
4415 GraphGroupSelectionContext::new(&GenerationSettings::default(), temp.path(), false),
4416 )
4417 .unwrap_err();
4418 assert!(format!("{err}").contains("already exists"));
4419
4420 let process = &state.process_list.processes[0];
4421 let kind_folder = match &process.collection {
4422 ProcessCollection::Amplitudes(_) => "amplitudes",
4423 ProcessCollection::CrossSections(_) => "cross_sections",
4424 };
4425 let stale_integrand_folder = temp
4426 .path()
4427 .join("processes")
4428 .join(kind_folder)
4429 .join("scalar_bubble")
4430 .join("selected");
4431 fs::create_dir_all(stale_integrand_folder.join("integrand")).unwrap();
4432 state.generation_summaries.insert(
4433 IntegrandGenerationSummaryKey {
4434 process_id: 0,
4435 integrand_name: "selected".to_string(),
4436 },
4437 IntegrandGenerationSummary {
4438 peak_ram_bytes: 1,
4439 reports: Vec::new(),
4440 },
4441 );
4442
4443 let clear_target =
4444 GraphGroupSelectionTarget::copy(None, Some("selected".to_string()), true);
4445 let selected = state
4446 .select_integrand_graph_groups(
4447 Some(&ProcessRef::Name("scalar_bubble".to_string())),
4448 Some(&"default".to_string()),
4449 &selection,
4450 &clear_target,
4451 GraphGroupSelectionContext::new(&GenerationSettings::default(), temp.path(), false),
4452 )
4453 .unwrap();
4454 assert!(selected.replaced_existing_target);
4455 assert!(selected.removed_target_artifacts);
4456 assert!(!stale_integrand_folder.exists());
4457 assert!(!state
4458 .generation_summaries
4459 .contains_key(&IntegrandGenerationSummaryKey {
4460 process_id: 0,
4461 integrand_name: "selected".to_string(),
4462 }));
4463
4464 let err = state
4465 .select_integrand_graph_groups(
4466 Some(&ProcessRef::Name("scalar_bubble".to_string())),
4467 Some(&"default".to_string()),
4468 &selection,
4469 &clear_target,
4470 GraphGroupSelectionContext::new(&GenerationSettings::default(), temp.path(), true),
4471 )
4472 .unwrap_err();
4473 assert!(format!("{err}").contains("--read-only-state"));
4474 }
4475
4476 #[test]
4477 fn select_amplitude_graphs_mode_rejects_amplitude_integrands() {
4478 let _guard = crate::LOG_TEST_MUTEX
4479 .lock()
4480 .unwrap_or_else(|err| err.into_inner());
4481 let mut state = build_scalar_bubble_diagram_state();
4482 let temp = tempdir().unwrap();
4483 let graph_name = scalar_bubble_master_graph_name(&state, "default");
4484 let selection = GraphGroupSelectionSpec::new()
4485 .with_mode(GraphGroupSelectionMode::CrossSectionAmplitudeGraphs)
4486 .with_master_graph_names(vec![graph_name]);
4487 let target = GraphGroupSelectionTarget::in_place();
4488
4489 let err = state
4490 .select_integrand_graph_groups(
4491 Some(&ProcessRef::Name("scalar_bubble".to_string())),
4492 Some(&"default".to_string()),
4493 &selection,
4494 &target,
4495 GraphGroupSelectionContext::new(&GenerationSettings::default(), temp.path(), false),
4496 )
4497 .unwrap_err();
4498
4499 assert!(format!("{err}").contains("--amplitude-graphs"));
4500 }
4501
4502 #[test]
4503 fn select_raised_cut_filters_reject_amplitude_integrands() {
4504 let _guard = crate::LOG_TEST_MUTEX
4505 .lock()
4506 .unwrap_or_else(|err| err.into_inner());
4507 let mut state = build_scalar_bubble_diagram_state();
4508 let temp = tempdir().unwrap();
4509 let selection = GraphGroupSelectionSpec::new().with_raised_cut_signatures(
4510 SelectionPolarity::With,
4511 RaisedPropagatorScope::All,
4512 vec![RaisedPropagatorSignature::from_str("[]").unwrap()],
4513 );
4514 let target = GraphGroupSelectionTarget::in_place();
4515
4516 let err = state
4517 .select_integrand_graph_groups(
4518 Some(&ProcessRef::Name("scalar_bubble".to_string())),
4519 Some(&"default".to_string()),
4520 &selection,
4521 &target,
4522 GraphGroupSelectionContext::new(&GenerationSettings::default(), temp.path(), false),
4523 )
4524 .unwrap_err();
4525
4526 assert!(format!("{err}").contains("cross-section integrands"));
4527 }
4528
4529 #[test]
4530 fn test_run_history() {
4531 use crate::state::RunHistory;
4532 let mut run_history: RunHistory = Default::default();
4534 let kinematics_settings = KinematicsSettings {
4535 e_cm: 100.0,
4536 externals: Externals::Constant {
4537 momenta: vec![
4538 ExternalMomenta::Independent([F(1.), F(2.), F(3.), F(4.)]),
4539 ExternalMomenta::Dependent(Dep::Dep),
4540 ],
4541 improvement_settings: PhaseSpaceImprovementSettings::default(),
4542 helicities: vec![Helicity::PLUS, Helicity::MINUS],
4543 f_64_cache: None,
4544 f_128_cache: None,
4545 arb_cache: Default::default(),
4546 },
4547 };
4548
4549 run_history.push(Commands::Set(Set::Global {
4550 input: SetArgs::Stored,
4551 }));
4552
4553 run_history.default_runtime_settings.kinematics = kinematics_settings;
4554 set_serialize_commands_as_strings(true);
4555 let toml = toml::to_string_pretty(&run_history).unwrap();
4556 println!("{}", toml);
4557 let deserialized: RunHistory = toml::from_str(&toml).unwrap();
4558 assert_eq!(run_history, deserialized);
4559 SHOWDEFAULTS.store(false, std::sync::atomic::Ordering::Relaxed);
4560
4561 run_history.to_file("test_path.toml", true).unwrap();
4562 let deserialized_from_file = RunHistory::from_file("test_path.toml", " ").unwrap();
4563 assert_eq!(run_history, deserialized_from_file);
4564 }
4565
4566 #[test]
4567 fn run_history_applies_default_runtime_before_commands() {
4568 let mut run_history = RunHistory {
4569 default_runtime_settings: toml::from_str(
4570 r#"
4571[sampling]
4572graphs = "monte_carlo"
4573orientations = "summed"
4574sampling_multichanneling = false
4575sampling_channels = "summed"
4576coordinate_system = "tropical"
4577mapping = "linear"
4578b = 1.0
4579"#,
4580 )
4581 .unwrap(),
4582 ..Default::default()
4583 };
4584
4585 let mut state = State::new_test();
4586 let mut cli_settings = CLISettings::default();
4587 let mut default_runtime_settings = RuntimeSettings::default();
4588
4589 let _ = run_history
4590 .run(&mut state, &mut cli_settings, &mut default_runtime_settings)
4591 .unwrap();
4592
4593 assert_eq!(
4594 default_runtime_settings.sampling,
4595 run_history.default_runtime_settings.sampling
4596 );
4597 }
4598
4599 #[test]
4600 fn test_command_history_serialization() {
4601 use super::{set_serialize_commands_as_strings, CommandHistory};
4602 use crate::commands::Commands;
4603
4604 let cmd_history = CommandHistory::new(Commands::Quit(SaveState::default()));
4606 assert_eq!(cmd_history.raw_string, None);
4607
4608 let cmd_history_with_raw =
4610 CommandHistory::new_with_raw(Commands::Quit(SaveState::default()), "quit".to_string());
4611 assert_eq!(cmd_history_with_raw.raw_string, Some("quit".to_string()));
4612
4613 set_serialize_commands_as_strings(false);
4615
4616 let json = serde_json::to_string(&cmd_history).unwrap();
4617 let deserialized_json: CommandHistory = serde_json::from_str(&json).unwrap();
4618 let toml = toml::to_string(&cmd_history).unwrap();
4619 let deserialized_toml: CommandHistory = toml::from_str(&toml).unwrap();
4620 assert_eq!(cmd_history, deserialized_json);
4621 assert_eq!(cmd_history, deserialized_toml);
4622
4623 set_serialize_commands_as_strings(true);
4625
4626 let json_string = serde_json::to_string_pretty(&cmd_history_with_raw).unwrap();
4627 assert!(json_string.contains("quit"));
4628
4629 set_serialize_commands_as_strings(false);
4631 }
4632
4633 #[test]
4634 fn test_command_history_toml_and_json_formats() {
4635 use super::{set_serialize_commands_as_strings, CommandHistory};
4636 use crate::commands::Commands;
4637
4638 let quit_cmd = CommandHistory::new(Commands::Quit(SaveState::default()));
4640 let quit_with_raw =
4641 CommandHistory::new_with_raw(Commands::Quit(SaveState::default()), "quit".to_string());
4642
4643 {
4645 set_serialize_commands_as_strings(false);
4647 let json = serde_json::to_string_pretty(&quit_cmd).unwrap();
4648 let deserialized: CommandHistory = serde_json::from_str(&json).unwrap();
4649 assert_eq!(quit_cmd, deserialized);
4650
4651 set_serialize_commands_as_strings(true);
4653 let json_string = serde_json::to_string_pretty(&quit_with_raw).unwrap();
4654 assert!(json_string.contains("quit"));
4655 let deserialized_string: CommandHistory = serde_json::from_str(&json_string).unwrap();
4656 assert_eq!(quit_with_raw, deserialized_string);
4657 }
4658
4659 {
4661 #[derive(serde::Serialize, serde::Deserialize)]
4662 struct CommandWrapper {
4663 command: CommandHistory,
4664 }
4665
4666 set_serialize_commands_as_strings(false);
4668 let wrapper = CommandWrapper {
4669 command: quit_cmd.clone(),
4670 };
4671 let toml = toml::to_string_pretty(&wrapper).unwrap();
4672 let deserialized_wrapper: CommandWrapper = toml::from_str(&toml).unwrap();
4673 assert_eq!(quit_cmd, deserialized_wrapper.command);
4674
4675 set_serialize_commands_as_strings(true);
4677 let wrapper_string = CommandWrapper {
4678 command: quit_with_raw.clone(),
4679 };
4680 let toml_string = toml::to_string_pretty(&wrapper_string).unwrap();
4681 assert!(toml_string.contains("quit"));
4682 let deserialized_string_wrapper: CommandWrapper = toml::from_str(&toml_string).unwrap();
4683 assert_eq!(quit_with_raw, deserialized_string_wrapper.command);
4684 }
4685
4686 {
4688 set_serialize_commands_as_strings(false);
4689
4690 let json = serde_json::to_string(&quit_cmd).unwrap();
4692 let json_value: serde_json::Value = serde_json::from_str(&json).unwrap();
4693 let from_json: CommandHistory = serde_json::from_value(json_value).unwrap();
4694 assert_eq!(quit_cmd, from_json);
4695 }
4696 set_serialize_commands_as_strings(false);
4698 }
4699
4700 #[test]
4701 fn run_history_push_with_raw_skips_quit_and_definition_commands() {
4702 use super::RunHistory;
4703 use crate::commands::{run::Run, StartCommandsBlock};
4704
4705 let mut run_history = RunHistory::default();
4706 run_history.push_with_raw(
4707 Commands::Run(Run {
4708 block_names: vec!["block_a".to_string()],
4709 defines: Vec::new(),
4710 commands: None,
4711 }),
4712 Some("run block_a".to_string()),
4713 );
4714 run_history.push_with_raw(
4715 Commands::Quit(SaveState::default()),
4716 Some("quit".to_string()),
4717 );
4718 run_history.push_with_raw(
4719 Commands::StartCommandsBlock(StartCommandsBlock {
4720 name: "block_a".to_string(),
4721 }),
4722 Some("start_commands_block block_a".to_string()),
4723 );
4724 run_history.push_with_raw(
4725 Commands::FinishCommandsBlock,
4726 Some("finish_commands_block".to_string()),
4727 );
4728
4729 assert_eq!(run_history.commands.len(), 1);
4730 assert_eq!(
4731 run_history.commands[0].raw_string.as_deref(),
4732 Some("run block_a")
4733 );
4734 }
4735
4736 #[test]
4737 fn run_history_filtered_for_save_preserves_command_blocks() {
4738 let mut run_history = RunHistory {
4739 command_blocks: vec![
4740 CommandsBlock {
4741 name: "generate".to_string(),
4742 commands: vec![CommandHistory::new_with_raw(
4743 Commands::Display(Display::Processes),
4744 "display processes".to_string(),
4745 )],
4746 },
4747 CommandsBlock {
4748 name: "integrate".to_string(),
4749 commands: vec![CommandHistory::new_with_raw(
4750 CommandHistory::from_raw_string("quit -o").unwrap().command,
4751 "quit -o".to_string(),
4752 )],
4753 },
4754 ],
4755 ..Default::default()
4756 };
4757 run_history.push_with_raw(
4758 CommandHistory::from_raw_string("display processes")
4759 .unwrap()
4760 .command,
4761 Some("display processes".to_string()),
4762 );
4763
4764 let filtered = run_history.filtered_for_save();
4765 set_serialize_commands_as_strings(true);
4766 let toml = toml::to_string_pretty(&filtered).unwrap();
4767 set_serialize_commands_as_strings(false);
4768
4769 assert_eq!(filtered.command_blocks.len(), 2);
4770 assert!(toml.contains("commands = ["));
4771 assert!(toml.contains("[[command_blocks]]"));
4772 assert!(toml.contains("quit -o"));
4773 }
4774
4775 #[test]
4776 fn command_history_parses_multiline_set_string() {
4777 let raw = "set process -p epem_a_tth -i LO string '[integrator]\nn_start = 1000\n'";
4778 let cmd = CommandHistory::from_raw_string(raw).unwrap();
4779 assert_eq!(cmd.raw_string.as_deref(), Some(raw));
4780
4781 match cmd.command {
4782 Commands::Set(Set::Process { input, .. }) => match input {
4783 ProcessSetArgs::String { string } => {
4784 assert_eq!(string, "[integrator]\nn_start = 1000\n");
4785 }
4786 other => panic!("Expected string set input, got {other:?}"),
4787 },
4788 other => panic!("Expected set process command, got {other:?}"),
4789 }
4790 }
4791
4792 #[test]
4793 fn command_history_parses_hash_process_refs() {
4794 for process in [
4797 ProcessRef::Id(0),
4798 ProcessRef::Id(12),
4799 ProcessRef::Name("12".into()),
4800 ProcessRef::Unqualified("scalar_bubble".into()),
4801 ] {
4802 let refs = BTreeMap::from([("process".to_string(), process)]);
4803 let toml = toml::to_string(&refs).unwrap();
4804 assert_eq!(
4805 toml::from_str::<BTreeMap<String, ProcessRef>>(&toml).unwrap(),
4806 refs
4807 );
4808 let json = serde_json::to_string(&refs).unwrap();
4809 assert_eq!(
4810 serde_json::from_str::<BTreeMap<String, ProcessRef>>(&json).unwrap(),
4811 refs
4812 );
4813 }
4814 for invalid in ["process = -1", "process = 1.5"] {
4815 assert!(toml::from_str::<BTreeMap<String, ProcessRef>>(invalid).is_err());
4816 }
4817 assert!(serde_json::from_str::<ProcessRef>("-1").is_err());
4818
4819 let cmd = CommandHistory::from_raw_string("display integrand -p #12").unwrap();
4820 match cmd.command {
4821 Commands::Display(Display::Integrands {
4822 process,
4823 integrand_name,
4824 graphs,
4825 categories,
4826 hide_non_existing_thresholds,
4827 show_threshold_functions,
4828 show_sampling,
4829 show_threshold_subtraction,
4830 }) => {
4831 assert_eq!(process, Some(ProcessRef::Id(12)));
4832 assert_eq!(integrand_name, None);
4833 assert!(graphs.is_empty());
4834 assert!(categories.is_empty());
4835 assert!(!hide_non_existing_thresholds);
4836 assert!(!show_threshold_functions);
4837 assert_eq!(show_sampling, None);
4838 assert_eq!(show_threshold_subtraction, None);
4839 }
4840 other => panic!("Expected display integrand command, got {other:?}"),
4841 }
4842 }
4843
4844 #[test]
4845 fn command_history_requires_explicit_generate_mode() {
4846 assert!(CommandHistory::from_raw_string("generate e+ e- > d d~").is_err());
4847 }
4848
4849 #[test]
4850 fn run_history_parses_triple_quoted_set_kv_command() {
4851 let toml = r#"
4852commands = [
4853 """set default-runtime kv kinematics.externals='{"type":"constant","data":{"momenta":[[1.0,2.0,3.0,4.0],[5.0,6.0,-7.0,-8.0]],"helicities":[1,1]}}'""",
4854]
4855
4856[default_runtime_settings.general]
4857integral_unit = "picobarn"
4858"#;
4859
4860 let run_history: RunHistory = toml::from_str(toml).unwrap();
4861 assert_eq!(run_history.commands.len(), 1);
4862
4863 let expected_cmd = r#"set default-runtime kv kinematics.externals='{"type":"constant","data":{"momenta":[[1.0,2.0,3.0,4.0],[5.0,6.0,-7.0,-8.0]],"helicities":[1,1]}}'"#;
4864 let command_history = &run_history.commands[0];
4865 assert_eq!(command_history.raw_string.as_deref(), Some(expected_cmd));
4866
4867 match &command_history.command {
4868 Commands::Set(Set::DefaultRuntime {
4869 input: SetArgs::Kv { pairs },
4870 }) => {
4871 assert_eq!(pairs.len(), 1);
4872 assert_eq!(pairs[0].key, "kinematics.externals");
4873 assert_eq!(
4874 pairs[0].value,
4875 r#"{"type":"constant","data":{"momenta":[[1.0,2.0,3.0,4.0],[5.0,6.0,-7.0,-8.0]],"helicities":[1,1]}}"#
4876 );
4877 }
4878 other => panic!("Expected set default-runtime kv command, got {other:?}"),
4879 }
4880
4881 assert_eq!(
4882 run_history.default_runtime_settings.general.integral_unit,
4883 gammalooprs::settings::runtime::IntegralUnit::Picobarn
4884 );
4885 }
4886
4887 #[test]
4888 fn run_history_load_preserves_command_blocks() {
4889 let temp = tempdir().unwrap();
4890 let run_path = temp.path().join("run.toml");
4891 fs::write(
4892 &run_path,
4893 r#"
4894[[command_blocks]]
4895name = "zeta"
4896commands = ["quit -n"]
4897
4898[[command_blocks]]
4899name = "alpha"
4900commands = ["quit -o"]
4901"#,
4902 )
4903 .unwrap();
4904
4905 let run_history = RunHistory::load(&run_path).unwrap();
4906 assert!(run_history.commands.is_empty());
4907 assert_eq!(run_history.command_blocks.len(), 2);
4908 }
4909
4910 #[test]
4911 fn run_history_selects_named_command_blocks_in_order() {
4912 let temp = tempdir().unwrap();
4913 let run_path = temp.path().join("run.toml");
4914 fs::write(
4915 &run_path,
4916 r#"
4917[[command_blocks]]
4918name = "first"
4919commands = ["quit -n"]
4920
4921[[command_blocks]]
4922name = "second"
4923commands = ["quit -o"]
4924"#,
4925 )
4926 .unwrap();
4927
4928 let requested = vec!["second".to_string(), "first".to_string()];
4929 let run_history = RunHistory::load(&run_path).unwrap();
4930 let selected = run_history
4931 .select_command_blocks(requested.as_slice())
4932 .unwrap();
4933 assert_eq!(selected.len(), 2);
4934 assert_eq!(
4935 selected[0].commands[0].raw_string.as_deref(),
4936 Some("quit -o")
4937 );
4938 assert_eq!(
4939 selected[1].commands[0].raw_string.as_deref(),
4940 Some("quit -n")
4941 );
4942 }
4943
4944 #[test]
4945 fn run_history_selection_rejects_unknown_command_block() {
4946 let temp = tempdir().unwrap();
4947 let run_path = temp.path().join("run.toml");
4948 fs::write(
4949 &run_path,
4950 r#"
4951[[command_blocks]]
4952name = "first"
4953commands = ["quit -n"]
4954"#,
4955 )
4956 .unwrap();
4957
4958 let requested = vec!["missing".to_string()];
4959 let run_history = RunHistory::load(&run_path).unwrap();
4960 let err = run_history
4961 .select_command_blocks(requested.as_slice())
4962 .unwrap_err();
4963 let message = format!("{err}");
4964 assert!(message.contains("Unknown command block 'missing'"));
4965 assert!(message.contains("first"));
4966 }
4967
4968 #[test]
4969 fn run_history_selection_rejects_missing_command_block_when_only_legacy_commands_exist() {
4970 let temp = tempdir().unwrap();
4971 let run_path = temp.path().join("run.toml");
4972 fs::write(
4973 &run_path,
4974 r#"
4975commands = ["quit -o"]
4976"#,
4977 )
4978 .unwrap();
4979
4980 let requested = vec!["first".to_string()];
4981 let run_history = RunHistory::load(&run_path).unwrap();
4982 let err = run_history
4983 .select_command_blocks(requested.as_slice())
4984 .unwrap_err();
4985 assert!(format!("{err}").contains("Unknown command block"));
4986 }
4987
4988 #[test]
4989 fn run_history_load_accepts_commands_and_command_blocks_together() {
4990 let temp = tempdir().unwrap();
4991 let run_path = temp.path().join("run.toml");
4992 fs::write(
4993 &run_path,
4994 r#"
4995commands = ["quit -o"]
4996
4997[[command_blocks]]
4998name = "first"
4999commands = ["quit -n"]
5000"#,
5001 )
5002 .unwrap();
5003
5004 let run_history = RunHistory::load(&run_path).unwrap();
5005 assert_eq!(run_history.commands.len(), 1);
5006 assert_eq!(run_history.command_blocks.len(), 1);
5007 assert_eq!(
5008 run_history.commands[0].raw_string.as_deref(),
5009 Some("quit -o")
5010 );
5011 }
5012
5013 #[test]
5014 fn run_history_load_rejects_duplicate_command_block_names() {
5015 let temp = tempdir().unwrap();
5016 let run_path = temp.path().join("run.toml");
5017 fs::write(
5018 &run_path,
5019 r#"
5020[[command_blocks]]
5021name = "first"
5022commands = ["quit -o"]
5023
5024[[command_blocks]]
5025name = "first"
5026commands = ["quit -n"]
5027"#,
5028 )
5029 .unwrap();
5030
5031 let err = RunHistory::load(&run_path).unwrap_err();
5032 assert!(format!("{err}").contains("duplicate block name"));
5033 }
5034
5035 #[test]
5036 fn state_manifest_roundtrip_current_version() {
5037 let temp = tempdir().unwrap();
5038 save_state_manifest(temp.path()).unwrap();
5039
5040 let manifest = load_state_manifest(temp.path()).unwrap();
5041 assert_eq!(manifest.version, CURRENT_STATE_MANIFEST_VERSION);
5042 }
5043
5044 #[test]
5045 fn state_manifest_rejects_legacy_versions() {
5046 for version in 0..CURRENT_STATE_MANIFEST_VERSION {
5047 let temp = tempdir().unwrap();
5048 fs::write(
5049 temp.path().join(STATE_MANIFEST_FILE),
5050 format!("version = {version}\n"),
5051 )
5052 .unwrap();
5053
5054 let err = load_state_manifest(temp.path()).unwrap_err();
5055 let message = err.to_string();
5056 assert!(message.contains("cannot be migrated"));
5057 assert!(message.contains("regenerate the saved state"));
5058 }
5059 }
5060
5061 #[test]
5062 fn state_manifest_requires_an_explicit_version() {
5063 let temp = tempdir().unwrap();
5064 fs::write(temp.path().join(STATE_MANIFEST_FILE), "").unwrap();
5065
5066 let err = load_state_manifest(temp.path()).unwrap_err();
5067 assert!(err.to_string().contains("missing field `version`"));
5068 }
5069
5070 #[test]
5071 fn state_save_rejects_legacy_target_before_writing() {
5072 let temp = tempdir().unwrap();
5073 let symbolica_state = temp.path().join("symbolica_state.bin");
5074 fs::write(temp.path().join(STATE_MANIFEST_FILE), "version = 1\n").unwrap();
5075 fs::write(&symbolica_state, b"legacy sentinel").unwrap();
5076 let mut state = State::new_test();
5077
5078 let err = state.save(temp.path(), true, false).unwrap_err();
5079 assert!(err.to_string().contains("cannot be migrated"));
5080 assert_eq!(fs::read(symbolica_state).unwrap(), b"legacy sentinel");
5081 assert!(!temp.path().join("model.json").exists());
5082 }
5083
5084 #[test]
5085 fn state_manifest_rejects_future_versions() {
5086 let temp = tempdir().unwrap();
5087 let future_manifest = StateManifest {
5088 version: CURRENT_STATE_MANIFEST_VERSION + 1,
5089 ..StateManifest::default()
5090 };
5091 fs::write(
5092 temp.path().join(STATE_MANIFEST_FILE),
5093 toml::to_string_pretty(&future_manifest).unwrap(),
5094 )
5095 .unwrap();
5096
5097 let err = load_state_manifest(temp.path()).unwrap_err();
5098 assert!(format!("{err}").contains("newer than this binary supports"));
5099 }
5100
5101 #[test]
5102 fn state_folder_classifies_saved_layout() {
5103 let temp = tempdir().unwrap();
5104 save_state_manifest(temp.path()).unwrap();
5105 fs::write(temp.path().join("model.json"), "{}").unwrap();
5106 fs::write(temp.path().join("symbolica_state.bin"), []).unwrap();
5107 fs::create_dir_all(temp.path().join("processes")).unwrap();
5108
5109 assert_eq!(
5110 classify_state_folder(temp.path()).unwrap(),
5111 StateFolderKind::Saved
5112 );
5113 }
5114
5115 #[test]
5116 fn state_folder_classifies_logs_only_folder_as_scratch() {
5117 let temp = tempdir().unwrap();
5118 fs::create_dir_all(temp.path().join("logs")).unwrap();
5119 fs::write(temp.path().join("logs").join("gammalog.jsonl"), "").unwrap();
5120
5121 assert_eq!(
5122 classify_state_folder(temp.path()).unwrap(),
5123 StateFolderKind::Scratch
5124 );
5125 }
5126
5127 #[test]
5128 fn state_folder_classifies_non_manifest_contents_as_unmanifested() {
5129 let temp = tempdir().unwrap();
5130 fs::create_dir_all(temp.path().join("processes").join("amplitudes")).unwrap();
5131
5132 assert_eq!(
5133 classify_state_folder(temp.path()).unwrap(),
5134 StateFolderKind::Unmanifested
5135 );
5136 }
5137
5138 #[test]
5139 fn activate_loaded_integrand_backends_falls_back_to_eager_when_external_artifacts_are_missing()
5140 {
5141 let mut state = build_generated_scalar_bubble_state_with_external_backend();
5142
5143 state.activate_loaded_integrand_backends(false).unwrap();
5144
5145 for process in &state.process_list.processes {
5146 match &process.collection {
5147 ProcessCollection::Amplitudes(amplitudes) => {
5148 for amplitude in amplitudes.values() {
5149 let integrand = amplitude.integrand.as_ref().unwrap();
5150 if matches!(
5151 integrand.frozen_compilation(),
5152 FrozenCompilationMode::Cpp(_) | FrozenCompilationMode::Assembly(_)
5153 ) {
5154 assert_eq!(integrand.active_f64_backend(), ActiveF64Backend::Eager);
5155 }
5156 }
5157 }
5158 ProcessCollection::CrossSections(cross_sections) => {
5159 for cross_section in cross_sections.values() {
5160 let integrand = cross_section.integrand.as_ref().unwrap();
5161 if matches!(
5162 integrand.frozen_compilation(),
5163 FrozenCompilationMode::Cpp(_) | FrozenCompilationMode::Assembly(_)
5164 ) {
5165 assert_eq!(integrand.active_f64_backend(), ActiveF64Backend::Eager);
5166 }
5167 }
5168 }
5169 }
5170 }
5171 }
5172
5173 #[test]
5174 fn saved_generated_process_runs_reference_acceptance_after_reload() -> Result<()> {
5175 use gammalooprs::{
5176 graph::GroupId,
5177 settings::runtime::{
5178 DiscreteGraphSamplingSettings, DiscreteGraphSamplingType, SamplingSettings,
5179 StabilityLevelSetting,
5180 },
5181 };
5182 use symbolica::numerical_integration::Sample;
5183
5184 let _guard = crate::LOG_TEST_MUTEX
5185 .lock()
5186 .unwrap_or_else(|err| err.into_inner());
5187 let bubble = build_generated_scalar_bubble_state_with_external_backend();
5188 let mut kite = State::new_test();
5189 kite.model = load_generic_model("scalars");
5190 kite.model.get_parameter_mut("mass_scalar_1")?.value = Some(Complex::new_re(F(1.0)));
5191 kite.model_parameters = InputParamCard::default_from_model(&kite.model);
5192 kite.import_graphs(
5193 Graph::from_path(
5194 crate::test_workspace_root().join("tests/resources/graphs/massive_kite.dot"),
5195 &kite.model,
5196 )?,
5197 GraphImportOptions {
5198 process_name: Some("sampling_kite".into()),
5199 process_id: None,
5200 process_definition: None,
5201 integrand_name: Some("default".into()),
5202 overwrite: false,
5203 append: false,
5204 },
5205 )?;
5206 let global: GlobalSettings = toml::from_str(
5207 r#"
5208[n_cores]
5209generate = 1
5210[generation]
5211override_lmb_heuristics = true
5212[generation.uv]
5213subtract_uv = false
5214generate_integrated = false
5215[generation.evaluator]
5216compile = false
5217summed = false
5218summed_function_map = true
5219iterative_orientation_optimization = false
5220"#,
5221 )?;
5222 let mut runtime: RuntimeSettings = toml::from_str(
5223 r#"
5224[general]
5225evaluator_method = "SummedFunctionMap"
5226integral_unit = "none"
5227enable_cache = false
5228[kinematics]
5229e_cm = 5.0
5230[kinematics.externals]
5231type = "constant"
5232[kinematics.externals.data]
5233momenta = [[5.0, 0.0, 0.0, 0.0], "dependent"]
5234helicities = [0, 0]
5235[sampling]
5236graphs = "summed"
5237orientations = "summed"
5238sampling_multichanneling = true
5239sampling_channels = "summed"
5240power = 2.0
5241default_channel_selection = ["C", "D", "ordinary"]
5242[sampling.channel_definitions.massive_kite.C]
5243around = "surface(2,4,6)"
5244parent_lmb = [4,6]
5245subspace_lmb = [4,6]
5246[sampling.channel_definitions.massive_kite.D]
5247around = "surface(3,5,6)"
5248parent_lmb = [4,6]
5249subspace_lmb = [4,6]
5250[sampling.channel_definitions.massive_kite.ordinary]
5251around = "lmb(4,6)"
5252parent_lmb = [4,6]
5253[stability]
5254rotation_axis = [{type = "x"}, {type = "y"}]
5255"#,
5256 )?;
5257 runtime.stability.levels = vec![StabilityLevelSetting::default_double()];
5258 kite.generate_integrands(&global, (&runtime).into())?;
5259
5260 let mut cut = State::new_test();
5264 cut.model = kite.model.clone();
5265 cut.model_parameters = InputParamCard::default_from_model(&cut.model);
5266 cut.import_graphs(
5267 Graph::from_string(
5268 r#"digraph sampling_cut_bubble {
5269 num=1; edge [pdg=1001]; node [num=1];
5270 ext [style=invis, is_cut=0];
5271 ext -> a [id=0];
5272 a -> b [id=1, lmb_id=0];
5273 a -> b [id=2];
5274 b -> ext [id=3];
5275 }"#,
5276 &cut.model,
5277 )?,
5278 GraphImportOptions {
5279 process_name: Some("sampling_cut".into()),
5280 process_id: None,
5281 process_definition: None,
5282 integrand_name: Some("default".into()),
5283 overwrite: false,
5284 append: false,
5285 },
5286 )?;
5287 let mut cut_runtime: RuntimeSettings = toml::from_str(
5288 r#"
5289[general]
5290evaluator_method = "SummedFunctionMap"
5291integral_unit = "none"
5292enable_cache = false
5293[kinematics]
5294e_cm = 5.0
5295[kinematics.externals]
5296type = "constant"
5297[kinematics.externals.data]
5298momenta = [[5.0, 0.0, 0.0, 0.0]]
5299helicities = ["summed_averaged"]
5300[sampling]
5301graphs = "summed"
5302orientations = "summed"
5303sampling_multichanneling = true
5304sampling_channels = "summed"
5305sampling_channel_weight = "map_density"
5306power = 2.0
5307default_channel_selection = ["matched", "wide"]
5308[sampling.channel_definitions.sampling_cut_bubble.matched]
5309around = "phase_space(cut(1,2))"
5310parent_lmb = [1]
5311subspace_lmb = [1]
5312radial_profile = "lu_h"
5313[sampling.channel_definitions.sampling_cut_bubble.wide]
5314around = "phase_space(cut(1,2))"
5315parent_lmb = [1]
5316subspace_lmb = [1]
5317radial_profile = { kind = "lu_h", approximation = "log_logistic", scale = 2.0, shape = 1.5, broad_fraction = 0.08 }
5318"#,
5319 )?;
5320 cut_runtime.stability.levels = vec![StabilityLevelSetting::default_double()];
5321 std::thread::scope(|scope| {
5324 std::thread::Builder::new()
5325 .stack_size(64 * 1024 * 1024)
5326 .spawn_scoped(scope, || {
5327 cut.generate_integrands(&global, (&cut_runtime).into())
5328 })?
5329 .join()
5330 .unwrap()
5331 })?;
5332
5333 for (mut state, reference) in [
5334 (
5335 bubble,
5336 GaussianReferenceFunction::new(2.0, vec![0.7, -0.4, 0.2])?,
5337 ),
5338 (
5339 kite,
5340 GaussianReferenceFunction::new(1.2, vec![0.4, -0.3, 0.2, -0.2, 0.1, 0.35])?,
5341 ),
5342 (
5343 cut,
5344 GaussianReferenceFunction::new(1.5, vec![0.2, -0.3, 0.1])?,
5345 ),
5346 ] {
5347 let temp = tempdir()?;
5348 let saved = temp.path().join("saved");
5349 state.save(&saved, true, false)?;
5350 let mut loaded = State::load(saved, None, None)?;
5351 loaded.activate_loaded_integrand_backends(false)?;
5352 let integrand = loaded.process_list.get_integrand_mut(0, "default")?;
5353 std::thread::scope(|scope| {
5354 std::thread::Builder::new()
5355 .stack_size(64 * 1024 * 1024)
5356 .spawn_scoped(scope, || integrand.warm_up(&loaded.model))?
5357 .join()
5358 .unwrap()
5359 })?;
5360
5361 let coordinates = (1..=8192)
5366 .map(|index| {
5367 [2, 3, 5, 7, 11, 13]
5368 .iter()
5369 .take(reference.center().len())
5370 .map(|&base| {
5371 let (mut index, mut fraction, mut value) = (index, 1.0, 0.0);
5372 while index > 0 {
5373 fraction /= base as f64;
5374 value += (index % base) as f64 * fraction;
5375 index /= base;
5376 }
5377 value
5378 })
5379 .collect::<Vec<_>>()
5380 })
5381 .collect::<Vec<_>>();
5382 let report = integrand.evaluate_reference_coordinates(&coordinates, &reference)?;
5383 assert_eq!(report.sample_count, coordinates.len());
5384 assert_eq!(report.finite_sample_count, coordinates.len());
5385 assert!((report.normalization - 1.0).abs() < 2.0e-2, "{report:?}");
5386 assert!(
5387 (report.second_moment - report.expected_second_moment).abs() < 2.0e-1,
5388 "{report:?}"
5389 );
5390
5391 if reference.center().len() == 6 {
5392 let original_settings = integrand.get_settings().clone();
5396 let SamplingSettings::MultiChanneling(multichanneling) =
5397 original_settings.sampling.clone()
5398 else {
5399 panic!("expected summed kite channels")
5400 };
5401 let ids = integrand.group_sampling_channel_ids(
5402 GroupId(0),
5403 &multichanneling.parameterization_settings,
5404 )?;
5405 assert_eq!(ids.len(), 3);
5406 let xs = vec![F(0.19), F(0.27), F(0.61), F(0.39), F(0.72), F(0.58)];
5407 let summed_sample = Sample::Continuous(F(1.0), xs.clone());
5408 let summed = integrand
5409 .evaluate_samples_raw(
5410 EvaluationTarget::Physical(&loaded.model),
5411 std::slice::from_ref(&summed_sample),
5412 0,
5413 false,
5414 false,
5415 Complex::new_zero(),
5416 )?
5417 .samples
5418 .remove(0);
5419 let summed_reference =
5420 integrand.evaluate_reference_sample_detailed(&summed_sample, &reference)?;
5421 let mut settings = original_settings.clone();
5422 settings.sampling =
5423 SamplingSettings::DiscreteGraphs(DiscreteGraphSamplingSettings {
5424 sample_orientations: false,
5425 sampling_type: DiscreteGraphSamplingType::SamplingMultiChanneling(
5426 multichanneling,
5427 ),
5428 ..Default::default()
5429 });
5430 *integrand.get_mut_settings() = settings;
5431 integrand.warm_up(&loaded.model)?;
5432 let mut expected = Complex::<F<f64>>::new_zero();
5433 let mut expected_reference = (0.0, 0.0);
5434 for channel_id in ids {
5435 let sample = Sample::Discrete(
5436 F(1.0),
5437 0,
5438 Some(Box::new(Sample::Discrete(
5439 F(1.0),
5440 channel_id.0,
5441 Some(Box::new(Sample::Continuous(F(1.0), xs.clone()))),
5442 ))),
5443 );
5444 let result = integrand
5445 .evaluate_samples_raw(
5446 EvaluationTarget::Physical(&loaded.model),
5447 std::slice::from_ref(&sample),
5448 0,
5449 false,
5450 false,
5451 Complex::new_zero(),
5452 )?
5453 .samples
5454 .remove(0);
5455 expected += result.integrand_result * result.parameterization_jacobian.unwrap();
5456 let result =
5457 integrand.evaluate_reference_sample_detailed(&sample, &reference)?;
5458 let jacobian = result.evaluation.parameterization_jacobian.unwrap().0;
5459 expected_reference.0 += result.evaluation.integrand_result.re.0 * jacobian;
5460 expected_reference.1 += result.moments.second_moment.0 * jacobian;
5461 }
5462 let actual = summed.integrand_result * summed.parameterization_jacobian.unwrap();
5463 let scale = expected.re.0.abs() + expected.im.0.abs();
5464 assert!(scale > 1.0e-18 && scale.is_finite());
5465 assert!(
5466 (actual.re.0 - expected.re.0).abs() + (actual.im.0 - expected.im.0).abs()
5467 < 1.0e-9 * scale
5468 );
5469 let jacobian = summed_reference
5470 .evaluation
5471 .parameterization_jacobian
5472 .unwrap()
5473 .0;
5474 for (actual, expected) in [
5475 (
5476 summed_reference.evaluation.integrand_result.re.0 * jacobian,
5477 expected_reference.0,
5478 ),
5479 (
5480 summed_reference.moments.second_moment.0 * jacobian,
5481 expected_reference.1,
5482 ),
5483 ] {
5484 assert!((actual - expected).abs() < 1.0e-10 * expected.abs().max(1.0));
5485 }
5486 }
5487
5488 std::thread::scope(|scope| {
5492 std::thread::Builder::new()
5493 .stack_size(64 * 1024 * 1024)
5494 .spawn_scoped(scope, || -> Result<()> {
5495 use crate::commands::integrate::Integrate;
5496 use gammalooprs::integrands::Integrand;
5497 use gammalooprs::integrate::{
5498 havana_integrate, slot_workspace_path, workspace_manifest_path,
5499 workspace_state_path, HavanaIntegrateRequest, IntegrationSlot,
5500 IntegrationState, IntegrationStatusKind, IntegrationWorkspaceManifest,
5501 IterationBatchingSettings, SamplingCorrelationMode, SlotMeta,
5502 WorkspaceSnapshotControl,
5503 };
5504 use gammalooprs::utils::serde_utils::SmartSerde;
5505 use symbolica::numerical_integration::{Grid, MonteCarloRng};
5506
5507 let process = loaded.process_list.get_integrand_mut(0, "default")?;
5508 let settings = process.get_mut_settings();
5509 settings.integrator.n_bins = 8;
5510 settings.integrator.min_samples_for_update = 4;
5511 settings.integrator.n_start = 256;
5512 settings.integrator.n_increase = 0;
5513 settings.integrator.n_max = 512;
5514 settings.integrator.seed = 7331;
5515 let mut command = Integrate::from_slots([(ProcessRef::Id(0), "default")]);
5516 command.n_cores = Some(2);
5517 command.workspace_path = Some(temp.path().join("reference_full"));
5518 command.batch_size = Some(64);
5519 command.batch_timing = 0.0;
5520 command.reference_gaussian = Some(
5522 if process.kind_name() == "amplitude" && reference.center().len() == 3 {
5523 GaussianReferenceFunction::new(reference.width(), vec![])?
5524 } else {
5525 reference.clone()
5526 },
5527 );
5528 let resolved = command
5529 .reference_gaussian
5530 .as_ref()
5531 .unwrap()
5532 .for_integrand(process)?;
5533 let mut cli = CLISettings::default();
5534 cli.state.folder = temp.path().join("saved");
5535 cli.session.set_user_requested_read_only_state(true);
5536 let source_path = generated_integrand_artifact_path(
5537 &cli.state.folder,
5538 &loaded.process_list.processes[0],
5539 "default",
5540 )
5541 .join("integrand.bin");
5542 let before_source = fs::read(&source_path)?;
5543 let full = command.run(&mut loaded, &cli)?;
5544 let result = full.result.single_slot().unwrap();
5545 assert_eq!(result.integral.neval, 512);
5546 assert_eq!(result.target, Some(Complex::new(F(1.0), F(1.0))));
5547 for (mean, error) in [
5548 (result.integral.result.re.0, result.integral.error.re.0),
5549 (result.integral.result.im.0, result.integral.error.im.0),
5550 ] {
5551 assert!(mean.is_finite() && error.is_finite() && error > 0.0);
5552 assert!(
5553 (mean - 1.0).abs() < 6.0 * error,
5554 "reference mean={mean}, error={error}"
5555 );
5556 }
5557 assert!(full.observables.is_empty());
5558 let meta = SlotMeta {
5559 process_name: result.process.clone(),
5560 integrand_name: result.integrand.clone(),
5561 };
5562 let manifest = IntegrationWorkspaceManifest::from_file(
5563 workspace_manifest_path(&full.workspace_path),
5564 "reference manifest",
5565 )?;
5566 assert_eq!(manifest.reference_gaussians, vec![Some(resolved.clone())]);
5567 assert_eq!(
5568 manifest.reference_observable_convention.as_deref(),
5569 Some(GaussianReferenceFunction::INTEGRATION_CONVENTION)
5570 );
5571
5572 let partial = temp.path().join("reference_resume");
5575 fs::create_dir_all(slot_workspace_path(&partial, &meta))?;
5576 fs::copy(
5577 workspace_manifest_path(&full.workspace_path),
5578 workspace_manifest_path(&partial),
5579 )?;
5580 fs::copy(
5581 slot_workspace_path(&full.workspace_path, &meta).join("settings.toml"),
5582 slot_workspace_path(&partial, &meta).join("settings.toml"),
5583 )?;
5584 let process = loaded.process_list.get_integrand_mut(0, "default")?.clone();
5585 let settings = process.get_settings().clone();
5586 let mut slot = IntegrationSlot::new(
5587 meta,
5588 settings.clone(),
5589 loaded.model.clone(),
5590 Integrand::ProcessIntegrand(Box::new(process)),
5591 Some(Complex::new(F(1.0), F(1.0))),
5592 );
5593 slot.reference_gaussian = Some(resolved.clone());
5594 let stopped = havana_integrate(
5595 HavanaIntegrateRequest {
5596 slots: vec![slot],
5597 sampling_correlation_mode: SamplingCorrelationMode::Correlated,
5598 n_cores: 2,
5599 state: None,
5600 workspace: Some(partial.clone()),
5601 output_control: WorkspaceSnapshotControl::default(),
5602 batching: IterationBatchingSettings {
5603 batch_size: Some(64),
5604 batch_timing_seconds: 0.0,
5605 ..Default::default()
5606 },
5607 view_options: command.build_render_options(&[settings], false),
5608 },
5609 |status| {
5610 if status.kind() == IntegrationStatusKind::Iteration {
5611 gammalooprs::request_interrupt();
5612 }
5613 Ok(())
5614 },
5615 );
5616 gammalooprs::clear_interrupt_request();
5617 stopped?;
5618 let bytes = fs::read(workspace_state_path(&partial))?;
5619 let (checkpoint, _): (IntegrationState, usize) =
5620 bincode::decode_from_slice(&bytes, bincode::config::standard())?;
5621 assert_eq!(checkpoint.iter, 1);
5622 assert_eq!(checkpoint.num_points, 256);
5623 let serialized = serde_json::to_value(&checkpoint)?;
5626 let mut grid: Grid<F<f64>> = serde_json::from_value(
5627 serialized["sampling_states"][0]["grid"].clone(),
5628 )?;
5629 let mut rng = MonteCarloRng::new(42, 0);
5630 let mut point = Sample::new();
5631 grid.sample(&mut rng, &mut point);
5632 assert!((point.get_weight().0 - 1.0).abs() > 1.0e-6);
5633 let process = loaded.process_list.get_integrand_mut(0, "default")?;
5634 let detailed =
5635 process.evaluate_reference_sample_detailed(&point, &resolved)?;
5636 let pair = process
5637 .evaluate_samples_raw(
5638 EvaluationTarget::Reference(&resolved),
5639 &[point.clone()],
5640 0,
5641 false,
5642 false,
5643 Complex::new_zero(),
5644 )?
5645 .samples
5646 .remove(0);
5647 assert_eq!(pair.integrator_weight, point.get_weight());
5648 assert!(
5649 (pair.integrand_result.re.0
5650 - detailed.evaluation.integrand_result.re.0)
5651 .abs()
5652 < 1.0e-12
5653 );
5654 assert!(
5655 (pair.integrand_result.im.0
5656 - detailed.moments.second_moment.0
5657 / resolved.expected_second_moment::<f64>().0)
5658 .abs()
5659 < 1.0e-12
5660 );
5661
5662 command.workspace_path = Some(partial.clone());
5663 let resumed = command.run(&mut loaded, &cli)?;
5664 let actual = &resumed.result.single_slot().unwrap().integral;
5665 assert_eq!(actual.neval, result.integral.neval);
5666 assert_eq!(actual.result, result.integral.result);
5667 assert_eq!(actual.error, result.integral.error);
5668 assert_eq!(fs::read(&source_path)?, before_source);
5669 loaded
5671 .process_list
5672 .get_integrand_mut(0, "default")?
5673 .get_mut_settings()
5674 .integrator
5675 .seed = 19;
5676 let mut physical = command.clone();
5677 physical.reference_gaussian = None;
5678 let unchanged = fs::read(workspace_state_path(&partial))?;
5679 assert!(physical
5680 .run(&mut loaded, &cli)
5681 .unwrap_err()
5682 .to_string()
5683 .contains("physical/reference"));
5684 assert_eq!(
5685 loaded
5686 .process_list
5687 .get_integrand_mut(0, "default")?
5688 .get_settings()
5689 .integrator
5690 .seed,
5691 19
5692 );
5693 assert_eq!(fs::read(workspace_state_path(&partial))?, unchanged);
5694 command.target = vec!["1,2".into()];
5695 assert!(command
5696 .run(&mut loaded, &cli)
5697 .unwrap_err()
5698 .to_string()
5699 .contains("contradictory"));
5700 Ok(())
5701 })?
5702 .join()
5703 .unwrap()
5704 })?;
5705 }
5706 Ok(())
5707 }
5708
5709 #[test]
5710 fn summed_sampling_matches_explicit_canonical_channels() -> Result<()> {
5711 use gammalooprs::{
5712 graph::GroupId,
5713 settings::runtime::{
5714 DiscreteGraphSamplingSettings, DiscreteGraphSamplingType, SamplingSettings,
5715 StabilityLevelSetting,
5716 },
5717 };
5718 use symbolica::numerical_integration::Sample;
5719
5720 use gammalooprs::integrands::evaluation::StabilityStatus;
5721
5722 let _guard = crate::LOG_TEST_MUTEX
5723 .lock()
5724 .unwrap_or_else(|err| err.into_inner());
5725 let mut state = build_generated_scalar_bubble_state_with_external_backend();
5726 state.activate_loaded_integrand_backends(false)?;
5727 let integrand = state.process_list.get_integrand_mut(0, "default")?;
5728 let mut settings: RuntimeSettings = toml::from_str(
5729 r#"
5730[kinematics]
5731e_cm = 2.0
5732[kinematics.externals]
5733type = "constant"
5734[kinematics.externals.data]
5735momenta = [[0.5, 0.2, 0.3, 0.1], "dependent"]
5736helicities = [0, 0]
5737[sampling]
5738graphs = "summed"
5739orientations = "summed"
5740sampling_multichanneling = true
5741sampling_channels = "summed"
5742default_channel_selection = ["auto:lmb"]
5743[stability]
5744rotation_axis = [{type = "x"}, {type = "y"}]
5745"#,
5746 )?;
5747 settings.stability.levels = vec![StabilityLevelSetting::default_double()];
5748 let SamplingSettings::MultiChanneling(multichanneling) = settings.sampling.clone() else {
5749 panic!("expected summed sampling settings");
5750 };
5751 let channel_ids = integrand
5752 .group_sampling_channel_ids(GroupId(0), &multichanneling.parameterization_settings)?;
5753 assert_eq!(
5754 channel_ids.len(),
5755 2,
5756 "bubble must exercise two distinct affine LMB charts"
5757 );
5758 let coordinates = [
5759 vec![F(0.27), F(0.36), F(0.71)],
5760 vec![F(0.62), F(0.24), F(0.53)],
5761 ];
5762 let samples = coordinates
5763 .iter()
5764 .map(|xs| Sample::Continuous(F(1.0), xs.clone()))
5765 .collect::<Vec<_>>();
5766 *integrand.get_mut_settings() = settings.clone();
5767 integrand.warm_up(&state.model)?;
5768 let reference = GaussianReferenceFunction::new(2.0, vec![0.7, -0.4, 0.2])?;
5769 let summed_reference = samples
5770 .iter()
5771 .map(|sample| integrand.evaluate_reference_sample_detailed(sample, &reference))
5772 .collect::<Result<Vec<_>>>()?;
5773 let summed = integrand.evaluate_samples_raw(
5774 EvaluationTarget::Physical(&state.model),
5775 &samples,
5776 0,
5777 false,
5778 false,
5779 Complex::new_zero(),
5780 )?;
5781
5782 settings.sampling = SamplingSettings::DiscreteGraphs(DiscreteGraphSamplingSettings {
5783 sample_orientations: false,
5784 sampling_type: DiscreteGraphSamplingType::SamplingMultiChanneling(
5785 multichanneling.clone(),
5786 ),
5787 ..Default::default()
5788 });
5789 *integrand.get_mut_settings() = settings.clone();
5790 integrand.warm_up(&state.model)?;
5791 let mut expected = vec![Complex::<F<f64>>::new_zero(); coordinates.len()];
5792 let mut expected_reference = vec![(0.0, 0.0); coordinates.len()];
5793 for channel_id in channel_ids {
5794 let samples = coordinates
5795 .iter()
5796 .map(|xs| {
5797 Sample::Discrete(
5798 F(1.0),
5799 0,
5800 Some(Box::new(Sample::Discrete(
5801 F(1.0),
5802 channel_id.0,
5803 Some(Box::new(Sample::Continuous(F(1.0), xs.clone()))),
5804 ))),
5805 )
5806 })
5807 .collect::<Vec<_>>();
5808 for (expected, sample) in expected_reference.iter_mut().zip(&samples) {
5809 let result = integrand.evaluate_reference_sample_detailed(sample, &reference)?;
5810 let jacobian = result.evaluation.parameterization_jacobian.unwrap().0;
5811 expected.0 += result.evaluation.integrand_result.re.0 * jacobian;
5812 expected.1 += result.moments.second_moment.0 * jacobian;
5813 }
5814 let result = integrand.evaluate_samples_raw(
5815 EvaluationTarget::Physical(&state.model),
5816 &samples,
5817 0,
5818 false,
5819 false,
5820 Complex::new_zero(),
5821 )?;
5822 for (total, sample) in expected.iter_mut().zip(result.samples) {
5823 *total +=
5824 sample.integrand_result * sample.parameterization_jacobian.unwrap_or(F(1.0));
5825 }
5826 }
5827 settings.sampling = SamplingSettings::DiscreteGraphs(DiscreteGraphSamplingSettings {
5830 sample_orientations: false,
5831 sampling_type: DiscreteGraphSamplingType::MultiChanneling(multichanneling),
5832 ..Default::default()
5833 });
5834 *integrand.get_mut_settings() = settings;
5835 integrand.warm_up(&state.model)?;
5836 let samples = samples
5837 .into_iter()
5838 .map(|sample| Sample::Discrete(F(1.0), 0, Some(Box::new(sample))))
5839 .collect::<Vec<_>>();
5840 let group_summed_reference = samples
5841 .iter()
5842 .map(|sample| integrand.evaluate_reference_sample_detailed(sample, &reference))
5843 .collect::<Result<Vec<_>>>()?;
5844 for results in [summed_reference, group_summed_reference] {
5845 for (result, expected) in results.iter().zip(&expected_reference) {
5846 let jacobian = result.evaluation.parameterization_jacobian.unwrap().0;
5847 for (actual, expected) in [
5848 (
5849 result.evaluation.integrand_result.re.0 * jacobian,
5850 expected.0,
5851 ),
5852 (result.moments.second_moment.0 * jacobian, expected.1),
5853 ] {
5854 assert!(
5855 (actual - expected).abs() < 1.0e-11 * expected.abs().max(1.0),
5856 "summed reference {actual} differs from explicit channel sum {expected}"
5857 );
5858 }
5859 }
5860 }
5861 let group_summed = integrand.evaluate_samples_raw(
5862 EvaluationTarget::Physical(&state.model),
5863 &samples,
5864 0,
5865 false,
5866 false,
5867 Complex::new_zero(),
5868 )?;
5869 for result in [summed, group_summed] {
5870 for (sample, expected) in result.samples.iter().zip(&expected) {
5871 let value =
5872 sample.integrand_result * sample.parameterization_jacobian.unwrap_or(F(1.0));
5873 let scale = expected.re.0.abs() + expected.im.0.abs();
5874 assert!(
5875 scale > 1.0e-18 && scale.is_finite(),
5876 "invalid oracle {expected}"
5877 );
5878 let error = (value - expected).re.0.abs() + (value - expected).im.0.abs();
5879 assert!(
5880 error < scale * 1.0e-10,
5881 "summed {value} differs from explicit channels {expected}"
5882 );
5883 assert!(!sample.evaluation_metadata.is_nan);
5884 assert!(
5885 sample
5886 .evaluation_metadata
5887 .stability_results
5888 .iter()
5889 .all(|level| matches!(level.status, StabilityStatus::Stable(_))),
5890 "rotation invariance failed"
5891 );
5892 }
5893 }
5894 use gammalooprs::{
5898 integrands::process::{
5899 sampling::context::SamplingMapContext, GraphTerm, MomentumSpaceEvaluationInput,
5900 ProcessIntegrand, ProcessIntegrandImpl, SamplingMapComponent,
5901 SamplingMapDefinition, SamplingMapKernel,
5902 },
5903 momentum::ThreeMomentum,
5904 };
5905 let mut settings = integrand.get_settings().clone();
5906 let mut parameterization = settings.sampling.get_parameterization_settings().unwrap();
5907 parameterization
5908 .lmb_basis_ids
5909 .insert("bubble".into(), vec![1]);
5910 parameterization.sampling_channels.default_channel_selection =
5911 vec!["auto:optimized_lmb".into()];
5912 settings.sampling = SamplingSettings::Default(parameterization.clone());
5913 *integrand.get_mut_settings() = settings.clone();
5914 integrand.warm_up(&state.model)?;
5915 let frame = {
5916 let ProcessIntegrand::Amplitude(amplitude) = integrand else {
5917 unreachable!()
5918 };
5919 let term = &litude.data.graph_terms[0];
5920 let basis = term.selected_lmb_basis_id(¶meterization)?;
5921 assert_eq!(usize::from(basis), 1);
5922 let externals = settings
5923 .kinematics
5924 .externals
5925 .get_dependent_externals::<f64>(amplitude.get_dependent_momenta_constructor())?;
5926 let externals = externals
5927 .iter()
5928 .map(|p| {
5929 [
5930 p.temporal.value.0,
5931 p.spatial.px.0,
5932 p.spatial.py.0,
5933 p.spatial.pz.0,
5934 ]
5935 })
5936 .collect::<Vec<_>>();
5937 let setup = term.sampling_setup();
5938 setup.lmb_frame_map(&setup.all_bases[basis], &externals)?
5939 };
5940 let kernel = SamplingMapKernel::new(
5941 SamplingMapDefinition::Lmb(vec![0]),
5942 parameterization,
5943 settings.kinematics.e_cm,
5944 1,
5945 )?;
5946 let xs = [0.27, 0.36, 0.71];
5947 let selected =
5948 SamplingMapComponent::forward(&kernel, &xs, &mut SamplingMapContext::detached(&[]))?;
5949 let raw = frame.forward(&selected.point, &mut SamplingMapContext::detached(&[]))?;
5950 let sampling_jacobian = selected.jacobian * raw.jacobian;
5951 let sample = Sample::Continuous(F(1.0), xs.into_iter().map(F).collect());
5952 let reference_result = integrand.evaluate_reference_sample_detailed(&sample, &reference)?;
5953 let distance_squared = raw
5954 .point
5955 .iter()
5956 .zip(reference.center())
5957 .map(|(x, c)| (x - c).powi(2))
5958 .sum::<f64>();
5959 let expected = (-distance_squared / (2.0 * reference.width().powi(2))).exp()
5960 / (2.0 * std::f64::consts::PI * reference.width().powi(2)).powf(1.5);
5961 assert_eq!(
5964 reference_result.evaluation.parameterization_jacobian,
5965 Some(F(1.0))
5966 );
5967 assert!(
5968 (reference_result.evaluation.integrand_result.re.0 / sampling_jacobian - expected)
5969 .abs()
5970 < expected * 1.0e-12
5971 );
5972 let radius_squared = raw.point.iter().map(|x| x * x).sum::<f64>();
5973 assert!(
5974 (reference_result.moments.second_moment.0 / sampling_jacobian
5975 - expected * radius_squared)
5976 .abs()
5977 < 1.0e-12
5978 );
5979 let physical = integrand
5980 .evaluate_samples_raw(
5981 EvaluationTarget::Physical(&state.model),
5982 &[sample],
5983 0,
5984 false,
5985 false,
5986 Complex::new_zero(),
5987 )?
5988 .samples
5989 .remove(0);
5990 let raw_result = integrand.evaluate_momentum_configuration(
5991 &state.model,
5992 &MomentumSpaceEvaluationInput {
5993 loop_momenta: raw
5994 .point
5995 .as_chunks::<3>()
5996 .0
5997 .iter()
5998 .map(|p| ThreeMomentum {
5999 px: F(p[0]),
6000 py: F(p[1]),
6001 pz: F(p[2]),
6002 })
6003 .collect(),
6004 integrator_weight: F(1.0),
6005 graph_id: Some(0),
6006 group_id: None,
6007 orientation: None,
6008 channel_id: None,
6009 },
6010 false,
6011 )?;
6012 assert_eq!(physical.parameterization_jacobian, Some(F(1.0)));
6013 let delta = physical.integrand_result / Complex::new_re(F(sampling_jacobian))
6014 - raw_result.integrand_result;
6015 let scale = raw_result.integrand_result.re.0.abs() + raw_result.integrand_result.im.0.abs();
6016 assert!(scale > 1.0e-18 && scale.is_finite());
6017 assert!(delta.re.0.abs() + delta.im.0.abs() < 1.0e-10 * scale);
6018 Ok(())
6019 }
6020
6021 #[test]
6022 fn resolve_effective_model_parameter_card_overlays_runtime_model_settings() {
6023 let mut state = State::new_test();
6024 state.model = load_generic_model("scalars");
6025 state.model_parameters = InputParamCard::default_from_model(&state.model);
6026
6027 let mut settings = RuntimeSettings::default();
6028 settings
6029 .model
6030 .external_parameters
6031 .insert("mass_scalar_2".to_string(), (F(7.5), F(0.0)));
6032
6033 let resolved = state
6034 .resolve_effective_model_parameter_card_for_settings(&settings)
6035 .unwrap();
6036
6037 assert_eq!(
6038 resolved[&UFOSymbol::from("mass_scalar_2")],
6039 Complex::new(F(7.5), F(0.0))
6040 );
6041 assert_eq!(
6042 resolved[&UFOSymbol::from("mass_scalar_1")],
6043 state.model_parameters[&UFOSymbol::from("mass_scalar_1")]
6044 );
6045 }
6046
6047 #[test]
6048 fn resolve_effective_model_parameter_card_rejects_non_overridable_parameters() {
6049 let mut state = State::new_test();
6050 state.model = load_generic_model("scalars");
6051 state.model_parameters = InputParamCard::default_from_model(&state.model);
6052 state
6053 .model_parameters
6054 .remove(&UFOSymbol::from("mass_scalar_2"));
6055
6056 let mut settings = RuntimeSettings::default();
6057 settings
6058 .model
6059 .external_parameters
6060 .insert("mass_scalar_2".to_string(), (F(7.5), F(0.0)));
6061
6062 let err = state
6063 .resolve_effective_model_parameter_card_for_settings(&settings)
6064 .unwrap_err();
6065
6066 assert!(err
6067 .to_string()
6068 .contains("cannot be overridden because it is not present"));
6069 }
6070}
6071
6072#[derive(Args, Debug, Clone)]
6073pub struct ExistingArgs {
6074 pub process_id: u32,
6075 pub name: Option<String>,
6076}