1use std::{
2 collections::{BTreeMap, BTreeSet, HashSet},
3 fs::{self},
4 io::{self, IsTerminal},
5 ops::ControlFlow,
6 path::{Path, PathBuf},
7 str::FromStr,
8 sync::{
9 atomic::{AtomicBool, AtomicU64, Ordering},
10 Arc, Mutex,
11 },
12 thread,
13 time::{Duration, Instant},
14};
15
16use clap::Args;
17use color_eyre::{Result, Section};
18use colored::Colorize;
19use eyre::{eyre, Context};
20use gammalooprs::{
21 processes::{Amplitude, CrossSection},
22 utils::serde_utils::IsDefault,
23};
24use linnet::half_edge::subgraph::SubGraphLike;
25use schemars::{schema_for, JsonSchema, Schema};
26use serde::{Deserialize, Serialize};
27use spenso::algebra::complex::Complex;
28use symbolica::numerical_integration::Sample;
29use sysinfo::{get_current_pid, ProcessRefreshKind, ProcessesToUpdate, System};
30use toml::Value as TomlValue;
31use tracing::{debug, info, info_span, Span};
32use tracing_indicatif::span_ext::IndicatifSpanExt;
33
34use gammalooprs::{
35 clear_interrupt_request,
36 feyngen::GenerationType,
37 graph::Graph,
38 initialisation::initialise,
39 integrands::{process::ProcessIntegrand, HasIntegrand},
40 is_interrupt_requested,
41 model::{InputParamCard, Model, SerializableInputParamCard, UFOSymbol},
42 processes::{
43 begin_phase, merge_generated_graph_reports, DotExportSettings, GeneratedGraphReport,
44 GenerationProcessKind, GenerationProgressMode, GenerationProgressModeGuard,
45 GenerationProgressObserver, GenerationProgressObserverGuard, GenerationProgressPhase,
46 GraphGenerationStats, GraphGroupSelectionMode, GraphGroupSelectionPlan,
47 GraphGroupSelectionReport, GraphGroupSelectionSpec, NamedGraphGenerationReport, Process,
48 ProcessCollection, ProcessDefinition, ProcessList,
49 },
50 settings::{
51 global::GenerationSettings, runtime::LockedRuntimeSettings, GlobalSettings, RuntimeSettings,
52 },
53 utils::{
54 serde_utils::{get_schema_folder, SmartSerde},
55 tracing::{init_bench_tracing, init_test_tracing},
56 F,
57 },
58 GammaLoopContextContainer,
59};
60
61use crate::{
62 command_parser::{normalize_clap_args, split_command_line},
63 commands::{save::SaveState, Commands},
64 integrand_info::{collect_integrand_info, IntegrandInfo},
65 model_parameters::{external_model_parameter_type, validate_model_parameter_type},
66 render_smart_toml,
67 tracing::{set_file_log_filter, set_log_style, set_stderr_log_filter},
68 CLISettings,
69};
70
71#[derive(Debug, Clone, Copy, Default)]
72pub struct GenerationResourceSummary {
73 pub peak_ram_bytes: u64,
74 pub generation_cores: usize,
75}
76
77#[derive(Debug, Clone, Default)]
78pub struct GenerationReports {
79 pub reports: Vec<GeneratedGraphReport>,
80 pub resources: GenerationResourceSummary,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
84pub struct IntegrandGenerationSummaryKey {
85 pub process_id: usize,
86 pub integrand_name: String,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct IntegrandGenerationSummary {
91 pub peak_ram_bytes: u64,
92 pub reports: Vec<GeneratedGraphReport>,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct SelectedGraphGroups {
97 pub source_process_id: usize,
98 pub source_process_name: String,
99 pub source_integrand_name: String,
100 pub process_id: usize,
101 pub process_name: String,
102 pub integrand_name: String,
103 pub report: GraphGroupSelectionReport,
104 pub copied_to_output: bool,
105 pub replaced_existing_target: bool,
106 pub removed_target_artifacts: bool,
107 pub discarded_generated_integrand: bool,
108 pub removed_generated_artifacts: bool,
109 pub removed_generation_summary: bool,
110}
111
112#[derive(Debug, Clone, Default, PartialEq, Eq)]
113pub struct GraphGroupSelectionTarget {
114 pub output_process_name: Option<String>,
115 pub output_integrand_name: Option<String>,
116 pub clear_existing_processes: bool,
117}
118
119impl GraphGroupSelectionTarget {
120 pub fn in_place() -> Self {
121 Self::default()
122 }
123
124 pub fn copy(
125 output_process_name: Option<String>,
126 output_integrand_name: Option<String>,
127 clear_existing_processes: bool,
128 ) -> Self {
129 Self {
130 output_process_name,
131 output_integrand_name,
132 clear_existing_processes,
133 }
134 }
135
136 fn is_copy_mode(&self) -> bool {
137 self.output_process_name.is_some() || self.output_integrand_name.is_some()
138 }
139}
140
141#[derive(Debug, Clone, Copy)]
142pub struct GraphGroupSelectionContext<'a> {
143 pub generation_settings: &'a GenerationSettings,
144 pub state_folder: &'a Path,
145 pub read_only_state: bool,
146}
147
148impl<'a> GraphGroupSelectionContext<'a> {
149 pub fn new(
150 generation_settings: &'a GenerationSettings,
151 state_folder: &'a Path,
152 read_only_state: bool,
153 ) -> Self {
154 Self {
155 generation_settings,
156 state_folder,
157 read_only_state,
158 }
159 }
160}
161
162struct IntegrandCopyInsertion {
163 process_id: usize,
164 process_name: String,
165 integrand_name: String,
166 replaced_existing_target: bool,
167 removed_target_artifacts: bool,
168}
169
170struct SelectionSource {
171 process_id: usize,
172 process_name: String,
173 integrand_name: String,
174}
175
176enum IntegrandCopyPayload {
177 Amplitude(Amplitude),
178 CrossSection(CrossSection),
179}
180
181impl IntegrandCopyPayload {
182 fn rename(&mut self, new_name: &str) {
183 match self {
184 Self::Amplitude(amplitude) => {
185 amplitude.name = new_name.to_string();
186 rename_process_integrand(amplitude.integrand.as_mut(), new_name);
187 }
188 Self::CrossSection(cross_section) => {
189 cross_section.name = new_name.to_string();
190 rename_process_integrand(cross_section.integrand.as_mut(), new_name);
191 }
192 }
193 }
194
195 fn apply_graph_group_selection(&mut self, plan: &GraphGroupSelectionPlan) -> Result<()> {
196 match self {
197 Self::Amplitude(amplitude) => amplitude.apply_graph_group_selection(plan),
198 Self::CrossSection(cross_section) => cross_section.apply_graph_group_selection(plan),
199 }
200 }
201
202 fn is_compatible_with(&self, collection: &ProcessCollection) -> bool {
203 matches!(
204 (self, collection),
205 (Self::Amplitude(_), ProcessCollection::Amplitudes(_))
206 | (Self::CrossSection(_), ProcessCollection::CrossSections(_))
207 )
208 }
209
210 fn kind_name(&self) -> &'static str {
211 match self {
212 Self::Amplitude(_) => "amplitudes",
213 Self::CrossSection(_) => "cross sections",
214 }
215 }
216
217 fn into_collection(self) -> ProcessCollection {
218 match self {
219 Self::Amplitude(amplitude) => {
220 let mut collection = ProcessCollection::Amplitudes(BTreeMap::new());
221 collection.add_amplitude(amplitude);
222 collection
223 }
224 Self::CrossSection(cross_section) => {
225 let mut collection = ProcessCollection::CrossSections(BTreeMap::new());
226 collection.add_cross_section(cross_section);
227 collection
228 }
229 }
230 }
231
232 fn insert_into_process(self, process: &mut Process, process_name: &str) -> Result<()> {
233 match (self, &mut process.collection) {
234 (Self::Amplitude(amplitude), ProcessCollection::Amplitudes(amplitudes)) => {
235 amplitudes.insert(amplitude.name.clone(), amplitude);
236 Ok(())
237 }
238 (
239 Self::CrossSection(cross_section),
240 ProcessCollection::CrossSections(cross_sections),
241 ) => {
242 cross_sections.insert(cross_section.name.clone(), cross_section);
243 Ok(())
244 }
245 (payload, _) => Err(eyre!(
246 "Destination process '{}' exists but does not contain {}",
247 process_name,
248 payload.kind_name()
249 )),
250 }
251 }
252}
253
254struct GenerationMonitor {
255 peak_ram_bytes: Arc<AtomicU64>,
256 current_ram_bytes: Arc<AtomicU64>,
257 stop_requested: Arc<AtomicBool>,
258 handle: Option<thread::JoinHandle<()>>,
259}
260
261struct AggregateGenerationProgressReporter {
262 progress_span: Span,
263 current_ram_bytes: Arc<AtomicU64>,
264 peak_ram_bytes: Arc<AtomicU64>,
265 state: Mutex<AggregateGenerationProgressState>,
266}
267
268#[derive(Default)]
269struct AggregateGenerationProgressState {
270 phase: Option<GenerationProgressPhase>,
271 kind: Option<GenerationProcessKind>,
272 process: String,
273 integrand: String,
274 total_graphs: usize,
275 done_graphs: usize,
276 total_cuts: Option<usize>,
277 done_cuts: usize,
278 discovered_st_cuts: usize,
279 discovered_valid_cuts: usize,
280 active_graphs: BTreeSet<String>,
281 last_graph: Option<String>,
282 stats: GraphGenerationStats,
283}
284
285impl AggregateGenerationProgressReporter {
286 fn new(
287 current_ram_bytes: Arc<AtomicU64>,
288 peak_ram_bytes: Arc<AtomicU64>,
289 eta_warmup_steps: u64,
290 ) -> Arc<Self> {
291 let span = info_span!(
292 "Integrand generation",
293 indicatif.pb_show = true,
294 indicatif.pb_msg = "Starting integrand generation"
295 );
296 span.pb_set_style(
297 &gammalooprs::utils::long_running_progress_style_with_eta_warmup(eta_warmup_steps),
298 );
299 span.pb_start();
300 span.pb_set_length(0);
301 span.pb_set_position(0);
302 span.pb_tick();
303 Self::new_with_span(current_ram_bytes, peak_ram_bytes, span)
304 }
305
306 #[cfg(test)]
307 fn new_hidden(current_ram_bytes: Arc<AtomicU64>, peak_ram_bytes: Arc<AtomicU64>) -> Arc<Self> {
308 Self::new_with_span(current_ram_bytes, peak_ram_bytes, Span::none())
309 }
310
311 fn new_with_span(
312 current_ram_bytes: Arc<AtomicU64>,
313 peak_ram_bytes: Arc<AtomicU64>,
314 progress_span: Span,
315 ) -> Arc<Self> {
316 Arc::new(Self {
317 progress_span,
318 current_ram_bytes,
319 peak_ram_bytes,
320 state: Mutex::new(AggregateGenerationProgressState::default()),
321 })
322 }
323
324 fn fixed_field(value: &str, width: usize) -> String {
325 let mut chars = value.chars().collect::<Vec<_>>();
326 if chars.len() > width {
327 chars.truncate(width.saturating_sub(1));
328 chars.push('~');
329 }
330 format!("{:<width$}", chars.into_iter().collect::<String>())
331 }
332
333 fn progress_memory(bytes: u64) -> String {
334 const MIB: f64 = 1024.0 * 1024.0;
335 const GIB: f64 = MIB * 1024.0;
336 let value = bytes as f64;
337 if value >= GIB {
338 format!("{:>6.2} GiB", value / GIB)
339 } else {
340 format!("{:>6.0} MiB", value / MIB)
341 }
342 }
343
344 fn progress_percent(percent: f64) -> String {
345 if percent < 0.01 {
346 "0%".to_string()
347 } else if percent < 0.1 {
348 format!("{percent:.3}%")
349 } else if percent < 1.0 {
350 format!("{percent:.2}%")
351 } else if percent < 10.0 {
352 format!("{percent:.1}%")
353 } else {
354 format!("{percent:.0}%")
355 }
356 }
357
358 fn progress_time_share(duration: Duration, total: Duration) -> String {
359 if total.is_zero() {
360 "--".to_string()
361 } else {
362 Self::progress_percent(duration.as_secs_f64() * 100.0 / total.as_secs_f64())
363 }
364 }
365
366 fn progress_units(state: &AggregateGenerationProgressState) -> (u64, u64) {
367 match (state.phase, state.total_cuts) {
368 (Some(GenerationProgressPhase::GraphGeneration), Some(total_cuts)) => {
369 let total = total_cuts.saturating_add(state.total_graphs) as u64;
370 let completed = state
371 .done_cuts
372 .saturating_add(state.done_graphs)
373 .min(total_cuts.saturating_add(state.total_graphs))
374 as u64;
375 (completed, total)
376 }
377 _ => (state.done_graphs as u64, state.total_graphs as u64),
378 }
379 }
380
381 fn graph_progress_counts(state: &AggregateGenerationProgressState) -> (usize, usize, usize) {
382 let active = state
383 .active_graphs
384 .len()
385 .min(state.total_graphs.saturating_sub(state.done_graphs));
386 (state.done_graphs, active, state.total_graphs)
387 }
388
389 fn refresh(&self, state: &AggregateGenerationProgressState) {
390 let (completed_units, total_units) = Self::progress_units(state);
391 self.progress_span.pb_set_length(total_units);
392 self.progress_span.pb_set_position(completed_units);
393
394 let current_ram = Self::progress_memory(self.current_ram_bytes.load(Ordering::Relaxed));
395 let peak_ram = Self::progress_memory(self.peak_ram_bytes.load(Ordering::Relaxed));
396 let kind = match state.kind {
397 Some(GenerationProcessKind::Amplitude) => "AMP",
398 Some(GenerationProcessKind::CrossSection) => "XS",
399 None => "GEN",
400 };
401 let phase = match state.phase {
402 Some(GenerationProgressPhase::GraphPreprocessing) => "prep",
403 Some(GenerationProgressPhase::GraphGeneration) => "graphs",
404 Some(GenerationProgressPhase::Backend) => "backend",
405 None => "gen",
406 };
407 let last_graph = state.last_graph.as_deref().unwrap_or("-");
408 let cut_context = match (state.phase, state.kind, state.total_cuts) {
409 (
410 Some(GenerationProgressPhase::GraphPreprocessing),
411 Some(GenerationProcessKind::Amplitude),
412 _,
413 ) => None,
414 (Some(GenerationProgressPhase::GraphPreprocessing), _, _) => Some(format!(
415 "{:>4}/{:<4}",
416 state.discovered_valid_cuts, state.discovered_st_cuts
417 )),
418 (_, _, Some(total_cuts)) => Some(format!("{:>4}/{:<4}", state.done_cuts, total_cuts)),
419 _ => Some(format!("{:>4}/{:<4}", "-", "-")),
420 };
421 let phase = Self::fixed_field(phase, 7).bold().blue();
422 let kind = Self::fixed_field(kind, 3).cyan();
423 let identifier = if state.process.is_empty() {
424 state.integrand.clone()
425 } else {
426 format!("{}@{}", state.integrand, state.process)
427 };
428 let identifier = Self::fixed_field(&identifier, 24).yellow();
429 let last_graph = Self::fixed_field(last_graph, 8).green();
430 let (done_graphs, active_graphs, total_graphs) = Self::graph_progress_counts(state);
431 let graph_ratio = format!(
432 "{} {} / {}",
433 format!("{done_graphs:>3}").green(),
434 format!("({active_graphs:>3})").dimmed(),
435 format!("{total_graphs:<3}").green(),
436 );
437 let ram = format!("{current_ram}/{peak_ram}").yellow();
438 let phase_detail = if state.phase == Some(GenerationProgressPhase::GraphPreprocessing) {
439 let steps = match state.kind {
440 Some(GenerationProcessKind::Amplitude) => {
441 "CFFs + LMBs + integrands + threshold CTs + tropical samplers"
442 }
443 _ => "cuts + CFFs + LMBs + integrands + threshold CTs",
444 };
445 format!("{} {}", "step".bold().blue(), steps.cyan())
446 } else {
447 let total_time = state.stats.total_time;
448 let expression_share =
449 Self::progress_time_share(state.stats.expression_build_time(), total_time);
450 let spenso_share =
451 Self::progress_time_share(state.stats.evaluator_spenso_time, total_time);
452 let symbolica_share =
453 Self::progress_time_share(state.stats.evaluator_symbolica_time, total_time);
454 let compile_share =
455 Self::progress_time_share(state.stats.evaluator_compile_time, total_time);
456 format!(
457 "{} {} {} / {} {} / {} {} / {} {}",
458 "time".bold().blue(),
459 "expr".bold().blue(),
460 expression_share.magenta(),
461 "spenso".bold().blue(),
462 spenso_share.cyan(),
463 "eval".bold().blue(),
464 symbolica_share.green(),
465 "compile".bold().blue(),
466 compile_share.yellow(),
467 )
468 };
469 let mut sections = vec![
470 format!("{} {} {}", phase, kind, identifier),
471 format!("{} {}", "g".bold().blue(), graph_ratio),
472 format!("{} {}", "last".bold().blue(), last_graph),
473 ];
474 if let Some(cut_context) = cut_context {
475 sections.push(format!("{} {}", "cut".bold().blue(), cut_context.green()));
476 }
477 sections.push(format!("{} {}", "ram".bold().blue(), ram));
478 sections.push(phase_detail);
479 self.progress_span.pb_set_message(§ions.join(" | "));
480 self.progress_span.pb_tick();
481 }
482}
483
484impl GenerationProgressObserver for AggregateGenerationProgressReporter {
485 fn begin_phase(
486 &self,
487 phase: GenerationProgressPhase,
488 kind: GenerationProcessKind,
489 process: &str,
490 integrand: &str,
491 total_graphs: usize,
492 total_cuts: Option<usize>,
493 ) {
494 let mut state = self
495 .state
496 .lock()
497 .expect("aggregate generation progress state mutex is poisoned");
498 state.phase = Some(phase);
499 state.kind = Some(kind);
500 state.process.clear();
501 state.process.push_str(process);
502 state.integrand.clear();
503 state.integrand.push_str(integrand);
504 state.total_graphs = total_graphs;
505 state.done_graphs = 0;
506 state.total_cuts = total_cuts;
507 state.done_cuts = 0;
508 state.discovered_st_cuts = 0;
509 state.discovered_valid_cuts = 0;
510 state.active_graphs.clear();
511 state.last_graph = None;
512 state.stats = GraphGenerationStats::default();
513 self.progress_span.pb_reset_elapsed();
514 self.refresh(&state);
515 }
516
517 fn graph_started(
518 &self,
519 _kind: GenerationProcessKind,
520 _integrand: &str,
521 graph: &str,
522 _cut_count: Option<usize>,
523 ) {
524 let mut state = self
525 .state
526 .lock()
527 .expect("aggregate generation progress state mutex is poisoned");
528 state.active_graphs.insert(graph.to_string());
529 state.last_graph = Some(graph.to_string());
530 self.refresh(&state);
531 }
532
533 fn graph_finished(
534 &self,
535 _kind: GenerationProcessKind,
536 _integrand: &str,
537 graph: &str,
538 stats: &GraphGenerationStats,
539 completed_cuts: Option<usize>,
540 ) {
541 let mut state = self
542 .state
543 .lock()
544 .expect("aggregate generation progress state mutex is poisoned");
545 state.done_graphs = state.done_graphs.saturating_add(1).min(state.total_graphs);
546 state.done_cuts += completed_cuts.unwrap_or(0);
547 state.active_graphs.remove(graph);
548 state.last_graph = Some(graph.to_string());
549 state.stats.merge_in_place(stats);
550 self.refresh(&state);
551 }
552
553 fn cuts_discovered(
554 &self,
555 _integrand: &str,
556 graph: &str,
557 st_cut_count: usize,
558 valid_cut_count: usize,
559 ) {
560 let mut state = self
561 .state
562 .lock()
563 .expect("aggregate generation progress state mutex is poisoned");
564 state.discovered_st_cuts += st_cut_count;
565 state.discovered_valid_cuts += valid_cut_count;
566 state.last_graph = Some(graph.to_string());
567 self.refresh(&state);
568 }
569
570 fn cut_finished(&self, _integrand: &str, graph: &str, cut_count: usize) {
571 let mut state = self
572 .state
573 .lock()
574 .expect("aggregate generation progress state mutex is poisoned");
575 state.done_cuts = state.done_cuts.saturating_add(cut_count);
576 if let Some(total_cuts) = state.total_cuts {
577 state.done_cuts = state.done_cuts.min(total_cuts);
578 }
579 state.last_graph = Some(graph.to_string());
580 self.refresh(&state);
581 }
582
583 fn backend_started(&self, kind: GenerationProcessKind, integrand: &str, graph_count: usize) {
584 let mut state = self
585 .state
586 .lock()
587 .expect("aggregate generation progress state mutex is poisoned");
588 state.phase = Some(GenerationProgressPhase::Backend);
589 state.kind = Some(kind);
590 state.integrand.clear();
591 state.integrand.push_str(integrand);
592 state.total_graphs = graph_count;
593 state.done_graphs = 0;
594 state.total_cuts = None;
595 state.done_cuts = 0;
596 state.active_graphs.clear();
597 state.last_graph = None;
598 self.progress_span.pb_reset_elapsed();
599 self.refresh(&state);
600 }
601
602 fn backend_finished(&self, _kind: GenerationProcessKind, _integrand: &str, elapsed: Duration) {
603 let mut state = self
604 .state
605 .lock()
606 .expect("aggregate generation progress state mutex is poisoned");
607 state.done_graphs = state.total_graphs;
608 state.stats.total_time += elapsed;
609 state.stats.evaluator_compile_time += elapsed;
610 self.refresh(&state);
611 }
612}
613
614impl GenerationMonitor {
615 const POLL_INTERVAL: Duration = Duration::from_millis(100);
616
617 fn start() -> Result<Self> {
618 let pid = get_current_pid().map_err(|err| eyre!("Failed to resolve current pid: {err}"))?;
619 let peak_ram_bytes = Arc::new(AtomicU64::new(0));
620 let current_ram_bytes = Arc::new(AtomicU64::new(0));
621 let stop_requested = Arc::new(AtomicBool::new(false));
622 let peak_ram_bytes_for_thread = Arc::clone(&peak_ram_bytes);
623 let current_ram_bytes_for_thread = Arc::clone(¤t_ram_bytes);
624 let stop_requested_for_thread = Arc::clone(&stop_requested);
625
626 let handle = thread::Builder::new()
627 .name("generation-ram-monitor".to_string())
628 .spawn(move || {
629 let mut system = System::new();
630 loop {
631 if stop_requested_for_thread.load(Ordering::Relaxed) || is_interrupt_requested()
632 {
633 break;
634 }
635
636 system.refresh_processes_specifics(
637 ProcessesToUpdate::Some(&[pid]),
638 true,
639 ProcessRefreshKind::nothing().with_memory(),
640 );
641 if let Some(process) = system.process(pid) {
642 let memory = process.memory();
643 current_ram_bytes_for_thread.store(memory, Ordering::Relaxed);
644 peak_ram_bytes_for_thread.fetch_max(memory, Ordering::Relaxed);
645 }
646 thread::sleep(Self::POLL_INTERVAL);
647 }
648 })
649 .map_err(|err| eyre!("Failed to spawn generation RAM monitor: {err}"))?;
650
651 Ok(Self {
652 peak_ram_bytes,
653 current_ram_bytes,
654 stop_requested,
655 handle: Some(handle),
656 })
657 }
658
659 fn current_ram_bytes(&self) -> Arc<AtomicU64> {
660 Arc::clone(&self.current_ram_bytes)
661 }
662
663 fn peak_ram_bytes(&self) -> Arc<AtomicU64> {
664 Arc::clone(&self.peak_ram_bytes)
665 }
666
667 fn finish(&mut self) -> u64 {
668 self.stop_requested.store(true, Ordering::Relaxed);
669 if let Some(handle) = self.handle.take() {
670 let _ = handle.join();
671 }
672 self.peak_ram_bytes.load(Ordering::Relaxed)
673 }
674}
675
676impl Drop for GenerationMonitor {
677 fn drop(&mut self) {
678 let _ = self.finish();
679 }
680}
681
682#[derive(Debug, Clone, PartialEq, Eq, JsonSchema)]
683pub enum ProcessRef {
684 Id(usize),
685 Name(String),
686 Unqualified(String),
687}
688
689impl Serialize for ProcessRef {
690 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
691 where
692 S: serde::Serializer,
693 {
694 match self {
695 ProcessRef::Id(id) => serializer.serialize_u64(*id as u64),
696 ProcessRef::Name(name) => serializer.serialize_str(&format!("name:{name}")),
697 ProcessRef::Unqualified(value) => serializer.serialize_str(value),
698 }
699 }
700}
701
702impl<'de> Deserialize<'de> for ProcessRef {
703 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
704 where
705 D: serde::Deserializer<'de>,
706 {
707 use serde::de::{self, Visitor};
708 use std::fmt;
709
710 struct ProcessRefVisitor;
711
712 impl<'de> Visitor<'de> for ProcessRefVisitor {
713 type Value = ProcessRef;
714
715 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
716 formatter.write_str("a process reference string or numeric id")
717 }
718
719 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
720 where
721 E: de::Error,
722 {
723 Ok(ProcessRef::Id(value as usize))
724 }
725
726 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
727 where
728 E: de::Error,
729 {
730 ProcessRef::from_str(value).map_err(E::custom)
731 }
732
733 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
734 where
735 E: de::Error,
736 {
737 self.visit_str(&value)
738 }
739 }
740
741 deserializer.deserialize_any(ProcessRefVisitor)
742 }
743}
744
745#[cfg(feature = "python_api")]
746impl<'a, 'py> pyo3::FromPyObject<'a, 'py> for ProcessRef {
747 type Error = pyo3::PyErr;
748
749 fn extract(obj: pyo3::Borrowed<'a, 'py, pyo3::types::PyAny>) -> pyo3::PyResult<Self> {
750 if let Ok(process_id) = <usize as pyo3::FromPyObject<'a, 'py>>::extract(obj) {
751 return Ok(ProcessRef::Id(process_id));
752 }
753
754 let selector = <String as pyo3::FromPyObject<'a, 'py>>::extract(obj).map_err(|_| {
755 pyo3::exceptions::PyTypeError::new_err(
756 "process selectors must be either an integer process id or a string selector",
757 )
758 })?;
759 ProcessRef::from_str(&selector).map_err(pyo3::exceptions::PyValueError::new_err)
760 }
761}
762
763impl FromStr for ProcessRef {
764 type Err = String;
765
766 fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
767 if let Some(rest) = value.strip_prefix('#') {
768 let id = rest
769 .parse::<usize>()
770 .map_err(|_| format!("Invalid process id in '{value}'"))?;
771 return Ok(ProcessRef::Id(id));
772 }
773 if let Some(rest) = value.strip_prefix("name:") {
774 if rest.is_empty() {
775 return Err("Process name cannot be empty".to_string());
776 }
777 return Ok(ProcessRef::Name(rest.to_string()));
778 }
779 if value.is_empty() {
780 return Err("Process reference cannot be empty".to_string());
781 }
782 Ok(ProcessRef::Unqualified(value.to_string()))
783 }
784}
785
786#[test]
787fn try_complicated() {
788 "GGHHH3loop_no_iterative_optimization_3L"
789 .parse::<ProcessRef>()
790 .unwrap();
791 }
793
794impl ProcessRef {
795 pub fn resolve(&self, process_list: &ProcessList) -> Result<usize> {
796 let process_count = process_list.processes.len();
797 match self {
798 ProcessRef::Id(id) => {
799 if *id >= process_count {
800 return Err(eyre!(
801 "Process ID {} invalid, only {} processes available",
802 id,
803 process_count
804 ));
805 }
806 Ok(*id)
807 }
808 ProcessRef::Name(name) => process_list
809 .processes
810 .iter()
811 .position(|p| p.definition.folder_name == *name)
812 .ok_or_else(|| {
813 eyre!(
814 "No process named '{}'. Use 'display processes' to list available processes.",
815 name
816 )
817 }),
818 ProcessRef::Unqualified(value) => {
819 let name_match = process_list
820 .processes
821 .iter()
822 .position(|p| p.definition.folder_name == *value);
823 if let Ok(id) = value.parse::<usize>() {
824 let id_valid = id < process_count;
825 match (id_valid, name_match) {
826 (true, Some(_)) => Err(eyre!(
827 "Ambiguous process reference '{}'. Use '#{}' or 'name:{}' to disambiguate.",
828 value,
829 id,
830 value
831 )),
832 (true, None) => Ok(id),
833 (false, Some(index)) => Ok(index),
834 (false, None) => Err(eyre!(
835 "No process named '{}'. Use 'display processes' to list available processes.",
836 value
837 )),
838 }
839 } else if let Some(index) = name_match {
840 Ok(index)
841 } else {
842 Err(eyre!(
843 "No process named '{}'. Use 'display processes' to list available processes.",
844 value
845 ))
846 }
847 }
848 }
849 }
850}
851
852pub trait ProcessListExt {
853 fn find_integrand_ref(
854 &self,
855 process: Option<&ProcessRef>,
856 integrand_name: Option<&String>,
857 ) -> Result<(usize, String)>;
858 fn get_amplitude_mut_ref(
859 &mut self,
860 process: Option<&ProcessRef>,
861 integrand_name: Option<&String>,
862 ) -> Result<&mut Amplitude>;
863 fn get_cross_section_mut_ref(
864 &mut self,
865 process: Option<&ProcessRef>,
866 integrand_name: Option<&String>,
867 ) -> Result<&mut CrossSection>;
868}
869
870impl ProcessListExt for ProcessList {
871 fn find_integrand_ref(
872 &self,
873 process: Option<&ProcessRef>,
874 integrand_name: Option<&String>,
875 ) -> Result<(usize, String)> {
876 let process_id = match process {
877 Some(process_ref) => process_ref.resolve(self)?,
878 None => self.find_process(None)?,
879 };
880 let integrand_name = self.processes[process_id]
881 .collection
882 .find_integrand(integrand_name.cloned())
883 .with_note(|| format!("in process id {process_id}"))?;
884 Ok((process_id, integrand_name))
885 }
886
887 fn get_amplitude_mut_ref(
888 &mut self,
889 process: Option<&ProcessRef>,
890 integrand_name: Option<&String>,
891 ) -> Result<&mut Amplitude> {
892 let (process_id, integrand_name) = self.find_integrand_ref(process, integrand_name)?;
893 let process = &mut self.processes[process_id];
894 match &mut process.collection {
895 ProcessCollection::Amplitudes(amplitudes) => {
896 amplitudes.get_mut(&integrand_name).ok_or_else(|| {
897 eyre!(
898 "No amplitude named '{}' in process '{}'",
899 integrand_name,
900 process.definition.folder_name
901 )
902 })
903 }
904 ProcessCollection::CrossSections(_) => Err(eyre!(
905 "Process '{}' does not contain amplitudes",
906 process.definition.folder_name
907 )),
908 }
909 }
910
911 fn get_cross_section_mut_ref(
912 &mut self,
913 process: Option<&ProcessRef>,
914 integrand_name: Option<&String>,
915 ) -> Result<&mut CrossSection> {
916 let (process_id, integrand_name) = self.find_integrand_ref(process, integrand_name)?;
917 let process = &mut self.processes[process_id];
918 match &mut process.collection {
919 ProcessCollection::CrossSections(crosssections) => {
920 crosssections.get_mut(&integrand_name).ok_or_else(|| {
921 eyre!(
922 "No cross section named '{}' in process '{}'",
923 integrand_name,
924 process.definition.folder_name
925 )
926 })
927 }
928 ProcessCollection::Amplitudes(_) => Err(eyre!(
929 "Process '{}' does not contain crosssections",
930 process.definition.folder_name
931 )),
932 }
933 }
934}
935
936pub trait SyncSettings {
937 fn sync_settings(&self) -> Result<()>;
938}
939
940impl SyncSettings for CLISettings {
941 fn sync_settings(&self) -> Result<()> {
942 set_file_log_filter(&self.global.logfile_directive)?;
944 set_stderr_log_filter(&self.global.display_directive)?;
945 set_log_style(self.global.log_style.to_runtime());
946 Ok(())
947 }
948}
949
950static SERIALIZE_COMMANDS_AS_STRINGS: AtomicBool = AtomicBool::new(false);
952
953fn is_command_blocks_empty(command_blocks: &[CommandsBlock]) -> bool {
954 command_blocks.is_empty()
955}
956
957fn should_persist_command(command: &Commands) -> bool {
958 !matches!(
959 command,
960 Commands::Quit(_) | Commands::StartCommandsBlock(_) | Commands::FinishCommandsBlock
961 )
962}
963
964pub fn set_serialize_commands_as_strings(value: bool) {
966 SERIALIZE_COMMANDS_AS_STRINGS.store(value, std::sync::atomic::Ordering::Relaxed);
967}
968
969pub fn get_serialize_commands_as_strings() -> bool {
971 SERIALIZE_COMMANDS_AS_STRINGS.load(std::sync::atomic::Ordering::Relaxed)
972}
973
974pub struct SerializeCommandsAsStringsGuard {
975 previous: bool,
976}
977
978impl SerializeCommandsAsStringsGuard {
979 pub fn new(value: bool) -> Self {
980 let previous = get_serialize_commands_as_strings();
981 set_serialize_commands_as_strings(value);
982 Self { previous }
983 }
984}
985
986impl Drop for SerializeCommandsAsStringsGuard {
987 fn drop(&mut self) {
988 set_serialize_commands_as_strings(self.previous);
989 }
990}
991
992#[derive(Debug, Clone, JsonSchema, PartialEq)]
998pub struct CommandHistory {
999 pub command: Commands,
1001 pub raw_string: Option<String>,
1003}
1004
1005impl Serialize for CommandHistory {
1006 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1007 where
1008 S: serde::Serializer,
1009 {
1010 if get_serialize_commands_as_strings() {
1011 if let Some(ref raw_string) = self.raw_string {
1012 raw_string.serialize(serializer)
1013 } else {
1014 self.command.serialize(serializer)
1015 }
1016 } else {
1017 self.command.serialize(serializer)
1018 }
1019 }
1020}
1021
1022impl<'de> Deserialize<'de> for CommandHistory {
1023 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1024 where
1025 D: serde::Deserializer<'de>,
1026 {
1027 use serde::de::{self, Visitor};
1028 use std::fmt;
1029
1030 struct CommandHistoryVisitor;
1031
1032 impl<'de> Visitor<'de> for CommandHistoryVisitor {
1033 type Value = CommandHistory;
1034
1035 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1036 formatter.write_str("a string or a Commands structure")
1037 }
1038
1039 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1040 where
1041 E: de::Error,
1042 {
1043 CommandHistory::from_raw_string(value).map_err(|err| {
1044 E::custom(format!(
1045 "Failed to parse command string '{}': {}",
1046 value, err
1047 ))
1048 })
1049 }
1050
1051 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
1052 where
1053 E: de::Error,
1054 {
1055 self.visit_str(&value)
1056 }
1057
1058 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
1059 where
1060 A: de::SeqAccess<'de>,
1061 {
1062 let command =
1064 Commands::deserialize(de::value::SeqAccessDeserializer::new(&mut seq))?;
1065 Ok(CommandHistory {
1066 command,
1067 raw_string: None,
1068 })
1069 }
1070
1071 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
1072 where
1073 A: de::MapAccess<'de>,
1074 {
1075 let command = Commands::deserialize(de::value::MapAccessDeserializer::new(map))?;
1077 Ok(CommandHistory {
1078 command,
1079 raw_string: None,
1080 })
1081 }
1082 }
1083
1084 deserializer.deserialize_any(CommandHistoryVisitor)
1085 }
1086}
1087
1088impl CommandHistory {
1089 pub fn new(command: Commands) -> Self {
1091 Self {
1092 command,
1093 raw_string: None,
1094 }
1095 }
1096
1097 pub fn new_with_raw(command: Commands, raw_string: String) -> Self {
1099 Self {
1100 command,
1101 raw_string: Some(raw_string),
1102 }
1103 }
1104
1105 pub fn from_command(command: Commands) -> Self {
1107 Self::new(command)
1108 }
1109
1110 pub fn from_raw_string(raw_string: &str) -> Result<Self, clap::Error> {
1115 use crate::Repl;
1116 use clap::error::ErrorKind;
1117 use clap::Parser;
1118
1119 let args = split_command_line(raw_string)
1120 .map(normalize_clap_args)
1121 .map_err(|_| {
1122 clap::Error::raw(
1123 ErrorKind::InvalidValue,
1124 "Could not parse command: unmatched quotes or trailing escape",
1125 )
1126 })?;
1127 let cli = Repl::try_parse_from(
1128 std::iter::once("gammaloop").chain(args.iter().map(String::as_str)),
1129 )?;
1130
1131 Ok(Self::new_with_raw(cli.command, raw_string.into()))
1132 }
1133}
1134
1135#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
1136#[serde(default, deny_unknown_fields)]
1137pub struct RunHistory {
1138 #[serde(skip_serializing_if = "IsDefault::is_default")]
1140 pub default_runtime_settings: RuntimeSettings,
1141
1142 #[serde(skip_serializing_if = "IsDefault::is_default")]
1144 pub cli_settings: CLISettings,
1145
1146 #[serde(skip_serializing_if = "is_command_blocks_empty")]
1148 pub command_blocks: Vec<CommandsBlock>,
1149 pub commands: Vec<CommandHistory>,
1153}
1154
1155#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema, PartialEq)]
1156#[serde(default, deny_unknown_fields)]
1157pub struct CommandsBlock {
1158 pub name: String,
1159 pub commands: Vec<CommandHistory>,
1160}
1161
1162#[derive(Debug, Deserialize, Default)]
1163#[serde(default, deny_unknown_fields)]
1164struct RawRunHistoryToml {
1165 default_runtime_settings: RuntimeSettings,
1166 cli_settings: CLISettings,
1167 command_blocks: Vec<RawCommandsBlockToml>,
1168 commands: Vec<TomlValue>,
1169}
1170
1171#[derive(Debug, Deserialize, Default)]
1172#[serde(default, deny_unknown_fields)]
1173struct RawCommandsBlockToml {
1174 name: String,
1175 commands: Vec<TomlValue>,
1176}
1177
1178impl CommandsBlock {
1179 pub fn semantically_eq(&self, other: &Self) -> bool {
1180 self.name == other.name
1181 && self.commands.len() == other.commands.len()
1182 && self
1183 .commands
1184 .iter()
1185 .zip(other.commands.iter())
1186 .all(|(left, right)| left.command == right.command)
1187 }
1188}
1189
1190impl SmartSerde for RunHistory {
1191 fn has_schema_path(&self, online: bool) -> Option<Result<PathBuf>> {
1192 Some(get_schema_folder(online).map(|f| f.join("runhistory.json")))
1193 }
1194}
1195
1196impl RunHistory {
1197 pub fn freeze_boot_settings_from(&mut self, boot_run_history: &RunHistory) {
1198 self.cli_settings.global = boot_run_history.cli_settings.global.clone();
1199 self.default_runtime_settings = boot_run_history.default_runtime_settings.clone();
1200 }
1201
1202 pub fn frozen_boot_settings_match(&self, boot_run_history: &RunHistory) -> bool {
1203 self.cli_settings.global == boot_run_history.cli_settings.global
1204 && self.default_runtime_settings == boot_run_history.default_runtime_settings
1205 }
1206
1207 pub fn push(&mut self, command: Commands) {
1209 self.push_with_raw(command, None);
1210 }
1211
1212 pub fn push_with_raw(&mut self, command: Commands, raw_string: Option<String>) {
1217 if should_persist_command(&command) {
1218 self.commands.push(CommandHistory {
1219 command,
1220 raw_string,
1221 });
1222 }
1223 }
1224
1225 pub fn schema() -> Schema {
1226 schema_for!(RunHistory)
1227 }
1228
1229 pub fn validate(&self) -> Result<()> {
1230 let mut seen_names = HashSet::with_capacity(self.command_blocks.len());
1231 for block in &self.command_blocks {
1232 if block.name.trim().is_empty() {
1233 return Err(eyre!(
1234 "Run card `command_blocks` contains a block with an empty name"
1235 ));
1236 }
1237 if !seen_names.insert(block.name.clone()) {
1238 return Err(eyre!(
1239 "Run card `command_blocks` contains duplicate block name '{}'",
1240 block.name
1241 ));
1242 }
1243 }
1244 Ok(())
1245 }
1246
1247 pub fn command_block(&self, name: &str) -> Option<&CommandsBlock> {
1248 self.command_blocks.iter().find(|block| block.name == name)
1249 }
1250
1251 pub fn select_command_blocks(
1252 &self,
1253 selected_block_names: &[String],
1254 ) -> Result<Vec<CommandsBlock>> {
1255 let mut selected = Vec::with_capacity(selected_block_names.len());
1256 for name in selected_block_names {
1257 let block = self.command_block(name).ok_or_else(|| {
1258 eyre!(
1259 "Unknown command block '{}'. Available command blocks: {}",
1260 name,
1261 self.command_blocks
1262 .iter()
1263 .map(|block| block.name.as_str())
1264 .collect::<Vec<_>>()
1265 .join(", ")
1266 )
1267 })?;
1268 selected.push(block.clone());
1269 }
1270 Ok(selected)
1271 }
1272
1273 pub fn conflicting_command_block_names(&self, command_blocks: &[CommandsBlock]) -> Vec<String> {
1274 command_blocks
1275 .iter()
1276 .filter_map(|new_block| match self.command_block(&new_block.name) {
1277 Some(existing_block) if !existing_block.semantically_eq(new_block) => {
1278 Some(new_block.name.clone())
1279 }
1280 _ => None,
1281 })
1282 .collect()
1283 }
1284
1285 pub fn merge_command_blocks_with_overwrite(
1286 &mut self,
1287 command_blocks: &[CommandsBlock],
1288 overwrite_conflicts: bool,
1289 ) -> Result<()> {
1290 for new_block in command_blocks {
1291 match self
1292 .command_blocks
1293 .iter()
1294 .position(|existing| existing.name == new_block.name)
1295 {
1296 Some(index) if self.command_blocks[index].semantically_eq(new_block) => {}
1297 Some(index) if overwrite_conflicts => {
1298 self.command_blocks[index] = new_block.clone();
1299 }
1300 Some(_) => {
1301 return Err(eyre!(
1302 "Run card command block '{}' redefines an existing block with different commands",
1303 new_block.name
1304 ));
1305 }
1306 None => self.command_blocks.push(new_block.clone()),
1307 }
1308 }
1309 self.validate()
1310 }
1311
1312 pub fn merge_command_blocks(&mut self, command_blocks: &[CommandsBlock]) -> Result<()> {
1313 self.merge_command_blocks_with_overwrite(command_blocks, false)
1314 }
1315
1316 pub fn apply_session_settings(
1317 &self,
1318 global_settings: &mut CLISettings,
1319 default_runtime_settings: &mut RuntimeSettings,
1320 ) -> Result<()> {
1321 if self.cli_settings.global != GlobalSettings::default() {
1322 global_settings.global = self.cli_settings.global.clone();
1323 global_settings.sync_settings()?;
1324 }
1325 if self.default_runtime_settings != RuntimeSettings::default() {
1326 *default_runtime_settings = self.default_runtime_settings.clone();
1327 }
1328 Ok(())
1329 }
1330
1331 pub fn run(
1332 &mut self,
1333 state: &mut State,
1334 global_settings: &mut CLISettings,
1335 default_runtime_settings: &mut RuntimeSettings,
1336 ) -> Result<ControlFlow<SaveState>> {
1337 let mut session_state = crate::session::CliSessionState::default();
1338 let mut session = crate::session::CliSession::new(
1339 state,
1340 self,
1341 global_settings,
1342 default_runtime_settings,
1343 &mut session_state,
1344 );
1345 session.replay_run_history()
1346 }
1347
1348 pub(crate) fn filtered_for_save(&self) -> Self {
1349 let mut filtered = self.clone();
1350 filtered
1351 .commands
1352 .retain(|command_history| should_persist_command(&command_history.command));
1353 filtered
1354 }
1355
1356 pub(crate) fn to_toml_string(&self, serialize_commands_as_strings: bool) -> Result<String> {
1357 let _serialize_commands_guard =
1358 SerializeCommandsAsStringsGuard::new(serialize_commands_as_strings);
1359 render_smart_toml(&self.filtered_for_save())
1360 }
1361
1362 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
1363 let path = path.as_ref();
1364 debug!("Loaded run history from file {}", path.display());
1365
1366 let runhistory = match path.extension().and_then(|ext| ext.to_str()) {
1367 Some("toml") => Self::load_toml(path)?,
1368 _ => Self::from_file(path, "run history")?,
1369 };
1370 runhistory.validate()?;
1371 Ok(runhistory)
1372 }
1373
1374 pub fn save_toml(
1375 &self,
1376 root_folder: &Path,
1377 override_state_file: bool,
1378 _strict: bool,
1379 ) -> Result<()> {
1380 self.filtered_for_save()
1381 .to_file(root_folder.join("run.toml"), override_state_file)?;
1382
1383 Ok(())
1385 }
1386
1387 pub fn save_yaml(
1388 &self,
1389 root_folder: &Path,
1390 override_state_file: bool,
1391 _strict: bool,
1392 ) -> Result<()> {
1393 self.filtered_for_save()
1394 .to_file(root_folder.join("run.yaml"), override_state_file)?;
1395 Ok(())
1397 }
1398
1399 fn load_toml(path: &Path) -> Result<Self> {
1400 let raw = fs::read_to_string(path)
1401 .with_context(|| format!("Could not open run history file {}", path.display()))?;
1402 let raw_run_history: RawRunHistoryToml =
1403 toml::from_str(&raw).wrap_err("Error parsing run history toml")?;
1404
1405 let commands = raw_run_history
1406 .commands
1407 .into_iter()
1408 .enumerate()
1409 .map(|(index, value)| {
1410 parse_toml_command_history(value, &format!("top-level command #{}", index + 1))
1411 })
1412 .collect::<Result<Vec<_>>>()?;
1413
1414 let command_blocks = raw_run_history
1415 .command_blocks
1416 .into_iter()
1417 .map(|block| {
1418 let RawCommandsBlockToml { name, commands } = block;
1419 let commands = commands
1420 .into_iter()
1421 .enumerate()
1422 .map(|(index, value)| {
1423 parse_toml_command_history(
1424 value,
1425 &format!("command block '{}' command #{}", name, index + 1),
1426 )
1427 })
1428 .collect::<Result<Vec<_>>>()?;
1429 Ok(CommandsBlock { name, commands })
1430 })
1431 .collect::<Result<Vec<_>>>()?;
1432
1433 Ok(Self {
1434 default_runtime_settings: raw_run_history.default_runtime_settings,
1435 cli_settings: raw_run_history.cli_settings,
1436 command_blocks,
1437 commands,
1438 })
1439 }
1440}
1441
1442fn parse_toml_command_history(value: TomlValue, context: &str) -> Result<CommandHistory> {
1443 match value {
1444 TomlValue::String(raw) => CommandHistory::from_raw_string(&raw)
1445 .map_err(|err| eyre!("Failed to parse {} '{}': {}", context, raw, err)),
1446 other => other
1447 .try_into::<CommandHistory>()
1448 .map_err(|err| eyre!("Failed to parse {}: {}", context, err)),
1449 }
1450}
1451
1452#[cfg_attr(
1453 feature = "python_api",
1454 pyo3::pyclass(from_py_object, unsendable, name = "GammaLoopState")
1455)]
1456#[derive(Clone)]
1457pub struct State {
1458 pub model: Model,
1460 pub model_parameters: InputParamCard<F<f64>>,
1462 pub process_list: ProcessList,
1464 pub generation_summaries: BTreeMap<IntegrandGenerationSummaryKey, IntegrandGenerationSummary>,
1466}
1467
1468const STATE_MANIFEST_FILE: &str = "state_manifest.toml";
1469const INTEGRAND_GENERATION_SUMMARY_FILE: &str = "generation_summary.json";
1470const CURRENT_STATE_MANIFEST_VERSION: u32 = 1;
1471const GENERATION_THREAD_STACK_SIZE_BYTES: usize = 32 * 1024 * 1024;
1472
1473#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1474#[serde(default, deny_unknown_fields)]
1475struct StateManifest {
1476 version: u32,
1477}
1478
1479impl Default for StateManifest {
1480 fn default() -> Self {
1481 Self {
1482 version: CURRENT_STATE_MANIFEST_VERSION,
1483 }
1484 }
1485}
1486
1487fn ensure_supported_state_manifest_version(manifest: &StateManifest) -> Result<()> {
1488 if manifest.version > CURRENT_STATE_MANIFEST_VERSION {
1489 return Err(eyre!(
1490 "State version {} is newer than this binary supports (max {}). Please upgrade gammaloop.",
1491 manifest.version,
1492 CURRENT_STATE_MANIFEST_VERSION
1493 ));
1494 }
1495
1496 Ok(())
1497}
1498
1499fn run_state_migration_checks(manifest: &StateManifest, save_path: &Path) -> Result<()> {
1500 ensure_supported_state_manifest_version(manifest)?;
1501
1502 match manifest.version {
1503 1 => {
1504 if !save_path.join("symbolica_state.bin").exists() {
1505 return Err(eyre!(
1506 "Saved state at '{}' is missing required file symbolica_state.bin",
1507 save_path.display()
1508 ));
1509 }
1510 if !save_path.join("processes").exists() {
1511 return Err(eyre!(
1512 "Saved state at '{}' is missing required folder processes/",
1513 save_path.display()
1514 ));
1515 }
1516 if !save_path.join("model.json").exists() {
1517 return Err(eyre!(
1518 "Saved state at '{}' is missing required file model.json",
1519 save_path.display()
1520 ));
1521 }
1522 Ok(())
1523 }
1524 _ => Err(eyre!(
1525 "State version {} is not supported by this binary.",
1526 manifest.version
1527 )),
1528 }
1529}
1530
1531#[derive(Debug, Clone, PartialEq, Eq)]
1532pub enum StateFolderKind {
1533 Missing,
1535 Scratch,
1537 Unmanifested,
1539 Saved,
1541 Invalid(String),
1543}
1544
1545fn is_scratch_state_entry(entry: &fs::DirEntry) -> bool {
1546 entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
1547 && entry.file_name().to_string_lossy() == "logs"
1548}
1549
1550pub fn classify_state_folder(save_path: &Path) -> Result<StateFolderKind> {
1551 if !save_path.exists() {
1552 return Ok(StateFolderKind::Missing);
1553 }
1554 if !save_path.is_dir() {
1555 return Ok(StateFolderKind::Invalid(format!(
1556 "'{}' exists but is not a directory",
1557 save_path.display()
1558 )));
1559 }
1560
1561 let manifest_path = save_path.join(STATE_MANIFEST_FILE);
1562 if manifest_path.exists() {
1563 let manifest = load_state_manifest(save_path)?;
1564 return Ok(match run_state_migration_checks(&manifest, save_path) {
1565 Ok(()) => StateFolderKind::Saved,
1566 Err(err) => StateFolderKind::Invalid(err.to_string()),
1567 });
1568 }
1569
1570 let mut entries = fs::read_dir(save_path)
1571 .with_context(|| format!("Trying to read state folder '{}'", save_path.display()))?;
1572 if entries.by_ref().all(|entry| {
1573 entry
1574 .map(|entry| is_scratch_state_entry(&entry))
1575 .unwrap_or(false)
1576 }) {
1577 return Ok(StateFolderKind::Scratch);
1578 }
1579
1580 Ok(StateFolderKind::Unmanifested)
1581}
1582
1583fn load_state_manifest(save_path: &Path) -> Result<StateManifest> {
1584 let manifest_path = save_path.join(STATE_MANIFEST_FILE);
1585 let raw_manifest = fs::read_to_string(&manifest_path).with_context(|| {
1586 format!(
1587 "Trying to read state manifest file {}",
1588 manifest_path.display()
1589 )
1590 })?;
1591 let manifest = toml::from_str::<StateManifest>(&raw_manifest).with_context(|| {
1592 format!(
1593 "Trying to parse state manifest file {}",
1594 manifest_path.display()
1595 )
1596 })?;
1597 ensure_supported_state_manifest_version(&manifest)?;
1598 Ok(manifest)
1599}
1600
1601fn save_state_manifest(save_path: &Path) -> Result<()> {
1602 let manifest = StateManifest::default();
1603 let raw_manifest =
1604 toml::to_string_pretty(&manifest).context("Trying to serialize state manifest to TOML")?;
1605 fs::write(save_path.join(STATE_MANIFEST_FILE), raw_manifest).with_context(|| {
1606 format!(
1607 "Trying to write state manifest file {}",
1608 save_path.join(STATE_MANIFEST_FILE).display()
1609 )
1610 })?;
1611 Ok(())
1612}
1613
1614fn integrand_generation_summary_path(
1615 root_folder: &Path,
1616 process: &Process,
1617 integrand_name: &str,
1618) -> PathBuf {
1619 let process_kind_folder = match &process.collection {
1620 ProcessCollection::Amplitudes(_) => "amplitudes",
1621 ProcessCollection::CrossSections(_) => "cross_sections",
1622 };
1623
1624 root_folder
1625 .join("processes")
1626 .join(process_kind_folder)
1627 .join(&process.definition.folder_name)
1628 .join(integrand_name)
1629 .join(INTEGRAND_GENERATION_SUMMARY_FILE)
1630}
1631
1632fn process_kind_folder(root_folder: &Path, process: &Process) -> PathBuf {
1633 let process_kind_folder = match &process.collection {
1634 ProcessCollection::Amplitudes(_) => "amplitudes",
1635 ProcessCollection::CrossSections(_) => "cross_sections",
1636 };
1637
1638 root_folder.join("processes").join(process_kind_folder)
1639}
1640
1641fn process_artifact_folder(root_folder: &Path, process: &Process) -> PathBuf {
1642 process_kind_folder(root_folder, process).join(&process.definition.folder_name)
1643}
1644
1645fn integrand_artifact_folder(
1646 root_folder: &Path,
1647 process: &Process,
1648 integrand_name: &str,
1649) -> PathBuf {
1650 process_artifact_folder(root_folder, process).join(integrand_name)
1651}
1652
1653fn generated_integrand_artifact_path(
1654 root_folder: &Path,
1655 process: &Process,
1656 integrand_name: &str,
1657) -> PathBuf {
1658 integrand_artifact_folder(root_folder, process, integrand_name).join("integrand")
1659}
1660
1661fn ensure_existing_path_under_root(path: &Path, root: &Path, description: &str) -> Result<PathBuf> {
1662 let canonical_root = root
1663 .canonicalize()
1664 .with_context(|| format!("Trying to resolve artifact cleanup root {}", root.display()))?;
1665 let canonical_path = path.canonicalize().with_context(|| {
1666 format!(
1667 "Trying to resolve {} path {} before removal",
1668 description,
1669 path.display()
1670 )
1671 })?;
1672 if !canonical_path.starts_with(&canonical_root) {
1673 return Err(eyre!(
1674 "Refusing to remove {} path {} because it resolves outside {}",
1675 description,
1676 canonical_path.display(),
1677 canonical_root.display()
1678 ));
1679 }
1680 Ok(canonical_path)
1681}
1682
1683fn remove_file_if_exists_under_root(path: &Path, root: &Path, description: &str) -> Result<bool> {
1684 if !path.try_exists().with_context(|| {
1685 format!(
1686 "Trying to check whether {} path {} exists",
1687 description,
1688 path.display()
1689 )
1690 })? {
1691 return Ok(false);
1692 }
1693 let canonical_path = ensure_existing_path_under_root(path, root, description)?;
1694 fs::remove_file(path).with_context(|| {
1695 format!(
1696 "Trying to remove {} path {}",
1697 description,
1698 canonical_path.display()
1699 )
1700 })?;
1701 Ok(true)
1702}
1703
1704fn remove_dir_if_exists_under_root(path: &Path, root: &Path, description: &str) -> Result<bool> {
1705 if !path.try_exists().with_context(|| {
1706 format!(
1707 "Trying to check whether {} path {} exists",
1708 description,
1709 path.display()
1710 )
1711 })? {
1712 return Ok(false);
1713 }
1714 let canonical_path = ensure_existing_path_under_root(path, root, description)?;
1715 fs::remove_dir_all(path).with_context(|| {
1716 format!(
1717 "Trying to remove {} path {}",
1718 description,
1719 canonical_path.display()
1720 )
1721 })?;
1722 Ok(true)
1723}
1724
1725fn remove_saved_process_artifacts(root_folder: &Path, process: &Process) -> Result<bool> {
1726 let root = process_kind_folder(root_folder, process);
1727 let path = process_artifact_folder(root_folder, process);
1728 remove_dir_if_exists_under_root(&path, &root, "saved process artifacts")
1729}
1730
1731fn remove_saved_integrand_artifacts(
1732 root_folder: &Path,
1733 process: &Process,
1734 integrand_name: &str,
1735) -> Result<bool> {
1736 let root = process_artifact_folder(root_folder, process);
1737 let path = integrand_artifact_folder(root_folder, process, integrand_name);
1738 remove_dir_if_exists_under_root(&path, &root, "saved integrand artifacts")
1739}
1740
1741fn validate_output_name(value: &str, flag_name: &str) -> Result<()> {
1742 if value.trim().is_empty() {
1743 return Err(eyre!("{flag_name} must not be empty"));
1744 }
1745 Ok(())
1746}
1747
1748fn rename_process_integrand(integrand: Option<&mut ProcessIntegrand>, new_name: &str) {
1749 let Some(integrand) = integrand else {
1750 return;
1751 };
1752 let _ = integrand.get_mut_settings();
1753 match integrand {
1754 ProcessIntegrand::Amplitude(amplitude) => {
1755 amplitude.data.name = new_name.to_string();
1756 }
1757 ProcessIntegrand::CrossSection(cross_section) => {
1758 cross_section.data.name = new_name.to_string();
1759 }
1760 }
1761}
1762
1763fn load_integrand_generation_summaries(
1764 root_folder: &Path,
1765 process_list: &ProcessList,
1766) -> Result<BTreeMap<IntegrandGenerationSummaryKey, IntegrandGenerationSummary>> {
1767 let mut summaries = BTreeMap::new();
1768
1769 for (process_id, process) in process_list.processes.iter().enumerate() {
1770 for integrand_name in process.collection.get_integrand_names() {
1771 let summary_path =
1772 integrand_generation_summary_path(root_folder, process, integrand_name);
1773 if !summary_path.exists() {
1774 continue;
1775 }
1776
1777 let raw_summary = fs::read_to_string(&summary_path).with_context(|| {
1778 format!(
1779 "Trying to read integrand generation summary {}",
1780 summary_path.display()
1781 )
1782 })?;
1783 let summary = serde_json::from_str(&raw_summary).with_context(|| {
1784 format!(
1785 "Trying to parse integrand generation summary {}",
1786 summary_path.display()
1787 )
1788 })?;
1789 summaries.insert(
1790 IntegrandGenerationSummaryKey {
1791 process_id,
1792 integrand_name: integrand_name.to_string(),
1793 },
1794 summary,
1795 );
1796 }
1797 }
1798
1799 Ok(summaries)
1800}
1801
1802impl State {
1803 fn overridable_model_parameter_names(&self) -> Vec<String> {
1804 let mut names = self
1805 .model_parameters
1806 .keys()
1807 .map(|symbol| symbol.to_string())
1808 .collect::<Vec<_>>();
1809 names.sort();
1810 names
1811 }
1812
1813 fn resolve_overridable_model_parameter_type(
1814 &self,
1815 parameter_name: &str,
1816 ) -> Result<gammalooprs::model::ParameterType> {
1817 let possibilities = self.overridable_model_parameter_names();
1818 let parameter_type = external_model_parameter_type(&self.model, parameter_name)
1819 .ok_or_else(|| eyre!("No model parameter named '{parameter_name}'"))
1820 .with_note(|| {
1821 format!(
1822 "Possible model parameters are: {}",
1823 possibilities.join(", ")
1824 )
1825 })?;
1826
1827 let symbol = UFOSymbol::from(parameter_name);
1828 if !self.model_parameters.contains_key(&symbol) {
1829 return Err(eyre!(
1830 "Model parameter '{parameter_name}' cannot be overridden because it is not present in the shared top-level model_parameters.json"
1831 ))
1832 .with_note(|| format!("Possible model parameters are: {}", possibilities.join(", ")));
1833 }
1834
1835 Ok(parameter_type)
1836 }
1837
1838 pub fn find_generated_integrand_ref_by_name(
1839 &self,
1840 integrand_name: &str,
1841 ) -> Result<(usize, String)> {
1842 let matches = self
1843 .process_list
1844 .processes
1845 .iter()
1846 .enumerate()
1847 .filter_map(|(process_id, process)| {
1848 let found = match &process.collection {
1849 ProcessCollection::Amplitudes(amplitudes) => amplitudes
1850 .get(integrand_name)
1851 .filter(|amplitude| amplitude.integrand.is_some())
1852 .map(|_| (process_id, process.definition.folder_name.clone())),
1853 ProcessCollection::CrossSections(cross_sections) => cross_sections
1854 .get(integrand_name)
1855 .filter(|cross_section| cross_section.integrand.is_some())
1856 .map(|_| (process_id, process.definition.folder_name.clone())),
1857 };
1858 found.map(|(process_id, process_name)| {
1859 (process_id, process_name, integrand_name.to_string())
1860 })
1861 })
1862 .collect::<Vec<_>>();
1863
1864 match matches.as_slice() {
1865 [(process_id, _, canonical_name)] => Ok((*process_id, canonical_name.clone())),
1866 [] => Err(eyre!(
1867 "No generated integrand named '{integrand_name}' was found. Per-integrand model parameters are only supported for generated integrands."
1868 )),
1869 _ => {
1870 let names = matches
1871 .iter()
1872 .map(|(process_id, process_name, _)| format!("#{process_id} ({process_name})"))
1873 .collect::<Vec<_>>();
1874 Err(eyre!(
1875 "Integrand name '{integrand_name}' is ambiguous across generated integrands"
1876 ))
1877 .with_note(|| format!("Matching processes: {}", names.join(", ")))
1878 }
1879 }
1880 }
1881
1882 pub fn import_model(&mut self, path: impl AsRef<Path>) -> Result<()> {
1883 self.model = Model::from_file(path)?;
1884 Ok(())
1885 }
1886
1887 fn matching_process_ids(&self, process: Option<&ProcessRef>) -> Result<Vec<usize>> {
1888 let Some(process) = process else {
1889 return Ok((0..self.process_list.processes.len()).collect());
1890 };
1891
1892 match process {
1893 ProcessRef::Id(id) => Ok((*id < self.process_list.processes.len())
1894 .then_some(*id)
1895 .into_iter()
1896 .collect()),
1897 ProcessRef::Name(name) => Ok(self
1898 .process_list
1899 .processes
1900 .iter()
1901 .position(|p| p.definition.folder_name == *name)
1902 .into_iter()
1903 .collect()),
1904 ProcessRef::Unqualified(value) => {
1905 let name_match = self
1906 .process_list
1907 .processes
1908 .iter()
1909 .position(|p| p.definition.folder_name == *value);
1910 if let Ok(id) = value.parse::<usize>() {
1911 let id_valid = id < self.process_list.processes.len();
1912 match (id_valid, name_match) {
1913 (true, Some(_)) => Err(eyre!(
1914 "Ambiguous process reference '{}'. Use '#{}' or 'name:{}' to disambiguate.",
1915 value,
1916 id,
1917 value
1918 )),
1919 (true, None) => Ok(vec![id]),
1920 (false, Some(index)) => Ok(vec![index]),
1921 (false, None) => Ok(vec![]),
1922 }
1923 } else {
1924 Ok(name_match.into_iter().collect())
1925 }
1926 }
1927 }
1928 }
1929
1930 pub fn remove_selected_integrands(
1931 &mut self,
1932 process: Option<&ProcessRef>,
1933 integrand_name: Option<&str>,
1934 ) -> Result<Vec<RemovedIntegrand>> {
1935 let process_ids = self.matching_process_ids(process)?;
1936 let mut removed = Vec::new();
1937
1938 for process_id in process_ids.into_iter().rev() {
1939 let process_name = self.process_list.processes[process_id]
1940 .definition
1941 .folder_name
1942 .clone();
1943 let integrand_names = {
1944 let collection = &self.process_list.processes[process_id].collection;
1945 match integrand_name {
1946 Some(name) => collection
1947 .get_integrand_names()
1948 .into_iter()
1949 .find(|candidate| *candidate == name)
1950 .map(|name| vec![name.to_string()])
1951 .unwrap_or_default(),
1952 None => collection
1953 .get_integrand_names()
1954 .into_iter()
1955 .map(str::to_string)
1956 .collect(),
1957 }
1958 };
1959
1960 if integrand_names.is_empty() {
1961 continue;
1962 }
1963
1964 {
1965 let process_entry = &mut self.process_list.processes[process_id];
1966 for integrand_name in &integrand_names {
1967 process_entry.collection.remove_integrand(integrand_name)?;
1968 }
1969 }
1970 for integrand_name in &integrand_names {
1971 self.generation_summaries
1972 .remove(&IntegrandGenerationSummaryKey {
1973 process_id,
1974 integrand_name: integrand_name.clone(),
1975 });
1976 }
1977
1978 let removed_empty_process = self.process_list.processes[process_id]
1979 .collection
1980 .get_integrand_names()
1981 .is_empty();
1982 if removed_empty_process {
1983 self.process_list.processes.remove(process_id);
1984 self.remove_generation_summaries_for_process(process_id);
1985 }
1986
1987 for integrand_name in integrand_names {
1988 removed.push(RemovedIntegrand {
1989 process_id,
1990 process_name: process_name.clone(),
1991 integrand_name,
1992 removed_empty_process,
1993 });
1994 }
1995 }
1996
1997 removed.reverse();
1998 Ok(removed)
1999 }
2000
2001 pub fn remove_process(&mut self, process: Option<&ProcessRef>) -> Result<RemovedProcess> {
2002 let process_id = self.resolve_process_ref(process)?;
2003 let removed = self.process_list.processes.remove(process_id);
2004 self.remove_generation_summaries_for_process(process_id);
2005 Ok(RemovedProcess {
2006 process_id,
2007 process_name: removed.definition.folder_name,
2008 })
2009 }
2010
2011 pub fn remove_integrand(
2012 &mut self,
2013 process: &ProcessRef,
2014 integrand_name: &str,
2015 ) -> Result<RemovedIntegrand> {
2016 let process_id = process.resolve(&self.process_list)?;
2017 let (process_name, canonical_integrand_name, removed_empty_process) = {
2018 let process_entry = &mut self.process_list.processes[process_id];
2019 let process_name = process_entry.definition.folder_name.clone();
2020 let canonical_integrand_name = process_entry
2021 .collection
2022 .find_integrand(Some(integrand_name.to_string()))?;
2023 process_entry
2024 .collection
2025 .remove_integrand(&canonical_integrand_name)?;
2026 let removed_empty_process = process_entry.collection.get_integrand_names().is_empty();
2027 (
2028 process_name,
2029 canonical_integrand_name,
2030 removed_empty_process,
2031 )
2032 };
2033 self.generation_summaries
2034 .remove(&IntegrandGenerationSummaryKey {
2035 process_id,
2036 integrand_name: canonical_integrand_name.clone(),
2037 });
2038
2039 if removed_empty_process {
2040 self.process_list.processes.remove(process_id);
2041 self.remove_generation_summaries_for_process(process_id);
2042 }
2043
2044 Ok(RemovedIntegrand {
2045 process_id,
2046 process_name,
2047 integrand_name: canonical_integrand_name,
2048 removed_empty_process,
2049 })
2050 }
2051
2052 pub fn resolve_process_ref(&self, process: Option<&ProcessRef>) -> Result<usize> {
2053 match process {
2054 Some(process_ref) => process_ref.resolve(&self.process_list),
2055 None => self.process_list.find_process(None),
2056 }
2057 }
2058
2059 pub fn find_integrand_ref(
2060 &self,
2061 process: Option<&ProcessRef>,
2062 integrand_name: Option<&String>,
2063 ) -> Result<(usize, String)> {
2064 let process_id = self.resolve_process_ref(process)?;
2065 let integrand_name = self.process_list.processes[process_id]
2066 .collection
2067 .find_integrand(integrand_name.cloned())
2068 .with_note(|| format!("in process id {process_id}"))?;
2069 Ok((process_id, integrand_name))
2070 }
2071
2072 pub fn get_integrand_info(
2073 &self,
2074 process: Option<&ProcessRef>,
2075 integrand_name: Option<&String>,
2076 ) -> Result<IntegrandInfo> {
2077 let (process_id, integrand_name) = self.find_integrand_ref(process, integrand_name)?;
2078 collect_integrand_info(self, process_id, &integrand_name)
2079 }
2080
2081 pub fn duplicate_integrand(
2082 &mut self,
2083 process: Option<&ProcessRef>,
2084 integrand_name: Option<&String>,
2085 output_process_name: &str,
2086 output_integrand_name: &str,
2087 ) -> Result<()> {
2088 validate_output_name(output_process_name, "--output_process_name")?;
2089 validate_output_name(output_integrand_name, "--output_integrand_name")?;
2090
2091 let (source_process_id, source_integrand_name) =
2092 self.find_integrand_ref(process, integrand_name)?;
2093 let mut payload =
2094 self.cloned_integrand_payload(source_process_id, &source_integrand_name)?;
2095 payload.rename(output_integrand_name);
2096 self.insert_integrand_copy(
2097 source_process_id,
2098 output_process_name,
2099 output_integrand_name,
2100 payload,
2101 false,
2102 false,
2103 None,
2104 false,
2105 )?;
2106 Ok(())
2107 }
2108
2109 pub fn select_integrand_graph_groups(
2110 &mut self,
2111 process: Option<&ProcessRef>,
2112 integrand_name: Option<&String>,
2113 selection: &GraphGroupSelectionSpec,
2114 target: &GraphGroupSelectionTarget,
2115 context: GraphGroupSelectionContext<'_>,
2116 ) -> Result<SelectedGraphGroups> {
2117 if target.clear_existing_processes && !target.is_copy_mode() {
2118 return Err(eyre!(
2119 "--clear-existing-processes requires --output_process or --output_integrand for `select`"
2120 ));
2121 }
2122
2123 let (source_process_id, source_integrand_name) =
2124 self.find_integrand_ref(process, integrand_name)?;
2125 let (source_process_name, plan, discarded_generated_integrand) = {
2126 let process_entry = &self.process_list.processes[source_process_id];
2127 let process_name = process_entry.definition.folder_name.clone();
2128 match &process_entry.collection {
2129 ProcessCollection::Amplitudes(amplitudes) => {
2130 if selection.has_raised_cut_rules() {
2131 return Err(eyre!(
2132 "Raised-cut signature selection can only be used with cross-section integrands."
2133 ));
2134 }
2135 if selection.mode() == GraphGroupSelectionMode::CrossSectionAmplitudeGraphs {
2136 return Err(eyre!(
2137 "`select --amplitude-graphs` can only be used with cross-section integrands."
2138 ));
2139 }
2140 let amplitude = amplitudes.get(&source_integrand_name).ok_or_else(|| {
2141 eyre!(
2142 "No amplitude named '{}' in process '{}'",
2143 source_integrand_name,
2144 process_name
2145 )
2146 })?;
2147 let plan = amplitude.plan_graph_group_selection(selection)?;
2148 amplitude.validate_graph_group_selection_plan(&plan)?;
2149 (process_name, plan, amplitude.integrand.is_some())
2150 }
2151 ProcessCollection::CrossSections(cross_sections) => {
2152 let cross_section =
2153 cross_sections.get(&source_integrand_name).ok_or_else(|| {
2154 eyre!(
2155 "No cross section named '{}' in process '{}'",
2156 source_integrand_name,
2157 process_name
2158 )
2159 })?;
2160 let plan = cross_section.plan_graph_group_selection_with_context(
2161 selection,
2162 &self.model,
2163 &process_entry.definition,
2164 context.generation_settings,
2165 )?;
2166 cross_section.validate_graph_group_selection_plan(&plan)?;
2167 (process_name, plan, cross_section.integrand.is_some())
2168 }
2169 }
2170 };
2171 let report = plan.report().clone();
2172 let source = SelectionSource {
2173 process_id: source_process_id,
2174 process_name: source_process_name,
2175 integrand_name: source_integrand_name,
2176 };
2177
2178 if target.is_copy_mode() {
2179 return self.select_integrand_graph_groups_to_output(
2180 &source,
2181 &plan,
2182 report,
2183 target,
2184 context.state_folder,
2185 context.read_only_state,
2186 );
2187 }
2188
2189 let mut removed_generated_artifacts = false;
2190 let mut removed_generation_summary = false;
2191 if discarded_generated_integrand {
2192 if context.read_only_state {
2193 return Err(eyre!(
2194 "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.",
2195 source.integrand_name,
2196 source.process_name
2197 ));
2198 }
2199
2200 let process_entry = &self.process_list.processes[source.process_id];
2201 let integrand_artifact_root = integrand_artifact_folder(
2202 context.state_folder,
2203 process_entry,
2204 &source.integrand_name,
2205 );
2206 let generated_artifact_path = generated_integrand_artifact_path(
2207 context.state_folder,
2208 process_entry,
2209 &source.integrand_name,
2210 );
2211 removed_generated_artifacts = remove_dir_if_exists_under_root(
2212 &generated_artifact_path,
2213 &integrand_artifact_root,
2214 "generated integrand artifact",
2215 )?;
2216 let generation_summary_path = integrand_generation_summary_path(
2217 context.state_folder,
2218 process_entry,
2219 &source.integrand_name,
2220 );
2221 removed_generation_summary = remove_file_if_exists_under_root(
2222 &generation_summary_path,
2223 &integrand_artifact_root,
2224 "integrand generation summary",
2225 )?;
2226 }
2227
2228 {
2229 let process_entry = &mut self.process_list.processes[source.process_id];
2230 match &mut process_entry.collection {
2231 ProcessCollection::Amplitudes(amplitudes) => {
2232 let amplitude =
2233 amplitudes.get_mut(&source.integrand_name).ok_or_else(|| {
2234 eyre!(
2235 "No amplitude named '{}' in process '{}'",
2236 source.integrand_name,
2237 source.process_name
2238 )
2239 })?;
2240 amplitude.apply_graph_group_selection(&plan)?;
2241 }
2242 ProcessCollection::CrossSections(cross_sections) => {
2243 let cross_section =
2244 cross_sections
2245 .get_mut(&source.integrand_name)
2246 .ok_or_else(|| {
2247 eyre!(
2248 "No cross section named '{}' in process '{}'",
2249 source.integrand_name,
2250 source.process_name
2251 )
2252 })?;
2253 cross_section.apply_graph_group_selection(&plan)?;
2254 }
2255 }
2256 }
2257
2258 if discarded_generated_integrand {
2259 self.generation_summaries
2260 .remove(&IntegrandGenerationSummaryKey {
2261 process_id: source.process_id,
2262 integrand_name: source.integrand_name.clone(),
2263 });
2264 }
2265
2266 Ok(SelectedGraphGroups {
2267 source_process_id: source.process_id,
2268 source_process_name: source.process_name.clone(),
2269 source_integrand_name: source.integrand_name.clone(),
2270 process_id: source.process_id,
2271 process_name: source.process_name,
2272 integrand_name: source.integrand_name,
2273 report,
2274 copied_to_output: false,
2275 replaced_existing_target: false,
2276 removed_target_artifacts: false,
2277 discarded_generated_integrand,
2278 removed_generated_artifacts,
2279 removed_generation_summary,
2280 })
2281 }
2282
2283 fn select_integrand_graph_groups_to_output(
2284 &mut self,
2285 source: &SelectionSource,
2286 plan: &GraphGroupSelectionPlan,
2287 report: GraphGroupSelectionReport,
2288 target: &GraphGroupSelectionTarget,
2289 state_folder: &Path,
2290 read_only_state: bool,
2291 ) -> Result<SelectedGraphGroups> {
2292 let output_process_name = target
2293 .output_process_name
2294 .as_deref()
2295 .unwrap_or(&source.process_name);
2296 let output_integrand_name = target
2297 .output_integrand_name
2298 .as_deref()
2299 .unwrap_or(&source.integrand_name);
2300
2301 validate_output_name(output_process_name, "--output_process")?;
2302 validate_output_name(output_integrand_name, "--output_integrand")?;
2303
2304 if output_process_name == source.process_name
2305 && output_integrand_name == source.integrand_name
2306 {
2307 return Err(eyre!(
2308 "Copy-mode select target '{} / {}' is the selected source integrand. Omit --output_process/--output_integrand for in-place selection or choose a different output target.",
2309 output_process_name,
2310 output_integrand_name
2311 ));
2312 }
2313
2314 let target_process_id = self
2315 .process_list
2316 .processes
2317 .iter()
2318 .position(|process| process.definition.folder_name == output_process_name);
2319 let replace_existing_process = target.output_process_name.is_some()
2320 && target.clear_existing_processes
2321 && target_process_id.is_some_and(|process_id| process_id != source.process_id);
2322
2323 let mut payload =
2324 self.cloned_integrand_payload(source.process_id, &source.integrand_name)?;
2325 payload.rename(output_integrand_name);
2326 payload.apply_graph_group_selection(plan)?;
2327
2328 let insertion = self.insert_integrand_copy(
2329 source.process_id,
2330 output_process_name,
2331 output_integrand_name,
2332 payload,
2333 target.clear_existing_processes,
2334 replace_existing_process,
2335 Some(state_folder),
2336 read_only_state,
2337 )?;
2338
2339 Ok(SelectedGraphGroups {
2340 source_process_id: source.process_id,
2341 source_process_name: source.process_name.clone(),
2342 source_integrand_name: source.integrand_name.clone(),
2343 process_id: insertion.process_id,
2344 process_name: insertion.process_name,
2345 integrand_name: insertion.integrand_name,
2346 report,
2347 copied_to_output: true,
2348 replaced_existing_target: insertion.replaced_existing_target,
2349 removed_target_artifacts: insertion.removed_target_artifacts,
2350 discarded_generated_integrand: false,
2351 removed_generated_artifacts: false,
2352 removed_generation_summary: false,
2353 })
2354 }
2355
2356 fn cloned_integrand_payload(
2357 &self,
2358 source_process_id: usize,
2359 source_integrand_name: &str,
2360 ) -> Result<IntegrandCopyPayload> {
2361 let source_process = &self.process_list.processes[source_process_id];
2362 match &source_process.collection {
2363 ProcessCollection::Amplitudes(amplitudes) => amplitudes
2364 .get(source_integrand_name)
2365 .cloned()
2366 .map(IntegrandCopyPayload::Amplitude)
2367 .ok_or_else(|| eyre!("Missing source amplitude '{}'", source_integrand_name)),
2368 ProcessCollection::CrossSections(cross_sections) => cross_sections
2369 .get(source_integrand_name)
2370 .cloned()
2371 .map(IntegrandCopyPayload::CrossSection)
2372 .ok_or_else(|| eyre!("Missing source cross section '{}'", source_integrand_name)),
2373 }
2374 }
2375
2376 #[allow(clippy::too_many_arguments)]
2377 fn insert_integrand_copy(
2378 &mut self,
2379 source_process_id: usize,
2380 output_process_name: &str,
2381 output_integrand_name: &str,
2382 payload: IntegrandCopyPayload,
2383 clear_existing_processes: bool,
2384 replace_existing_process: bool,
2385 state_folder: Option<&Path>,
2386 read_only_state: bool,
2387 ) -> Result<IntegrandCopyInsertion> {
2388 if let Some(destination_process_id) = self
2389 .process_list
2390 .processes
2391 .iter()
2392 .position(|process| process.definition.folder_name == output_process_name)
2393 {
2394 if replace_existing_process {
2395 if read_only_state {
2396 return Err(eyre!(
2397 "Cannot overwrite output process '{}' because this session was started with --read-only-state.",
2398 output_process_name
2399 ));
2400 }
2401 let removed_target_artifacts = if let Some(state_folder) = state_folder {
2402 remove_saved_process_artifacts(
2403 state_folder,
2404 &self.process_list.processes[destination_process_id],
2405 )?
2406 } else {
2407 false
2408 };
2409 self.remove_generation_summaries_for_process_without_shifting(
2410 destination_process_id,
2411 );
2412 let process = self.single_integrand_process(
2413 source_process_id,
2414 destination_process_id,
2415 output_process_name,
2416 payload,
2417 );
2418 self.process_list.processes[destination_process_id] = process;
2419 return Ok(IntegrandCopyInsertion {
2420 process_id: destination_process_id,
2421 process_name: output_process_name.to_string(),
2422 integrand_name: output_integrand_name.to_string(),
2423 replaced_existing_target: true,
2424 removed_target_artifacts,
2425 });
2426 }
2427
2428 let target_exists = self.process_list.processes[destination_process_id]
2429 .collection
2430 .get_integrand_names()
2431 .contains(&output_integrand_name);
2432 if target_exists {
2433 if !clear_existing_processes {
2434 return Err(eyre!(
2435 "An integrand '{}' already exists in process '{}'",
2436 output_integrand_name,
2437 output_process_name
2438 ));
2439 }
2440 if read_only_state {
2441 return Err(eyre!(
2442 "Cannot overwrite output integrand '{}' in process '{}' because this session was started with --read-only-state.",
2443 output_integrand_name,
2444 output_process_name
2445 ));
2446 }
2447 }
2448
2449 if !payload
2450 .is_compatible_with(&self.process_list.processes[destination_process_id].collection)
2451 {
2452 return Err(eyre!(
2453 "Destination process '{}' exists but does not contain {}",
2454 output_process_name,
2455 payload.kind_name()
2456 ));
2457 }
2458
2459 let removed_target_artifacts = if target_exists {
2460 if let Some(state_folder) = state_folder {
2461 remove_saved_integrand_artifacts(
2462 state_folder,
2463 &self.process_list.processes[destination_process_id],
2464 output_integrand_name,
2465 )?
2466 } else {
2467 false
2468 }
2469 } else {
2470 false
2471 };
2472
2473 if target_exists {
2474 self.generation_summaries
2475 .remove(&IntegrandGenerationSummaryKey {
2476 process_id: destination_process_id,
2477 integrand_name: output_integrand_name.to_string(),
2478 });
2479 }
2480
2481 payload.insert_into_process(
2482 &mut self.process_list.processes[destination_process_id],
2483 output_process_name,
2484 )?;
2485 Ok(IntegrandCopyInsertion {
2486 process_id: destination_process_id,
2487 process_name: output_process_name.to_string(),
2488 integrand_name: output_integrand_name.to_string(),
2489 replaced_existing_target: target_exists,
2490 removed_target_artifacts,
2491 })
2492 } else {
2493 let process_id = self.process_list.processes.len();
2494 let process = self.single_integrand_process(
2495 source_process_id,
2496 process_id,
2497 output_process_name,
2498 payload,
2499 );
2500 self.process_list.add_process(process);
2501 Ok(IntegrandCopyInsertion {
2502 process_id,
2503 process_name: output_process_name.to_string(),
2504 integrand_name: output_integrand_name.to_string(),
2505 replaced_existing_target: false,
2506 removed_target_artifacts: false,
2507 })
2508 }
2509 }
2510
2511 fn single_integrand_process(
2512 &self,
2513 source_process_id: usize,
2514 process_id: usize,
2515 process_name: &str,
2516 payload: IntegrandCopyPayload,
2517 ) -> Process {
2518 let source_process = &self.process_list.processes[source_process_id];
2519 let mut definition = source_process.definition.clone();
2520 definition.folder_name = process_name.to_string();
2521 definition.process_id = process_id;
2522 Process {
2523 definition,
2524 settings_history: source_process.settings_history.clone(),
2525 collection: payload.into_collection(),
2526 }
2527 }
2528
2529 pub fn resolve_effective_model_parameter_card_for_settings(
2530 &self,
2531 settings: &RuntimeSettings,
2532 ) -> Result<InputParamCard<F<f64>>> {
2533 let mut card = self.model_parameters.clone();
2534
2535 for (parameter_name, value) in &settings.model.external_parameters {
2536 let parameter_type = self.resolve_overridable_model_parameter_type(parameter_name)?;
2537 let value = Complex::new(value.0, value.1);
2538 validate_model_parameter_type(parameter_name, parameter_type, &value)?;
2539
2540 let parameter = card
2541 .get_mut(&UFOSymbol::from(parameter_name.as_str()))
2542 .ok_or_else(|| {
2543 eyre!(
2544 "Model parameter '{parameter_name}' is missing from the shared top-level model_parameters.json"
2545 )
2546 })?;
2547 *parameter = value;
2548 }
2549
2550 Ok(card)
2551 }
2552
2553 pub fn resolve_serializable_model_parameter_card_for_settings(
2554 &self,
2555 settings: &RuntimeSettings,
2556 ) -> Result<SerializableInputParamCard<F<f64>>> {
2557 Ok(self
2558 .resolve_effective_model_parameter_card_for_settings(settings)?
2559 .to_serializable())
2560 }
2561
2562 pub fn resolve_effective_model_parameter_card_for_integrand(
2563 &self,
2564 process_id: usize,
2565 integrand_name: &str,
2566 ) -> Result<SerializableInputParamCard<F<f64>>> {
2567 let resolved = self
2568 .process_list
2569 .get_integrand(process_id, integrand_name)?;
2570 match resolved.get_settings() {
2571 Some(settings) => self.resolve_serializable_model_parameter_card_for_settings(settings),
2572 None => Ok(self.model_parameters.to_serializable()),
2573 }
2574 }
2575
2576 pub fn resolve_model_for_settings(&self, settings: &RuntimeSettings) -> Result<Model> {
2577 let mut model = self.model.clone();
2578 self.resolve_effective_model_parameter_card_for_settings(settings)?
2579 .apply_to_model(&mut model)?;
2580 Ok(model)
2581 }
2582
2583 pub fn resolve_model_for_integrand(
2584 &self,
2585 process_id: usize,
2586 integrand_name: &str,
2587 ) -> Result<Model> {
2588 let resolved = self
2589 .process_list
2590 .get_integrand(process_id, integrand_name)?;
2591 match resolved.get_settings() {
2592 Some(settings) => self.resolve_model_for_settings(settings),
2593 None => Ok(self.model.clone()),
2594 }
2595 }
2596
2597 pub fn generate_integrands(
2598 &mut self,
2599 global_settings: &GlobalSettings,
2600 runtime_default: LockedRuntimeSettings,
2601 ) -> Result<GenerationReports> {
2602 self.run_generation_with_monitor(global_settings, move |state, generation_pool| {
2603 let mut reports = state.process_list.preprocess(
2604 &state.model,
2605 global_settings,
2606 &runtime_default,
2607 generation_pool,
2608 )?;
2609 merge_generated_graph_reports(
2610 &mut reports,
2611 state.process_list.generate_integrands(
2612 &state.model,
2613 global_settings,
2614 runtime_default,
2615 generation_pool,
2616 )?,
2617 );
2618 Ok(reports)
2619 })
2620 }
2621
2622 fn attach_process_id_to_named_reports(
2623 process_id: usize,
2624 reports: Vec<NamedGraphGenerationReport>,
2625 ) -> Vec<GeneratedGraphReport> {
2626 reports
2627 .into_iter()
2628 .map(|report| GeneratedGraphReport {
2629 process_id,
2630 integrand_name: report.integrand_name,
2631 graph_name: report.graph_name,
2632 stats: report.stats,
2633 })
2634 .collect()
2635 }
2636
2637 pub fn generate_integrand(
2638 &mut self,
2639 global_settings: &GlobalSettings,
2640 runtime_default: LockedRuntimeSettings,
2641 process_id: usize,
2642 integrand_name: Option<String>,
2643 ) -> Result<GenerationReports> {
2644 self.run_generation_with_monitor(global_settings, move |state, generation_pool| {
2645 let p = &mut state.process_list.processes[process_id];
2646 let process_name = p.definition.folder_name.clone();
2647 if let Some(name) = &integrand_name {
2648 let mut reports = Vec::new();
2649 match &mut p.collection {
2650 ProcessCollection::Amplitudes(a) => {
2651 if let Some(a) = a.get_mut(name) {
2652 begin_phase(
2653 GenerationProgressPhase::GraphPreprocessing,
2654 GenerationProcessKind::Amplitude,
2655 &process_name,
2656 &a.name,
2657 a.graphs.len(),
2658 None,
2659 );
2660 merge_generated_graph_reports(
2661 &mut reports,
2662 Self::attach_process_id_to_named_reports(
2663 process_id,
2664 a.preprocess(
2665 &state.model,
2666 &global_settings.generation,
2667 &runtime_default,
2668 generation_pool,
2669 )?,
2670 ),
2671 );
2672 merge_generated_graph_reports(
2673 &mut reports,
2674 Self::attach_process_id_to_named_reports(
2675 process_id,
2676 a.build_integrand(
2677 &state.model,
2678 &process_name,
2679 global_settings,
2680 runtime_default,
2681 generation_pool,
2682 )?,
2683 ),
2684 );
2685 } else {
2686 return Err(eyre!(
2687 "No amplitude named '{}' in process id {}",
2688 name,
2689 process_id
2690 ));
2691 }
2692 }
2693 ProcessCollection::CrossSections(cs) => {
2694 if let Some(cs) = cs.get_mut(name) {
2695 merge_generated_graph_reports(
2696 &mut reports,
2697 Self::attach_process_id_to_named_reports(
2698 process_id,
2699 cs.preprocess(
2700 &state.model,
2701 &p.definition,
2702 &global_settings.generation,
2703 runtime_default,
2704 generation_pool,
2705 )?,
2706 ),
2707 );
2708 merge_generated_graph_reports(
2709 &mut reports,
2710 Self::attach_process_id_to_named_reports(
2711 process_id,
2712 cs.build_integrand(
2713 &state.model,
2714 &process_name,
2715 global_settings,
2716 runtime_default,
2717 generation_pool,
2718 )?,
2719 ),
2720 );
2721 } else {
2722 return Err(eyre!(
2723 "No cross section named '{}' in process id {}",
2724 name,
2725 process_id
2726 ));
2727 }
2728 }
2729 }
2730 Ok(reports)
2731 } else {
2732 let mut reports = p.preprocess(
2733 &state.model,
2734 global_settings,
2735 &runtime_default,
2736 generation_pool,
2737 )?;
2738 merge_generated_graph_reports(
2739 &mut reports,
2740 p.generate_integrands(
2741 &state.model,
2742 global_settings,
2743 runtime_default,
2744 generation_pool,
2745 )?,
2746 );
2747 Ok(reports)
2748 }
2749 })
2750 }
2751
2752 fn run_generation_with_monitor<F>(
2753 &mut self,
2754 global_settings: &GlobalSettings,
2755 generation: F,
2756 ) -> Result<GenerationReports>
2757 where
2758 F: FnOnce(&mut Self, &rayon::ThreadPool) -> Result<Vec<GeneratedGraphReport>>,
2759 {
2760 let generation_pool = rayon::ThreadPoolBuilder::new()
2761 .num_threads(global_settings.n_cores.generate)
2762 .stack_size(GENERATION_THREAD_STACK_SIZE_BYTES)
2763 .build()?;
2764 let generation_cores = generation_pool.current_num_threads();
2765 clear_interrupt_request();
2766 let mut monitor = GenerationMonitor::start()?;
2767 let stderr_is_terminal = io::stderr().is_terminal();
2768 let progress_mode = if generation_cores == 1 && stderr_is_terminal {
2769 GenerationProgressMode::Detailed
2770 } else {
2771 GenerationProgressMode::Aggregate
2772 };
2773 let _progress_mode_guard = GenerationProgressModeGuard::set(progress_mode);
2774 let _progress_observer_guard = if generation_cores > 1 && stderr_is_terminal {
2775 let reporter = AggregateGenerationProgressReporter::new(
2776 monitor.current_ram_bytes(),
2777 monitor.peak_ram_bytes(),
2778 generation_cores as u64,
2779 );
2780 Some(GenerationProgressObserverGuard::set(reporter))
2781 } else {
2782 None
2783 };
2784 let generation_result = generation(self, &generation_pool);
2785 let peak_ram_bytes = monitor.finish();
2786 clear_interrupt_request();
2787
2788 generation_result.map(|reports| GenerationReports {
2789 reports,
2790 resources: GenerationResourceSummary {
2791 peak_ram_bytes,
2792 generation_cores,
2793 },
2794 })
2795 }
2796
2797 pub fn compile_integrands(
2798 &mut self,
2799 folder: impl AsRef<Path>,
2800 override_existing: bool,
2801 global_settings: &GlobalSettings,
2802 process_id: Option<usize>,
2803 integrand_name: Option<String>,
2804 ) -> Result<Vec<GeneratedGraphReport>> {
2805 let compile_pool = rayon::ThreadPoolBuilder::new()
2806 .num_threads(global_settings.n_cores.compile)
2807 .build()?;
2808 self.process_list.compile(
2809 folder,
2810 override_existing,
2811 process_id,
2812 integrand_name,
2813 &compile_pool,
2814 )
2815 }
2816
2817 pub fn generation_summary(
2818 &self,
2819 process_id: usize,
2820 integrand_name: &str,
2821 ) -> Option<&IntegrandGenerationSummary> {
2822 self.generation_summaries
2823 .get(&IntegrandGenerationSummaryKey {
2824 process_id,
2825 integrand_name: integrand_name.to_string(),
2826 })
2827 }
2828
2829 pub fn record_generation_summary(
2830 &mut self,
2831 reports: &[GeneratedGraphReport],
2832 resources: GenerationResourceSummary,
2833 ) {
2834 let mut reports_by_integrand: BTreeMap<
2835 IntegrandGenerationSummaryKey,
2836 Vec<GeneratedGraphReport>,
2837 > = BTreeMap::new();
2838
2839 for report in reports {
2840 let Some(process) = self.process_list.processes.get(report.process_id) else {
2841 continue;
2842 };
2843 if !process
2844 .collection
2845 .get_integrand_names()
2846 .contains(&report.integrand_name.as_str())
2847 {
2848 continue;
2849 }
2850
2851 reports_by_integrand
2852 .entry(IntegrandGenerationSummaryKey {
2853 process_id: report.process_id,
2854 integrand_name: report.integrand_name.clone(),
2855 })
2856 .or_default()
2857 .push(report.clone());
2858 }
2859
2860 for (key, reports) in reports_by_integrand {
2861 self.generation_summaries.insert(
2862 key,
2863 IntegrandGenerationSummary {
2864 peak_ram_bytes: resources.peak_ram_bytes,
2865 reports,
2866 },
2867 );
2868 }
2869 }
2870
2871 fn remove_generation_summaries_for_process(&mut self, process_id: usize) {
2872 let mut updated = BTreeMap::new();
2873 for (mut key, mut summary) in std::mem::take(&mut self.generation_summaries) {
2874 if key.process_id == process_id {
2875 continue;
2876 }
2877 if key.process_id > process_id {
2878 key.process_id -= 1;
2879 for report in &mut summary.reports {
2880 if report.process_id > process_id {
2881 report.process_id -= 1;
2882 }
2883 }
2884 }
2885 updated.insert(key, summary);
2886 }
2887 self.generation_summaries = updated;
2888 }
2889
2890 fn remove_generation_summaries_for_process_without_shifting(&mut self, process_id: usize) {
2891 self.generation_summaries
2892 .retain(|key, _| key.process_id != process_id);
2893 }
2894
2895 pub fn export_dots(
2896 &mut self,
2897 path: impl AsRef<Path>,
2898 settings: &DotExportSettings,
2899 ) -> Result<()> {
2900 self.process_list.export_dot(path, settings)?;
2901 Ok(())
2902 }
2903
2904 pub fn import_graphs(
2905 &mut self,
2906 graphs: Vec<Graph>,
2907 process_name: Option<String>,
2908 process_id: Option<usize>,
2909 integrand_name: Option<String>,
2910 overwrite: bool,
2911 append: bool,
2912 ) -> Result<()> {
2913 let generation_type = if graphs.iter().all(|g| g.initial_state_cut.nedges(g) == 0) {
2914 GenerationType::Amplitude
2915 } else if graphs.iter().all(|g| g.initial_state_cut.nedges(g) > 0) {
2916 GenerationType::CrossSection
2917 } else {
2918 return Err(eyre!(
2919 "Mix of amplitude and cross section graphs in the same file is not supported"
2920 ));
2921 };
2922
2923 let integrand_base_name = integrand_name.clone().unwrap_or("default".to_string());
2924 let process = if let Some(proc_id) = process_id {
2925 if proc_id >= self.process_list.processes.len() {
2926 return Err(eyre!(
2927 "Process ID {} invalid, only {} processes available",
2928 proc_id,
2929 self.process_list.processes.len()
2930 ));
2931 }
2932 Some(&mut self.process_list.processes[proc_id])
2933 } else {
2934 let p_name = match process_name {
2935 Some(n) => n,
2936 None => {
2937 return Err(eyre!(
2938 "Either process ID or process name must be provided when importing graphs"
2939 ));
2940 }
2941 };
2942 if let Some(existing_proc) = self
2943 .process_list
2944 .processes
2945 .iter_mut()
2946 .find(|p| p.definition.folder_name == p_name)
2947 {
2948 Some(existing_proc)
2949 } else {
2950 let process_defintion =
2951 ProcessDefinition::from_graph_list(&graphs, generation_type, &self.model)?;
2952 let process = Process::from_graph_list(
2953 p_name,
2954 integrand_base_name.clone(),
2955 graphs.clone(),
2957 generation_type,
2958 Some(process_defintion),
2959 None,
2960 &self.model,
2961 )?;
2962
2963 self.process_list.add_process(process);
2964 None
2965 }
2966 };
2967 if let Some(p) = process {
2968 let existing_names = p.get_integrand_names();
2969 let integrand_name = if existing_names.contains(&integrand_base_name.as_str()) {
2970 if append {
2971 let mut integrand_i = 0;
2972 while existing_names
2973 .iter()
2974 .any(|ce| *ce == format!("{}_{}", integrand_base_name, integrand_i))
2975 {
2976 integrand_i += 1;
2977 }
2978 format!("{}_{}", integrand_base_name, integrand_i)
2979 } else if overwrite {
2980 p.collection.remove_integrand(&integrand_base_name)?;
2981 integrand_base_name.clone()
2982 } else {
2983 return Err(eyre!(
2984 "Integrand name '{}' already exists in process '{}', use either --overwrite or --append flag when loading graphs",
2985 integrand_base_name,
2986 p.definition.folder_name
2987 ));
2988 }
2989 } else {
2990 integrand_base_name.clone()
2991 };
2992
2993 match generation_type {
2994 GenerationType::Amplitude => p
2995 .collection
2996 .add_amplitude(Amplitude::from_graph_list(integrand_name.clone(), graphs)?),
2997 GenerationType::CrossSection => {
2998 p.collection
2999 .add_cross_section(CrossSection::from_graph_list(
3000 integrand_name.clone(),
3001 graphs,
3002 &self.model,
3003 )?)
3004 }
3005 }
3006 }
3007
3008 Ok(())
3009 }
3010
3011 pub fn bench(
3012 &mut self,
3013 samples: usize,
3014 process_id: usize,
3015 integrand_name: String,
3016 _n_cores: usize,
3017 ) -> Result<()> {
3018 let integrand = self
3019 .process_list
3020 .get_integrand_mut(process_id, integrand_name)?;
3021 let name = integrand.name();
3022
3023 info!(
3024 "\nBenchmarking runtime of integrand '{}' over {} samples...\n",
3025 name.green(),
3026 samples.to_string().blue()
3027 );
3028
3029 let now = Instant::now();
3030 for _ in 0..samples {
3031 let _ = integrand.evaluate_sample(
3032 &Sample::Continuous(
3033 F(1.),
3034 (0..integrand.get_n_dim())
3035 .map(|_| F(rand::random::<f64>()))
3036 .collect(),
3037 ),
3038 &self.model,
3039 F(1.),
3040 1,
3041 false,
3042 Complex::new_zero(),
3043 );
3044 }
3045 let total_time = now.elapsed().as_secs_f64();
3046 info!(
3047 "\n> Total time: {} s for {} samples, {} ms per sample\n",
3048 format!("{:.1}", total_time).blue(),
3049 format!("{}", samples).blue(),
3050 format!("{:.5}", total_time * 1000. / (samples as f64)).green(),
3051 );
3052
3053 Ok(())
3054 }
3055
3056 pub fn new(log_dir: impl AsRef<Path>, log_file_name: Option<String>) -> Self {
3057 super::tracing::init_tracing(log_dir.as_ref().join("logs"), log_file_name);
3058 let _ = initialise();
3059
3060 Self {
3061 model: Model::default(),
3062 process_list: ProcessList::default(),
3063 model_parameters: InputParamCard::default(),
3064 generation_summaries: BTreeMap::new(),
3065 }
3066 }
3067
3068 pub fn new_test() -> Self {
3069 init_test_tracing();
3070
3071 Self {
3072 model: Model::default(),
3073 process_list: ProcessList::default(),
3074 model_parameters: InputParamCard::default(),
3075 generation_summaries: BTreeMap::new(),
3076 }
3077 }
3078
3079 pub fn new_bench() -> Self {
3080 init_bench_tracing();
3081
3082 Self {
3083 model: Model::default(),
3084 process_list: ProcessList::default(),
3085 model_parameters: InputParamCard::default(),
3086 generation_summaries: BTreeMap::new(),
3087 }
3088 }
3089}
3090
3091#[derive(Debug, Clone, PartialEq, Eq)]
3092pub struct RemovedProcess {
3093 pub process_id: usize,
3094 pub process_name: String,
3095}
3096
3097#[derive(Debug, Clone, PartialEq, Eq)]
3098pub struct RemovedIntegrand {
3099 pub process_id: usize,
3100 pub process_name: String,
3101 pub integrand_name: String,
3102 pub removed_empty_process: bool,
3103}
3104
3105impl State {
3106 pub fn load(
3107 save_path: PathBuf,
3108 model_path: Option<PathBuf>,
3109 trace_logs_filename: Option<String>,
3110 ) -> Result<Self> {
3111 let manifest = load_state_manifest(&save_path)?;
3113 run_state_migration_checks(&manifest, &save_path)?;
3114 let mut loaded_state = State::new(&save_path, trace_logs_filename);
3118 debug!("Loading state manifest version {}", manifest.version);
3119
3120 let mut model = if let Some(model_path) = &model_path {
3121 info!("Loading model from {}", model_path.display());
3122 Model::from_file(model_path)?
3123 } else {
3124 let model_dir = save_path.join("model.json");
3125 info!(
3126 "Loading model from default location: {}",
3127 model_dir.display()
3128 );
3129 Model::from_file(model_dir)?
3130 };
3131
3132 debug!("Loaded model: {}", model.name);
3133
3134 let input_param_card = if save_path.join("model_parameters.json").exists() {
3135 let a = InputParamCard::from_file(save_path.join("model_parameters.json"))?;
3136
3137 let _ = model.apply_param_card(&a);
3138 a
3139 } else {
3140 InputParamCard::default_from_model(&model)
3141 };
3142
3143 let symbolica_state = symbolica::state::State::import(
3144 &mut fs::File::open(save_path.join("symbolica_state.bin"))
3145 .context("Trying to open symbolica state binary")?,
3146 None,
3147 )?;
3148
3149 let context: GammaLoopContextContainer<'_> = GammaLoopContextContainer {
3150 state_map: &symbolica_state,
3151 model: &model,
3152 };
3153
3154 let process_list =
3155 ProcessList::load(&save_path, context).context("Trying to load processList")?;
3156
3157 loaded_state.process_list = process_list;
3158 loaded_state.model = model;
3159 loaded_state.model_parameters = input_param_card;
3160 loaded_state.generation_summaries =
3161 load_integrand_generation_summaries(&save_path, &loaded_state.process_list)?;
3162 Ok(loaded_state)
3163 }
3164
3165 pub fn activate_loaded_integrand_backends(
3166 &mut self,
3167 allow_symjit_fallback: bool,
3168 ) -> Result<()> {
3169 self.process_list
3170 .activate_loaded_integrand_backends(allow_symjit_fallback)
3171 }
3172
3173 pub fn compile(
3174 &mut self,
3175 root_folder: &Path,
3176 override_compiled: bool,
3177 settings: &GlobalSettings,
3178 ) -> Result<()> {
3179 fs::create_dir_all(root_folder)?;
3180
3181 let compile_pool = rayon::ThreadPoolBuilder::new()
3182 .num_threads(settings.n_cores.compile)
3183 .build()?;
3184 self.process_list
3185 .compile(root_folder, override_compiled, None, None, &compile_pool)?;
3186 Ok(())
3187 }
3188
3189 fn save_generation_summaries(&self, root_folder: &Path) -> Result<()> {
3190 for (process_id, process) in self.process_list.processes.iter().enumerate() {
3191 for integrand_name in process.collection.get_integrand_names() {
3192 let summary_path =
3193 integrand_generation_summary_path(root_folder, process, integrand_name);
3194 let key = IntegrandGenerationSummaryKey {
3195 process_id,
3196 integrand_name: integrand_name.to_string(),
3197 };
3198 if let Some(summary) = self.generation_summaries.get(&key) {
3199 if let Some(parent) = summary_path.parent() {
3200 fs::create_dir_all(parent)?;
3201 }
3202 let raw_summary = serde_json::to_string_pretty(summary).with_context(|| {
3203 format!(
3204 "Trying to serialize integrand generation summary {}",
3205 summary_path.display()
3206 )
3207 })?;
3208 fs::write(&summary_path, raw_summary).with_context(|| {
3209 format!(
3210 "Trying to write integrand generation summary {}",
3211 summary_path.display()
3212 )
3213 })?;
3214 } else if summary_path.exists() {
3215 fs::remove_file(&summary_path).with_context(|| {
3216 format!(
3217 "Trying to remove stale integrand generation summary {}",
3218 summary_path.display()
3219 )
3220 })?;
3221 }
3222 }
3223 }
3224
3225 Ok(())
3226 }
3227
3228 pub fn save(
3229 &mut self,
3230 root_folder: &Path,
3231 override_state_file: bool,
3232 strict: bool,
3233 ) -> Result<()> {
3234 let mut selected_root_folder = PathBuf::from(root_folder);
3238 let mut user_input = String::new();
3239 if !root_folder.exists() {
3240 fs::create_dir_all(root_folder)?;
3241 } else {
3242 if strict {
3243 return Err(eyre!(
3244 "Export root already exists, please choose a different path or remove the existing directory",
3245 ));
3246 }
3247
3248 if !override_state_file {
3249 while selected_root_folder.exists() {
3250 println!(
3251 "Gammaloop export root {} already exists. Specify 'o' for overwriting, 'n' for not saving, or '<NEW_PATH>' to specify where to save current state to:",
3252 selected_root_folder.display()
3253 );
3254 user_input.clear();
3255 io::stdin()
3256 .read_line(&mut user_input)
3257 .expect("Could not read user-specified gammaloop state export destination");
3258 match user_input.trim() {
3260 "o" => break,
3261 "n" => {
3262 return Ok(());
3263 }
3264 new_path => {
3265 selected_root_folder = PathBuf::from(new_path);
3266 continue;
3267 }
3268 }
3269 }
3270 }
3271 }
3272
3273 fs::create_dir_all(&selected_root_folder)?;
3274
3275 let mut state_file =
3276 fs::File::create(selected_root_folder.join("symbolica_state.bin"))?;
3278
3279 symbolica::state::State::export(&mut state_file)?;
3280 self.process_list
3281 .save(&selected_root_folder, override_state_file)?;
3282 self.save_generation_summaries(&selected_root_folder)?;
3283
3284 self.model
3287 .to_serializable()
3288 .to_file(selected_root_folder.join("model.json"), override_state_file)?;
3289 self.model_parameters.to_file(
3290 selected_root_folder.join("model_parameters.json"),
3291 override_state_file,
3292 )?;
3293 save_state_manifest(&selected_root_folder)?;
3294 Ok(())
3295 }
3296}
3297
3298#[cfg(test)]
3299mod tests {
3300 use std::fs;
3301
3302 use gammalooprs::{
3303 graph::Graph,
3304 initialisation::test_initialise,
3305 integrands::process::ActiveF64Backend,
3306 model::InputParamCard,
3307 momentum::{Dep, ExternalMomenta, Helicity},
3308 processes::{
3309 process::ProcessCollection, RaisedPropagatorScope, RaisedPropagatorSignature,
3310 SelectionPolarity,
3311 },
3312 settings::global::{CompilationMode, FrozenCompilationMode},
3313 settings::{
3314 runtime::kinematic::{improvement::PhaseSpaceImprovementSettings, Externals},
3315 KinematicsSettings, RuntimeSettings,
3316 },
3317 utils::{load_generic_model, serde_utils::SHOWDEFAULTS},
3318 };
3319 use tempfile::tempdir;
3320
3321 use crate::commands::{
3322 display::Display,
3323 save::SaveState,
3324 set::{ProcessSetArgs, Set, SetArgs},
3325 };
3326
3327 use super::*;
3328
3329 fn build_generated_scalar_bubble_state_with_external_backend() -> State {
3330 test_initialise().expect("test initialisation should succeed");
3331 let mut state = State::new_test();
3332 state.model = load_generic_model("scalars");
3333 state.model_parameters = InputParamCard::default_from_model(&state.model);
3334
3335 let graph_path =
3336 crate::test_workspace_root().join("tests/resources/graphs/scalar_bubble.dot");
3337 let graphs = Graph::from_path(&graph_path, &state.model)
3338 .expect("scalar bubble graph fixture should load");
3339
3340 state
3341 .import_graphs(
3342 graphs,
3343 Some("scalar_bubble".to_string()),
3344 None,
3345 Some("default".to_string()),
3346 false,
3347 false,
3348 )
3349 .expect("graph import should succeed");
3350
3351 let mut cli_settings = CLISettings::default();
3352 cli_settings.global.generation.evaluator.compile = true;
3353 cli_settings.global.generation.compile.compilation_mode = CompilationMode::Assembly;
3354
3355 let runtime_defaults = RuntimeSettings::default();
3356 state
3357 .generate_integrands(&cli_settings.global, (&runtime_defaults).into())
3358 .expect("integrand generation should succeed");
3359
3360 state
3361 }
3362
3363 #[test]
3364 fn aggregate_generation_progress_tracks_counts_and_timings() {
3365 let current_ram_bytes = Arc::new(AtomicU64::new(256 * 1024 * 1024));
3366 let peak_ram_bytes = Arc::new(AtomicU64::new(512 * 1024 * 1024));
3367 let reporter =
3368 AggregateGenerationProgressReporter::new_hidden(current_ram_bytes, peak_ram_bytes);
3369
3370 reporter.begin_phase(
3371 GenerationProgressPhase::GraphPreprocessing,
3372 GenerationProcessKind::CrossSection,
3373 "proc",
3374 "itg",
3375 2,
3376 None,
3377 );
3378 reporter.graph_started(GenerationProcessKind::CrossSection, "itg", "GL01", None);
3379 reporter.cuts_discovered("itg", "GL01", 4, 2);
3380 {
3381 let state = reporter
3382 .state
3383 .lock()
3384 .expect("aggregate generation progress state mutex is poisoned");
3385 assert_eq!(state.done_graphs, 0);
3386 assert_eq!(state.discovered_st_cuts, 4);
3387 assert_eq!(state.discovered_valid_cuts, 2);
3388 assert_eq!(
3389 AggregateGenerationProgressReporter::progress_units(&state),
3390 (0, 2)
3391 );
3392 assert_eq!(
3393 AggregateGenerationProgressReporter::graph_progress_counts(&state),
3394 (0, 1, 2)
3395 );
3396 }
3397 reporter.graph_finished(
3398 GenerationProcessKind::CrossSection,
3399 "itg",
3400 "GL01",
3401 &GraphGenerationStats {
3402 total_time: Duration::from_secs(3),
3403 ..GraphGenerationStats::default()
3404 },
3405 None,
3406 );
3407 {
3408 let state = reporter
3409 .state
3410 .lock()
3411 .expect("aggregate generation progress state mutex is poisoned");
3412 assert_eq!(state.done_graphs, 1);
3413 assert_eq!(state.stats.total_time, Duration::from_secs(3));
3414 assert_eq!(
3415 AggregateGenerationProgressReporter::graph_progress_counts(&state),
3416 (1, 0, 2)
3417 );
3418 }
3419
3420 reporter.begin_phase(
3421 GenerationProgressPhase::GraphGeneration,
3422 GenerationProcessKind::CrossSection,
3423 "proc",
3424 "itg",
3425 2,
3426 Some(3),
3427 );
3428 reporter.graph_started(GenerationProcessKind::CrossSection, "itg", "GL01", Some(1));
3429 reporter.graph_started(GenerationProcessKind::CrossSection, "itg", "GL02", Some(2));
3430 {
3431 let state = reporter
3432 .state
3433 .lock()
3434 .expect("aggregate generation progress state mutex is poisoned");
3435 assert_eq!(
3436 AggregateGenerationProgressReporter::graph_progress_counts(&state),
3437 (0, 2, 2)
3438 );
3439 }
3440 reporter.graph_finished(
3441 GenerationProcessKind::CrossSection,
3442 "itg",
3443 "GL01",
3444 &GraphGenerationStats {
3445 evaluator_count: 2,
3446 total_time: Duration::from_secs(4),
3447 evaluator_spenso_time: Duration::from_secs(1),
3448 evaluator_symbolica_time: Duration::from_secs(1),
3449 evaluator_compile_time: Duration::ZERO,
3450 },
3451 None,
3452 );
3453 reporter.cut_finished("itg", "GL01", 1);
3454 reporter.graph_finished(
3455 GenerationProcessKind::CrossSection,
3456 "itg",
3457 "GL02",
3458 &GraphGenerationStats {
3459 evaluator_count: 3,
3460 total_time: Duration::from_secs(6),
3461 evaluator_spenso_time: Duration::from_secs(2),
3462 evaluator_symbolica_time: Duration::ZERO,
3463 evaluator_compile_time: Duration::ZERO,
3464 },
3465 None,
3466 );
3467 reporter.cut_finished("itg", "GL02", 2);
3468
3469 {
3470 let state = reporter
3471 .state
3472 .lock()
3473 .expect("aggregate generation progress state mutex is poisoned");
3474 assert_eq!(state.done_graphs, 2);
3475 assert_eq!(state.done_cuts, 3);
3476 assert!(state.active_graphs.is_empty());
3477 assert_eq!(state.last_graph.as_deref(), Some("GL02"));
3478 assert_eq!(state.stats.evaluator_count, 5);
3479 assert_eq!(state.stats.total_time, Duration::from_secs(10));
3480 assert_eq!(state.stats.expression_build_time(), Duration::from_secs(6));
3481 assert_eq!(state.stats.evaluator_spenso_time, Duration::from_secs(3));
3482 assert_eq!(state.stats.evaluator_symbolica_time, Duration::from_secs(1));
3483 assert_eq!(
3484 AggregateGenerationProgressReporter::progress_units(&state),
3485 (5, 5)
3486 );
3487 assert_eq!(
3488 AggregateGenerationProgressReporter::graph_progress_counts(&state),
3489 (2, 0, 2)
3490 );
3491 }
3492
3493 reporter.backend_started(GenerationProcessKind::CrossSection, "itg", 2);
3494 reporter.backend_finished(
3495 GenerationProcessKind::CrossSection,
3496 "itg",
3497 Duration::from_secs(2),
3498 );
3499
3500 let state = reporter
3501 .state
3502 .lock()
3503 .expect("aggregate generation progress state mutex is poisoned");
3504 assert_eq!(state.phase, Some(GenerationProgressPhase::Backend));
3505 assert_eq!(state.done_graphs, 2);
3506 assert_eq!(state.stats.total_time, Duration::from_secs(12));
3507 assert_eq!(state.stats.evaluator_compile_time, Duration::from_secs(2));
3508 }
3509
3510 #[test]
3511 fn aggregate_generation_progress_tracks_amplitude_preprocessing() {
3512 let reporter = AggregateGenerationProgressReporter::new_hidden(
3513 Arc::new(AtomicU64::new(0)),
3514 Arc::new(AtomicU64::new(0)),
3515 );
3516
3517 reporter.begin_phase(
3518 GenerationProgressPhase::GraphPreprocessing,
3519 GenerationProcessKind::Amplitude,
3520 "proc",
3521 "amp",
3522 3,
3523 None,
3524 );
3525 reporter.graph_started(GenerationProcessKind::Amplitude, "amp", "GL01", None);
3526 reporter.graph_started(GenerationProcessKind::Amplitude, "amp", "GL02", None);
3527
3528 {
3529 let state = reporter
3530 .state
3531 .lock()
3532 .expect("aggregate generation progress state mutex is poisoned");
3533 assert_eq!(state.kind, Some(GenerationProcessKind::Amplitude));
3534 assert_eq!(state.process, "proc");
3535 assert_eq!(state.integrand, "amp");
3536 assert_eq!(
3537 AggregateGenerationProgressReporter::progress_units(&state),
3538 (0, 3)
3539 );
3540 assert_eq!(
3541 AggregateGenerationProgressReporter::graph_progress_counts(&state),
3542 (0, 2, 3)
3543 );
3544 }
3545
3546 reporter.graph_finished(
3547 GenerationProcessKind::Amplitude,
3548 "amp",
3549 "GL01",
3550 &GraphGenerationStats::default(),
3551 None,
3552 );
3553
3554 let state = reporter
3555 .state
3556 .lock()
3557 .expect("aggregate generation progress state mutex is poisoned");
3558 assert_eq!(
3559 AggregateGenerationProgressReporter::progress_units(&state),
3560 (1, 3)
3561 );
3562 assert_eq!(
3563 AggregateGenerationProgressReporter::graph_progress_counts(&state),
3564 (1, 1, 3)
3565 );
3566 assert_eq!(state.last_graph.as_deref(), Some("GL01"));
3567 }
3568
3569 #[test]
3570 fn aggregate_generation_progress_formats_memory_and_time_shares() {
3571 assert_eq!(
3572 AggregateGenerationProgressReporter::progress_memory(512 * 1024 * 1024),
3573 " 512 MiB"
3574 );
3575 assert_eq!(
3576 AggregateGenerationProgressReporter::progress_memory(5 * 1024 * 1024 * 1024),
3577 " 5.00 GiB"
3578 );
3579
3580 assert_eq!(
3581 AggregateGenerationProgressReporter::progress_percent(0.0),
3582 "0%"
3583 );
3584 assert_eq!(
3585 AggregateGenerationProgressReporter::progress_percent(0.009),
3586 "0%"
3587 );
3588 assert_eq!(
3589 AggregateGenerationProgressReporter::progress_percent(0.024),
3590 "0.024%"
3591 );
3592 assert_eq!(
3593 AggregateGenerationProgressReporter::progress_percent(0.24),
3594 "0.24%"
3595 );
3596 assert_eq!(
3597 AggregateGenerationProgressReporter::progress_percent(2.4),
3598 "2.4%"
3599 );
3600 assert_eq!(
3601 AggregateGenerationProgressReporter::progress_percent(54.0),
3602 "54%"
3603 );
3604
3605 assert_eq!(
3606 AggregateGenerationProgressReporter::progress_time_share(
3607 Duration::ZERO,
3608 Duration::ZERO,
3609 ),
3610 "--"
3611 );
3612 assert_eq!(
3613 AggregateGenerationProgressReporter::progress_time_share(
3614 Duration::ZERO,
3615 Duration::from_secs(4),
3616 ),
3617 "0%"
3618 );
3619 assert_eq!(
3620 AggregateGenerationProgressReporter::progress_time_share(
3621 Duration::from_secs(1),
3622 Duration::from_secs(4),
3623 ),
3624 "25%"
3625 );
3626 }
3627
3628 fn build_scalar_bubble_diagram_state() -> State {
3629 test_initialise().expect("test initialisation should succeed");
3630 let mut state = State::new_test();
3631 state.model = load_generic_model("scalars");
3632 state.model_parameters = InputParamCard::default_from_model(&state.model);
3633
3634 let graph_path =
3635 crate::test_workspace_root().join("tests/resources/graphs/scalar_bubble.dot");
3636 let graphs = Graph::from_path(&graph_path, &state.model)
3637 .expect("scalar bubble graph fixture should load");
3638 state
3639 .import_graphs(
3640 graphs,
3641 Some("scalar_bubble".to_string()),
3642 None,
3643 Some("default".to_string()),
3644 false,
3645 false,
3646 )
3647 .expect("graph import should succeed");
3648 state
3649 }
3650
3651 fn scalar_bubble_master_graph_name(state: &State, integrand_name: &str) -> String {
3652 let process = &state.process_list.processes[0];
3653 match &process.collection {
3654 ProcessCollection::Amplitudes(amplitudes) => amplitudes
3655 .get(integrand_name)
3656 .expect("amplitude should exist")
3657 .graphs[0]
3658 .graph
3659 .name
3660 .clone(),
3661 ProcessCollection::CrossSections(cross_sections) => cross_sections
3662 .get(integrand_name)
3663 .expect("cross section should exist")
3664 .supergraphs[0]
3665 .graph
3666 .name
3667 .clone(),
3668 }
3669 }
3670
3671 #[test]
3672 fn select_copy_mode_creates_pregenerated_target_without_mutating_source() {
3673 let _guard = crate::LOG_TEST_MUTEX
3674 .lock()
3675 .unwrap_or_else(|err| err.into_inner());
3676 let mut state = build_scalar_bubble_diagram_state();
3677 let temp = tempdir().unwrap();
3678 let graph_name = scalar_bubble_master_graph_name(&state, "default");
3679 let selection = GraphGroupSelectionSpec::new().with_master_graph_names(vec![graph_name]);
3680 let target = GraphGroupSelectionTarget::copy(None, Some("selected".to_string()), false);
3681
3682 let selected = state
3683 .select_integrand_graph_groups(
3684 Some(&ProcessRef::Name("scalar_bubble".to_string())),
3685 Some(&"default".to_string()),
3686 &selection,
3687 &target,
3688 GraphGroupSelectionContext::new(&GenerationSettings::default(), temp.path(), false),
3689 )
3690 .unwrap();
3691
3692 assert!(selected.copied_to_output);
3693 assert_eq!(selected.process_id, 0);
3694 assert_eq!(selected.integrand_name, "selected");
3695 let process = &state.process_list.processes[0];
3696 assert!(process
3697 .collection
3698 .get_integrand_names()
3699 .contains(&"default"));
3700 assert!(process
3701 .collection
3702 .get_integrand_names()
3703 .contains(&"selected"));
3704 match &process.collection {
3705 ProcessCollection::Amplitudes(amplitudes) => {
3706 assert!(amplitudes["default"].integrand.is_none());
3707 assert!(amplitudes["selected"].integrand.is_none());
3708 assert_eq!(amplitudes["default"].graphs.len(), 1);
3709 assert_eq!(amplitudes["selected"].graphs.len(), 1);
3710 }
3711 ProcessCollection::CrossSections(cross_sections) => {
3712 assert!(cross_sections["default"].integrand.is_none());
3713 assert!(cross_sections["selected"].integrand.is_none());
3714 assert_eq!(cross_sections["default"].supergraphs.len(), 1);
3715 assert_eq!(cross_sections["selected"].supergraphs.len(), 1);
3716 }
3717 }
3718 }
3719
3720 #[test]
3721 fn select_copy_mode_rejects_and_clears_existing_target_integrand() {
3722 let _guard = crate::LOG_TEST_MUTEX
3723 .lock()
3724 .unwrap_or_else(|err| err.into_inner());
3725 let mut state = build_scalar_bubble_diagram_state();
3726 let temp = tempdir().unwrap();
3727 let graph_name = scalar_bubble_master_graph_name(&state, "default");
3728 let selection = GraphGroupSelectionSpec::new().with_master_graph_names(vec![graph_name]);
3729 let target = GraphGroupSelectionTarget::copy(None, Some("selected".to_string()), false);
3730
3731 state
3732 .select_integrand_graph_groups(
3733 Some(&ProcessRef::Name("scalar_bubble".to_string())),
3734 Some(&"default".to_string()),
3735 &selection,
3736 &target,
3737 GraphGroupSelectionContext::new(&GenerationSettings::default(), temp.path(), false),
3738 )
3739 .unwrap();
3740 let err = state
3741 .select_integrand_graph_groups(
3742 Some(&ProcessRef::Name("scalar_bubble".to_string())),
3743 Some(&"default".to_string()),
3744 &selection,
3745 &target,
3746 GraphGroupSelectionContext::new(&GenerationSettings::default(), temp.path(), false),
3747 )
3748 .unwrap_err();
3749 assert!(format!("{err}").contains("already exists"));
3750
3751 let process = &state.process_list.processes[0];
3752 let kind_folder = match &process.collection {
3753 ProcessCollection::Amplitudes(_) => "amplitudes",
3754 ProcessCollection::CrossSections(_) => "cross_sections",
3755 };
3756 let stale_integrand_folder = temp
3757 .path()
3758 .join("processes")
3759 .join(kind_folder)
3760 .join("scalar_bubble")
3761 .join("selected");
3762 fs::create_dir_all(stale_integrand_folder.join("integrand")).unwrap();
3763 state.generation_summaries.insert(
3764 IntegrandGenerationSummaryKey {
3765 process_id: 0,
3766 integrand_name: "selected".to_string(),
3767 },
3768 IntegrandGenerationSummary {
3769 peak_ram_bytes: 1,
3770 reports: Vec::new(),
3771 },
3772 );
3773
3774 let clear_target =
3775 GraphGroupSelectionTarget::copy(None, Some("selected".to_string()), true);
3776 let selected = state
3777 .select_integrand_graph_groups(
3778 Some(&ProcessRef::Name("scalar_bubble".to_string())),
3779 Some(&"default".to_string()),
3780 &selection,
3781 &clear_target,
3782 GraphGroupSelectionContext::new(&GenerationSettings::default(), temp.path(), false),
3783 )
3784 .unwrap();
3785 assert!(selected.replaced_existing_target);
3786 assert!(selected.removed_target_artifacts);
3787 assert!(!stale_integrand_folder.exists());
3788 assert!(!state
3789 .generation_summaries
3790 .contains_key(&IntegrandGenerationSummaryKey {
3791 process_id: 0,
3792 integrand_name: "selected".to_string(),
3793 }));
3794
3795 let err = state
3796 .select_integrand_graph_groups(
3797 Some(&ProcessRef::Name("scalar_bubble".to_string())),
3798 Some(&"default".to_string()),
3799 &selection,
3800 &clear_target,
3801 GraphGroupSelectionContext::new(&GenerationSettings::default(), temp.path(), true),
3802 )
3803 .unwrap_err();
3804 assert!(format!("{err}").contains("--read-only-state"));
3805 }
3806
3807 #[test]
3808 fn select_amplitude_graphs_mode_rejects_amplitude_integrands() {
3809 let _guard = crate::LOG_TEST_MUTEX
3810 .lock()
3811 .unwrap_or_else(|err| err.into_inner());
3812 let mut state = build_scalar_bubble_diagram_state();
3813 let temp = tempdir().unwrap();
3814 let graph_name = scalar_bubble_master_graph_name(&state, "default");
3815 let selection = GraphGroupSelectionSpec::new()
3816 .with_mode(GraphGroupSelectionMode::CrossSectionAmplitudeGraphs)
3817 .with_master_graph_names(vec![graph_name]);
3818 let target = GraphGroupSelectionTarget::in_place();
3819
3820 let err = state
3821 .select_integrand_graph_groups(
3822 Some(&ProcessRef::Name("scalar_bubble".to_string())),
3823 Some(&"default".to_string()),
3824 &selection,
3825 &target,
3826 GraphGroupSelectionContext::new(&GenerationSettings::default(), temp.path(), false),
3827 )
3828 .unwrap_err();
3829
3830 assert!(format!("{err}").contains("--amplitude-graphs"));
3831 }
3832
3833 #[test]
3834 fn select_raised_cut_filters_reject_amplitude_integrands() {
3835 let _guard = crate::LOG_TEST_MUTEX
3836 .lock()
3837 .unwrap_or_else(|err| err.into_inner());
3838 let mut state = build_scalar_bubble_diagram_state();
3839 let temp = tempdir().unwrap();
3840 let selection = GraphGroupSelectionSpec::new().with_raised_cut_signatures(
3841 SelectionPolarity::With,
3842 RaisedPropagatorScope::All,
3843 vec![RaisedPropagatorSignature::from_str("[]").unwrap()],
3844 );
3845 let target = GraphGroupSelectionTarget::in_place();
3846
3847 let err = state
3848 .select_integrand_graph_groups(
3849 Some(&ProcessRef::Name("scalar_bubble".to_string())),
3850 Some(&"default".to_string()),
3851 &selection,
3852 &target,
3853 GraphGroupSelectionContext::new(&GenerationSettings::default(), temp.path(), false),
3854 )
3855 .unwrap_err();
3856
3857 assert!(format!("{err}").contains("cross-section integrands"));
3858 }
3859
3860 #[test]
3861 fn test_run_history() {
3862 use crate::state::RunHistory;
3863 let mut run_history: RunHistory = Default::default();
3865 let kinematics_settings = KinematicsSettings {
3866 e_cm: 100.0,
3867 externals: Externals::Constant {
3868 momenta: vec![
3869 ExternalMomenta::Independent([F(1.), F(2.), F(3.), F(4.)]),
3870 ExternalMomenta::Dependent(Dep::Dep),
3871 ],
3872 improvement_settings: PhaseSpaceImprovementSettings::default(),
3873 helicities: vec![Helicity::PLUS, Helicity::MINUS],
3874 f_64_cache: None,
3875 f_128_cache: None,
3876 },
3877 };
3878
3879 run_history.push(Commands::Set(Set::Global {
3880 input: SetArgs::Stored,
3881 }));
3882
3883 run_history.default_runtime_settings.kinematics = kinematics_settings;
3884 set_serialize_commands_as_strings(true);
3885 let toml = toml::to_string_pretty(&run_history).unwrap();
3886 println!("{}", toml);
3887 let deserialized: RunHistory = toml::from_str(&toml).unwrap();
3888 assert_eq!(run_history, deserialized);
3889 SHOWDEFAULTS.store(false, std::sync::atomic::Ordering::Relaxed);
3890
3891 run_history.to_file("test_path.toml", true).unwrap();
3892 let deserialized_from_file = RunHistory::from_file("test_path.toml", " ").unwrap();
3893 assert_eq!(run_history, deserialized_from_file);
3894 }
3895
3896 #[test]
3897 fn run_history_applies_default_runtime_before_commands() {
3898 let mut run_history = RunHistory {
3899 default_runtime_settings: toml::from_str(
3900 r#"
3901[sampling]
3902graphs = "monte_carlo"
3903orientations = "summed"
3904lmb_multichanneling = false
3905lmb_channels = "summed"
3906coordinate_system = "tropical"
3907mapping = "linear"
3908b = 1.0
3909"#,
3910 )
3911 .unwrap(),
3912 ..Default::default()
3913 };
3914
3915 let mut state = State::new_test();
3916 let mut cli_settings = CLISettings::default();
3917 let mut default_runtime_settings = RuntimeSettings::default();
3918
3919 let _ = run_history
3920 .run(&mut state, &mut cli_settings, &mut default_runtime_settings)
3921 .unwrap();
3922
3923 assert_eq!(
3924 default_runtime_settings.sampling,
3925 run_history.default_runtime_settings.sampling
3926 );
3927 }
3928
3929 #[test]
3930 fn test_command_history_serialization() {
3931 use super::{set_serialize_commands_as_strings, CommandHistory};
3932 use crate::commands::Commands;
3933
3934 let cmd_history = CommandHistory::new(Commands::Quit(SaveState::default()));
3936 assert_eq!(cmd_history.raw_string, None);
3937
3938 let cmd_history_with_raw =
3940 CommandHistory::new_with_raw(Commands::Quit(SaveState::default()), "quit".to_string());
3941 assert_eq!(cmd_history_with_raw.raw_string, Some("quit".to_string()));
3942
3943 set_serialize_commands_as_strings(false);
3945
3946 let json = serde_json::to_string(&cmd_history).unwrap();
3947 let deserialized_json: CommandHistory = serde_json::from_str(&json).unwrap();
3948 let toml = toml::to_string(&cmd_history).unwrap();
3949 let deserialized_toml: CommandHistory = toml::from_str(&toml).unwrap();
3950 assert_eq!(cmd_history, deserialized_json);
3951 assert_eq!(cmd_history, deserialized_toml);
3952
3953 set_serialize_commands_as_strings(true);
3955
3956 let json_string = serde_json::to_string_pretty(&cmd_history_with_raw).unwrap();
3957 assert!(json_string.contains("quit"));
3958
3959 set_serialize_commands_as_strings(false);
3961 }
3962
3963 #[test]
3964 fn test_command_history_toml_and_json_formats() {
3965 use super::{set_serialize_commands_as_strings, CommandHistory};
3966 use crate::commands::Commands;
3967
3968 let quit_cmd = CommandHistory::new(Commands::Quit(SaveState::default()));
3970 let quit_with_raw =
3971 CommandHistory::new_with_raw(Commands::Quit(SaveState::default()), "quit".to_string());
3972
3973 {
3975 set_serialize_commands_as_strings(false);
3977 let json = serde_json::to_string_pretty(&quit_cmd).unwrap();
3978 let deserialized: CommandHistory = serde_json::from_str(&json).unwrap();
3979 assert_eq!(quit_cmd, deserialized);
3980
3981 set_serialize_commands_as_strings(true);
3983 let json_string = serde_json::to_string_pretty(&quit_with_raw).unwrap();
3984 assert!(json_string.contains("quit"));
3985 let deserialized_string: CommandHistory = serde_json::from_str(&json_string).unwrap();
3986 assert_eq!(quit_with_raw, deserialized_string);
3987 }
3988
3989 {
3991 #[derive(serde::Serialize, serde::Deserialize)]
3992 struct CommandWrapper {
3993 command: CommandHistory,
3994 }
3995
3996 set_serialize_commands_as_strings(false);
3998 let wrapper = CommandWrapper {
3999 command: quit_cmd.clone(),
4000 };
4001 let toml = toml::to_string_pretty(&wrapper).unwrap();
4002 let deserialized_wrapper: CommandWrapper = toml::from_str(&toml).unwrap();
4003 assert_eq!(quit_cmd, deserialized_wrapper.command);
4004
4005 set_serialize_commands_as_strings(true);
4007 let wrapper_string = CommandWrapper {
4008 command: quit_with_raw.clone(),
4009 };
4010 let toml_string = toml::to_string_pretty(&wrapper_string).unwrap();
4011 assert!(toml_string.contains("quit"));
4012 let deserialized_string_wrapper: CommandWrapper = toml::from_str(&toml_string).unwrap();
4013 assert_eq!(quit_with_raw, deserialized_string_wrapper.command);
4014 }
4015
4016 {
4018 set_serialize_commands_as_strings(false);
4019
4020 let json = serde_json::to_string(&quit_cmd).unwrap();
4022 let json_value: serde_json::Value = serde_json::from_str(&json).unwrap();
4023 let from_json: CommandHistory = serde_json::from_value(json_value).unwrap();
4024 assert_eq!(quit_cmd, from_json);
4025 }
4026 set_serialize_commands_as_strings(false);
4028 }
4029
4030 #[test]
4031 fn run_history_push_with_raw_skips_quit_and_definition_commands() {
4032 use super::RunHistory;
4033 use crate::commands::{run::Run, StartCommandsBlock};
4034
4035 let mut run_history = RunHistory::default();
4036 run_history.push_with_raw(
4037 Commands::Run(Run {
4038 block_names: vec!["block_a".to_string()],
4039 commands: None,
4040 }),
4041 Some("run block_a".to_string()),
4042 );
4043 run_history.push_with_raw(
4044 Commands::Quit(SaveState::default()),
4045 Some("quit".to_string()),
4046 );
4047 run_history.push_with_raw(
4048 Commands::StartCommandsBlock(StartCommandsBlock {
4049 name: "block_a".to_string(),
4050 }),
4051 Some("start_commands_block block_a".to_string()),
4052 );
4053 run_history.push_with_raw(
4054 Commands::FinishCommandsBlock,
4055 Some("finish_commands_block".to_string()),
4056 );
4057
4058 assert_eq!(run_history.commands.len(), 1);
4059 assert_eq!(
4060 run_history.commands[0].raw_string.as_deref(),
4061 Some("run block_a")
4062 );
4063 }
4064
4065 #[test]
4066 fn run_history_filtered_for_save_preserves_command_blocks() {
4067 let mut run_history = RunHistory {
4068 command_blocks: vec![
4069 CommandsBlock {
4070 name: "generate".to_string(),
4071 commands: vec![CommandHistory::new_with_raw(
4072 Commands::Display(Display::Processes),
4073 "display processes".to_string(),
4074 )],
4075 },
4076 CommandsBlock {
4077 name: "integrate".to_string(),
4078 commands: vec![CommandHistory::new_with_raw(
4079 CommandHistory::from_raw_string("quit -o").unwrap().command,
4080 "quit -o".to_string(),
4081 )],
4082 },
4083 ],
4084 ..Default::default()
4085 };
4086 run_history.push_with_raw(
4087 CommandHistory::from_raw_string("display processes")
4088 .unwrap()
4089 .command,
4090 Some("display processes".to_string()),
4091 );
4092
4093 let filtered = run_history.filtered_for_save();
4094 set_serialize_commands_as_strings(true);
4095 let toml = toml::to_string_pretty(&filtered).unwrap();
4096 set_serialize_commands_as_strings(false);
4097
4098 assert_eq!(filtered.command_blocks.len(), 2);
4099 assert!(toml.contains("commands = ["));
4100 assert!(toml.contains("[[command_blocks]]"));
4101 assert!(toml.contains("quit -o"));
4102 }
4103
4104 #[test]
4105 fn command_history_parses_multiline_set_string() {
4106 let raw = "set process -p epem_a_tth -i LO string '[integrator]\nn_start = 1000\n'";
4107 let cmd = CommandHistory::from_raw_string(raw).unwrap();
4108 assert_eq!(cmd.raw_string.as_deref(), Some(raw));
4109
4110 match cmd.command {
4111 Commands::Set(Set::Process { input, .. }) => match input {
4112 ProcessSetArgs::String { string } => {
4113 assert_eq!(string, "[integrator]\nn_start = 1000\n");
4114 }
4115 other => panic!("Expected string set input, got {other:?}"),
4116 },
4117 other => panic!("Expected set process command, got {other:?}"),
4118 }
4119 }
4120
4121 #[test]
4122 fn command_history_parses_hash_process_refs() {
4123 let cmd = CommandHistory::from_raw_string("display integrand -p #12").unwrap();
4124 match cmd.command {
4125 Commands::Display(Display::Integrands {
4126 process,
4127 integrand_name,
4128 graphs,
4129 categories,
4130 hide_non_existing_thresholds,
4131 }) => {
4132 assert_eq!(process, Some(ProcessRef::Id(12)));
4133 assert_eq!(integrand_name, None);
4134 assert!(graphs.is_empty());
4135 assert!(categories.is_empty());
4136 assert!(!hide_non_existing_thresholds);
4137 }
4138 other => panic!("Expected display integrand command, got {other:?}"),
4139 }
4140 }
4141
4142 #[test]
4143 fn command_history_requires_explicit_generate_mode() {
4144 assert!(CommandHistory::from_raw_string("generate e+ e- > d d~").is_err());
4145 }
4146
4147 #[test]
4148 fn run_history_parses_triple_quoted_set_kv_command() {
4149 let toml = r#"
4150commands = [
4151 """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]}}'""",
4152]
4153
4154[default_runtime_settings.general]
4155integral_unit = "picobarn"
4156"#;
4157
4158 let run_history: RunHistory = toml::from_str(toml).unwrap();
4159 assert_eq!(run_history.commands.len(), 1);
4160
4161 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]}}'"#;
4162 let command_history = &run_history.commands[0];
4163 assert_eq!(command_history.raw_string.as_deref(), Some(expected_cmd));
4164
4165 match &command_history.command {
4166 Commands::Set(Set::DefaultRuntime {
4167 input: SetArgs::Kv { pairs },
4168 }) => {
4169 assert_eq!(pairs.len(), 1);
4170 assert_eq!(pairs[0].key, "kinematics.externals");
4171 assert_eq!(
4172 pairs[0].value,
4173 r#"{"type":"constant","data":{"momenta":[[1.0,2.0,3.0,4.0],[5.0,6.0,-7.0,-8.0]],"helicities":[1,1]}}"#
4174 );
4175 }
4176 other => panic!("Expected set default-runtime kv command, got {other:?}"),
4177 }
4178
4179 assert_eq!(
4180 run_history.default_runtime_settings.general.integral_unit,
4181 gammalooprs::settings::runtime::IntegralUnit::Picobarn
4182 );
4183 }
4184
4185 #[test]
4186 fn run_history_load_preserves_command_blocks() {
4187 let temp = tempdir().unwrap();
4188 let run_path = temp.path().join("run.toml");
4189 fs::write(
4190 &run_path,
4191 r#"
4192[[command_blocks]]
4193name = "zeta"
4194commands = ["quit -n"]
4195
4196[[command_blocks]]
4197name = "alpha"
4198commands = ["quit -o"]
4199"#,
4200 )
4201 .unwrap();
4202
4203 let run_history = RunHistory::load(&run_path).unwrap();
4204 assert!(run_history.commands.is_empty());
4205 assert_eq!(run_history.command_blocks.len(), 2);
4206 }
4207
4208 #[test]
4209 fn run_history_selects_named_command_blocks_in_order() {
4210 let temp = tempdir().unwrap();
4211 let run_path = temp.path().join("run.toml");
4212 fs::write(
4213 &run_path,
4214 r#"
4215[[command_blocks]]
4216name = "first"
4217commands = ["quit -n"]
4218
4219[[command_blocks]]
4220name = "second"
4221commands = ["quit -o"]
4222"#,
4223 )
4224 .unwrap();
4225
4226 let requested = vec!["second".to_string(), "first".to_string()];
4227 let run_history = RunHistory::load(&run_path).unwrap();
4228 let selected = run_history
4229 .select_command_blocks(requested.as_slice())
4230 .unwrap();
4231 assert_eq!(selected.len(), 2);
4232 assert_eq!(
4233 selected[0].commands[0].raw_string.as_deref(),
4234 Some("quit -o")
4235 );
4236 assert_eq!(
4237 selected[1].commands[0].raw_string.as_deref(),
4238 Some("quit -n")
4239 );
4240 }
4241
4242 #[test]
4243 fn run_history_selection_rejects_unknown_command_block() {
4244 let temp = tempdir().unwrap();
4245 let run_path = temp.path().join("run.toml");
4246 fs::write(
4247 &run_path,
4248 r#"
4249[[command_blocks]]
4250name = "first"
4251commands = ["quit -n"]
4252"#,
4253 )
4254 .unwrap();
4255
4256 let requested = vec!["missing".to_string()];
4257 let run_history = RunHistory::load(&run_path).unwrap();
4258 let err = run_history
4259 .select_command_blocks(requested.as_slice())
4260 .unwrap_err();
4261 let message = format!("{err}");
4262 assert!(message.contains("Unknown command block 'missing'"));
4263 assert!(message.contains("first"));
4264 }
4265
4266 #[test]
4267 fn run_history_selection_rejects_missing_command_block_when_only_legacy_commands_exist() {
4268 let temp = tempdir().unwrap();
4269 let run_path = temp.path().join("run.toml");
4270 fs::write(
4271 &run_path,
4272 r#"
4273commands = ["quit -o"]
4274"#,
4275 )
4276 .unwrap();
4277
4278 let requested = vec!["first".to_string()];
4279 let run_history = RunHistory::load(&run_path).unwrap();
4280 let err = run_history
4281 .select_command_blocks(requested.as_slice())
4282 .unwrap_err();
4283 assert!(format!("{err}").contains("Unknown command block"));
4284 }
4285
4286 #[test]
4287 fn run_history_load_accepts_commands_and_command_blocks_together() {
4288 let temp = tempdir().unwrap();
4289 let run_path = temp.path().join("run.toml");
4290 fs::write(
4291 &run_path,
4292 r#"
4293commands = ["quit -o"]
4294
4295[[command_blocks]]
4296name = "first"
4297commands = ["quit -n"]
4298"#,
4299 )
4300 .unwrap();
4301
4302 let run_history = RunHistory::load(&run_path).unwrap();
4303 assert_eq!(run_history.commands.len(), 1);
4304 assert_eq!(run_history.command_blocks.len(), 1);
4305 assert_eq!(
4306 run_history.commands[0].raw_string.as_deref(),
4307 Some("quit -o")
4308 );
4309 }
4310
4311 #[test]
4312 fn run_history_load_rejects_duplicate_command_block_names() {
4313 let temp = tempdir().unwrap();
4314 let run_path = temp.path().join("run.toml");
4315 fs::write(
4316 &run_path,
4317 r#"
4318[[command_blocks]]
4319name = "first"
4320commands = ["quit -o"]
4321
4322[[command_blocks]]
4323name = "first"
4324commands = ["quit -n"]
4325"#,
4326 )
4327 .unwrap();
4328
4329 let err = RunHistory::load(&run_path).unwrap_err();
4330 assert!(format!("{err}").contains("duplicate block name"));
4331 }
4332
4333 #[test]
4334 fn state_manifest_roundtrip_current_version() {
4335 let temp = tempdir().unwrap();
4336 save_state_manifest(temp.path()).unwrap();
4337
4338 let manifest = load_state_manifest(temp.path()).unwrap();
4339 assert_eq!(manifest.version, CURRENT_STATE_MANIFEST_VERSION);
4340 }
4341
4342 #[test]
4343 fn state_manifest_rejects_future_versions() {
4344 let temp = tempdir().unwrap();
4345 let future_manifest = StateManifest {
4346 version: CURRENT_STATE_MANIFEST_VERSION + 1,
4347 };
4348 fs::write(
4349 temp.path().join(STATE_MANIFEST_FILE),
4350 toml::to_string_pretty(&future_manifest).unwrap(),
4351 )
4352 .unwrap();
4353
4354 let err = load_state_manifest(temp.path()).unwrap_err();
4355 assert!(format!("{err}").contains("newer than this binary supports"));
4356 }
4357
4358 #[test]
4359 fn state_folder_classifies_saved_layout() {
4360 let temp = tempdir().unwrap();
4361 save_state_manifest(temp.path()).unwrap();
4362 fs::write(temp.path().join("model.json"), "{}").unwrap();
4363 fs::write(temp.path().join("symbolica_state.bin"), []).unwrap();
4364 fs::create_dir_all(temp.path().join("processes")).unwrap();
4365
4366 assert_eq!(
4367 classify_state_folder(temp.path()).unwrap(),
4368 StateFolderKind::Saved
4369 );
4370 }
4371
4372 #[test]
4373 fn state_folder_classifies_logs_only_folder_as_scratch() {
4374 let temp = tempdir().unwrap();
4375 fs::create_dir_all(temp.path().join("logs")).unwrap();
4376 fs::write(temp.path().join("logs").join("gammalog.jsonl"), "").unwrap();
4377
4378 assert_eq!(
4379 classify_state_folder(temp.path()).unwrap(),
4380 StateFolderKind::Scratch
4381 );
4382 }
4383
4384 #[test]
4385 fn state_folder_classifies_non_manifest_contents_as_unmanifested() {
4386 let temp = tempdir().unwrap();
4387 fs::create_dir_all(temp.path().join("processes").join("amplitudes")).unwrap();
4388
4389 assert_eq!(
4390 classify_state_folder(temp.path()).unwrap(),
4391 StateFolderKind::Unmanifested
4392 );
4393 }
4394
4395 #[test]
4396 fn activate_loaded_integrand_backends_falls_back_to_eager_when_external_artifacts_are_missing()
4397 {
4398 let mut state = build_generated_scalar_bubble_state_with_external_backend();
4399
4400 state.activate_loaded_integrand_backends(false).unwrap();
4401
4402 for process in &state.process_list.processes {
4403 match &process.collection {
4404 ProcessCollection::Amplitudes(amplitudes) => {
4405 for amplitude in amplitudes.values() {
4406 let integrand = amplitude.integrand.as_ref().unwrap();
4407 if matches!(
4408 integrand.frozen_compilation(),
4409 FrozenCompilationMode::Cpp(_) | FrozenCompilationMode::Assembly(_)
4410 ) {
4411 assert_eq!(integrand.active_f64_backend(), ActiveF64Backend::Eager);
4412 }
4413 }
4414 }
4415 ProcessCollection::CrossSections(cross_sections) => {
4416 for cross_section in cross_sections.values() {
4417 let integrand = cross_section.integrand.as_ref().unwrap();
4418 if matches!(
4419 integrand.frozen_compilation(),
4420 FrozenCompilationMode::Cpp(_) | FrozenCompilationMode::Assembly(_)
4421 ) {
4422 assert_eq!(integrand.active_f64_backend(), ActiveF64Backend::Eager);
4423 }
4424 }
4425 }
4426 }
4427 }
4428 }
4429
4430 #[test]
4431 fn resolve_effective_model_parameter_card_overlays_runtime_model_settings() {
4432 let mut state = State::new_test();
4433 state.model = load_generic_model("scalars");
4434 state.model_parameters = InputParamCard::default_from_model(&state.model);
4435
4436 let mut settings = RuntimeSettings::default();
4437 settings
4438 .model
4439 .external_parameters
4440 .insert("mass_scalar_2".to_string(), (F(7.5), F(0.0)));
4441
4442 let resolved = state
4443 .resolve_effective_model_parameter_card_for_settings(&settings)
4444 .unwrap();
4445
4446 assert_eq!(
4447 resolved[&UFOSymbol::from("mass_scalar_2")],
4448 Complex::new(F(7.5), F(0.0))
4449 );
4450 assert_eq!(
4451 resolved[&UFOSymbol::from("mass_scalar_1")],
4452 state.model_parameters[&UFOSymbol::from("mass_scalar_1")]
4453 );
4454 }
4455
4456 #[test]
4457 fn resolve_effective_model_parameter_card_rejects_non_overridable_parameters() {
4458 let mut state = State::new_test();
4459 state.model = load_generic_model("scalars");
4460 state.model_parameters = InputParamCard::default_from_model(&state.model);
4461 state
4462 .model_parameters
4463 .remove(&UFOSymbol::from("mass_scalar_2"));
4464
4465 let mut settings = RuntimeSettings::default();
4466 settings
4467 .model
4468 .external_parameters
4469 .insert("mass_scalar_2".to_string(), (F(7.5), F(0.0)));
4470
4471 let err = state
4472 .resolve_effective_model_parameter_card_for_settings(&settings)
4473 .unwrap_err();
4474
4475 assert!(err
4476 .to_string()
4477 .contains("cannot be overridden because it is not present"));
4478 }
4479}
4480
4481#[derive(Args, Debug, Clone)]
4482pub struct ExistingArgs {
4483 pub process_id: u32,
4484 pub name: Option<String>,
4485}