1use std::{
2 collections::{BTreeMap, HashSet},
3 fmt, fs,
4 io::{self, IsTerminal, Write},
5 ops::Deref,
6 path::PathBuf,
7 sync::mpsc,
8 thread,
9 time::Duration,
10};
11
12use clap::{ArgAction, Args, ValueEnum};
13use crossterm::{
14 cursor::{Hide, MoveDown, MoveToColumn, MoveUp, Show},
15 event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
16 execute, queue,
17 terminal::{
18 self, disable_raw_mode, enable_raw_mode, Clear, ClearType, EnterAlternateScreen,
19 LeaveAlternateScreen,
20 },
21};
22use gammalooprs::utils::serde_utils::SmartSerde;
23use schemars::JsonSchema;
24use serde::{Deserialize, Serialize};
25use spenso::algebra::complex::Complex;
26use unicode_width::UnicodeWidthChar;
27
28use color_eyre::{eyre::eyre, Result};
29use colored::Colorize;
30use gammalooprs::{
31 integrands::{HasIntegrand, Integrand},
32 integrate::{
33 build_integration_result, emit_integration_status_via_tracing, havana_integrate,
34 latest_observable_resume_state_path, print_integral_result,
35 render_saved_integration_summary, render_status_update_tabled, slot_workspace_path,
36 workspace_manifest_path, workspace_state_path, ContributionSortMode,
37 HavanaIntegrateRequest, IntegrationSlot, IntegrationState, IntegrationStatusKind,
38 IntegrationStatusPhaseDisplay, IntegrationStatusViewOptions, IntegrationWorkspaceManifest,
39 IterationBatchingSettings, RatatuiDashboardState, SamplingCorrelationMode, SlotMeta,
40 StatusUpdate, TabledRenderOptions, WorkspaceSnapshotControl,
41 },
42 model::{Model, SerializableInputParamCard},
43 observables::ObservableSnapshotBundle,
44 request_interrupt, request_iteration_abort,
45 settings::{
46 runtime::{IntegratedPhase, IntegrationResult, SamplingSettings},
47 RuntimeSettings,
48 },
49 utils::F,
50};
51use itertools::{izip, Itertools};
52use ratatui::{backend::CrosstermBackend, Terminal};
53use symbolica::numerical_integration::Grid;
54use tracing::{info, warn};
55
56use crate::{
57 completion::CompletionArgExt,
58 state::{ProcessRef, State},
59 CLISettings,
60};
61
62#[cfg_attr(
63 feature = "python_api",
64 pyo3::pyclass(from_py_object, unsendable, name = "IntegrationSettings")
65)]
66#[derive(Debug, Args, Serialize, Deserialize, Clone, JsonSchema, PartialEq)]
67pub struct Integrate {
68 #[arg(
70 short = 'p',
71 long = "process",
72 value_name = "PROCESS",
73 completion_process_selector(crate::completion::SelectorKind::Any)
74 )]
75 pub process: Vec<ProcessRef>,
76
77 #[arg(
79 short = 'i',
80 long = "integrand-name",
81 value_name = "NAME",
82 completion_integrand_selector(crate::completion::SelectorKind::Any)
83 )]
84 pub integrand_name: Vec<String>,
85
86 #[arg(short = 'c', long)]
88 pub n_cores: Option<usize>,
89
90 #[arg(short = 'w', long, value_hint = clap::ValueHint::DirPath)]
92 pub workspace_path: Option<PathBuf>,
93
94 #[arg(
96 short = 't',
97 long,
98 num_args = 1..=2,
99 action = ArgAction::Append,
100 allow_negative_numbers = true,
101 completion_selected_integrand_target()
102 )]
103 pub target: Vec<String>,
104
105 #[arg(short = 'r', long)]
107 pub restart: bool,
108
109 #[arg(long = "uncorrelated")]
111 pub uncorrelated: bool,
112
113 #[arg(long = "show-max-weight-info", default_value_t = true)]
115 pub show_max_weight_info: bool,
116
117 #[arg(long = "no-show-integration-statistics")]
119 pub no_show_integration_statistics: bool,
120
121 #[arg(long = "show-phase", default_value = "both")]
123 pub show_phase: ShowPhaseOption,
124
125 #[arg(long = "show-top-discrete-grid")]
127 pub show_top_discrete_grid: bool,
128
129 #[arg(long = "show-discrete-contributions-sum")]
131 pub show_discrete_contributions_sum: bool,
132
133 #[arg(long = "sort-contributions", default_value = "error")]
135 pub sort_contributions: ContributionSortOption,
136
137 #[arg(
139 long = "show-max-weight-info-for-discrete-bins",
140 default_value_t = false
141 )]
142 pub show_max_weight_info_for_discrete_bins: bool,
143
144 #[arg(long = "show-summary-only")]
146 pub show_summary_only: bool,
147
148 #[arg(long = "no-stream-iterations")]
150 pub no_stream_iterations: bool,
151
152 #[arg(long = "no-stream-updates")]
154 pub no_stream_updates: bool,
155
156 #[arg(long = "renderer", default_value = "ratatui")]
158 pub renderer: RendererOption,
159
160 #[arg(long = "batch-size")]
162 pub batch_size: Option<usize>,
163
164 #[arg(long = "batch-timing", default_value_t = 5.0)]
166 pub batch_timing: f64,
167
168 #[arg(long = "min-time-between-status-updates", default_value_t = 0.0)]
170 pub min_time_between_status_updates: f64,
171
172 #[arg(long = "max-table-width", default_value_t = 250)]
174 pub max_table_width: usize,
175
176 #[arg(long = "write-results-for-each-iteration")]
178 pub write_results_for_each_iteration: bool,
179}
180
181#[derive(Debug, Clone, Default, Serialize, Deserialize)]
182#[serde(default, deny_unknown_fields)]
183pub struct IntegrationOutput {
184 pub result: IntegrationResult,
186 pub observables: BTreeMap<String, ObservableSnapshotBundle>,
188 pub workspace_path: PathBuf,
190}
191
192impl IntegrationOutput {
193 pub fn slot_observables(&self, key: &str) -> Option<&ObservableSnapshotBundle> {
194 self.observables.get(key)
195 }
196
197 pub fn single_slot_observables(&self) -> Option<&ObservableSnapshotBundle> {
198 (self.observables.len() == 1)
199 .then(|| self.observables.values().next())
200 .flatten()
201 }
202}
203
204impl Deref for IntegrationOutput {
205 type Target = IntegrationResult;
206
207 fn deref(&self) -> &Self::Target {
208 &self.result
209 }
210}
211
212impl fmt::Display for IntegrationOutput {
213 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214 self.result.fmt(f)
215 }
216}
217
218impl Default for Integrate {
219 fn default() -> Self {
220 Self {
221 process: Vec::new(),
222 integrand_name: Vec::new(),
223 n_cores: None,
224 workspace_path: None,
225 target: Vec::new(),
226 restart: false,
227 uncorrelated: false,
228 show_max_weight_info: true,
229 no_show_integration_statistics: false,
230 show_phase: ShowPhaseOption::Both,
231 show_top_discrete_grid: false,
232 show_discrete_contributions_sum: false,
233 sort_contributions: ContributionSortOption::Error,
234 show_max_weight_info_for_discrete_bins: false,
235 show_summary_only: false,
236 no_stream_iterations: false,
237 no_stream_updates: false,
238 renderer: RendererOption::Ratatui,
239 batch_size: None,
240 batch_timing: 5.0,
241 min_time_between_status_updates: 0.0,
242 max_table_width: 250,
243 write_results_for_each_iteration: false,
244 }
245 }
246}
247
248#[derive(
249 Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ValueEnum, Default,
250)]
251pub enum ShowPhaseOption {
252 #[default]
253 Both,
254 Real,
255 Imag,
256 Selected,
257}
258
259impl ShowPhaseOption {
260 fn resolve(self, selected_phase: IntegratedPhase) -> IntegrationStatusPhaseDisplay {
261 match self {
262 Self::Both => IntegrationStatusPhaseDisplay::Both,
263 Self::Real => IntegrationStatusPhaseDisplay::Real,
264 Self::Imag => IntegrationStatusPhaseDisplay::Imag,
265 Self::Selected => match selected_phase {
266 IntegratedPhase::Imag => IntegrationStatusPhaseDisplay::Imag,
267 IntegratedPhase::Both => IntegrationStatusPhaseDisplay::Both,
268 IntegratedPhase::Real => IntegrationStatusPhaseDisplay::Real,
269 },
270 }
271 }
272}
273
274#[derive(
275 Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ValueEnum, Default,
276)]
277pub enum ContributionSortOption {
278 Index,
279 Integral,
280 #[default]
281 Error,
282}
283
284#[derive(
285 Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ValueEnum, Default,
286)]
287pub enum RendererOption {
288 Tabled,
289 #[default]
290 Ratatui,
291}
292
293impl From<ContributionSortOption> for ContributionSortMode {
294 fn from(value: ContributionSortOption) -> Self {
295 match value {
296 ContributionSortOption::Index => ContributionSortMode::Index,
297 ContributionSortOption::Integral => ContributionSortMode::Integral,
298 ContributionSortOption::Error => ContributionSortMode::Error,
299 }
300 }
301}
302
303struct TabledStreamRenderer {
304 stderr: io::Stderr,
305 rendered_line_count: usize,
306 control_listener: Option<TabledControlListener>,
307}
308
309impl TabledStreamRenderer {
310 fn new() -> Self {
311 Self {
312 stderr: io::stderr(),
313 rendered_line_count: 0,
314 control_listener: None,
315 }
316 }
317
318 fn render(&mut self, block: &str) -> Result<()> {
319 self.ensure_control_listener();
320 let prepared = prepare_stream_block(block, stream_terminal_width());
321 if self.rendered_line_count > 0 {
322 self.clear_rendered_block()?;
323 }
324
325 queue!(self.stderr, MoveToColumn(0))?;
326 write!(self.stderr, "{prepared}")?;
327 self.stderr.flush()?;
328 self.rendered_line_count = prepared.lines().count().max(1);
329 Ok(())
330 }
331
332 fn clear(&mut self) -> Result<()> {
333 if self.rendered_line_count > 0 {
334 self.clear_rendered_block()?;
335 self.rendered_line_count = 0;
336 self.stderr.flush()?;
337 }
338
339 self.shutdown_control_listener()?;
340 Ok(())
341 }
342
343 fn ensure_control_listener(&mut self) {
344 if self.control_listener.is_none() {
345 self.control_listener = TabledControlListener::new()
346 .map_err(|err| {
347 warn!("Failed to start tabled streaming control listener: {err}");
348 err
349 })
350 .ok();
351 }
352 }
353
354 fn shutdown_control_listener(&mut self) -> Result<()> {
355 if let Some(mut control_listener) = self.control_listener.take() {
356 control_listener.shutdown()?;
357 }
358 Ok(())
359 }
360
361 fn clear_rendered_block(&mut self) -> Result<()> {
362 queue!(self.stderr, MoveToColumn(0))?;
363 if self.rendered_line_count > 1 {
364 queue!(
365 self.stderr,
366 MoveUp((self.rendered_line_count.saturating_sub(1)) as u16)
367 )?;
368 }
369
370 for line_index in 0..self.rendered_line_count {
371 queue!(self.stderr, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
372 if line_index + 1 < self.rendered_line_count {
373 queue!(self.stderr, MoveDown(1))?;
374 }
375 }
376 queue!(self.stderr, MoveToColumn(0))?;
377 if self.rendered_line_count > 1 {
378 queue!(
379 self.stderr,
380 MoveUp((self.rendered_line_count.saturating_sub(1)) as u16)
381 )?;
382 }
383 Ok(())
384 }
385}
386
387impl Drop for TabledStreamRenderer {
388 fn drop(&mut self) {
389 let _ = self.clear();
390 }
391}
392
393enum TabledControlCommand {
394 Shutdown(mpsc::SyncSender<Result<()>>),
395}
396
397struct TabledControlListener {
398 sender: mpsc::Sender<TabledControlCommand>,
399 handle: Option<thread::JoinHandle<Result<()>>>,
400}
401
402impl TabledControlListener {
403 fn new() -> Result<Self> {
404 let (sender, receiver) = mpsc::channel();
405 let (startup_sender, startup_receiver) = mpsc::sync_channel(0);
406 let handle = thread::spawn(move || tabled_control_event_loop(receiver, startup_sender));
407 startup_receiver
408 .recv()
409 .map_err(|err| eyre!("Failed to start tabled control listener: {err}"))?
410 .map_err(|err| eyre!("Failed to start tabled control listener: {err}"))?;
411 Ok(Self {
412 sender,
413 handle: Some(handle),
414 })
415 }
416
417 fn shutdown(&mut self) -> Result<()> {
418 let (ack_sender, ack_receiver) = mpsc::sync_channel(0);
419 self.sender
420 .send(TabledControlCommand::Shutdown(ack_sender))
421 .map_err(|err| eyre!("Failed to stop tabled control listener: {err}"))?;
422 ack_receiver.recv().map_err(|err| {
423 eyre!("Failed to receive tabled control listener acknowledgement: {err}")
424 })??;
425 if let Some(handle) = self.handle.take() {
426 handle
427 .join()
428 .map_err(|_| eyre!("Tabled control listener thread panicked"))??;
429 }
430 Ok(())
431 }
432}
433
434impl Drop for TabledControlListener {
435 fn drop(&mut self) {
436 let _ = self.shutdown();
437 }
438}
439
440enum DashboardCommand {
441 Update(Box<StatusUpdate>),
442 Suspend(mpsc::SyncSender<Result<()>>),
443 Shutdown(mpsc::SyncSender<Result<()>>),
444}
445
446struct RatatuiTerminal {
447 terminal: Terminal<CrosstermBackend<io::Stderr>>,
448}
449
450impl RatatuiTerminal {
451 fn enter() -> Result<Self> {
452 enable_raw_mode()?;
453 let mut stderr = io::stderr();
454 execute!(stderr, EnterAlternateScreen, Hide)?;
455 let backend = CrosstermBackend::new(io::stderr());
456 let mut terminal = Terminal::new(backend)?;
457 terminal.clear()?;
458 Ok(Self { terminal })
459 }
460
461 fn draw(&mut self, dashboard: &RatatuiDashboardState) -> Result<()> {
462 self.terminal.draw(|frame| dashboard.draw(frame))?;
463 Ok(())
464 }
465
466 fn leave(mut self) -> Result<()> {
467 self.terminal.clear()?;
468 execute!(self.terminal.backend_mut(), Show, LeaveAlternateScreen)?;
469 disable_raw_mode()?;
470 Ok(())
471 }
472}
473
474struct RatatuiStreamRenderer {
475 sender: mpsc::Sender<DashboardCommand>,
476 handle: Option<thread::JoinHandle<Result<()>>>,
477}
478
479impl RatatuiStreamRenderer {
480 fn new() -> Self {
481 let (sender, receiver) = mpsc::channel();
482 let handle = thread::spawn(move || dashboard_event_loop(receiver));
483 Self {
484 sender,
485 handle: Some(handle),
486 }
487 }
488
489 fn render(&mut self, update: StatusUpdate) -> Result<()> {
490 self.sender
491 .send(DashboardCommand::Update(Box::new(update)))
492 .map_err(|err| eyre!("Failed to send dashboard update: {err}"))?;
493 Ok(())
494 }
495
496 fn suspend(&mut self) -> Result<()> {
497 self.send_ack_command(DashboardCommand::Suspend)
498 }
499
500 fn shutdown(&mut self) -> Result<()> {
501 self.send_ack_command(DashboardCommand::Shutdown)?;
502 if let Some(handle) = self.handle.take() {
503 handle
504 .join()
505 .map_err(|_| eyre!("Ratatui dashboard thread panicked"))??;
506 }
507 Ok(())
508 }
509
510 fn send_ack_command(
511 &mut self,
512 build: impl FnOnce(mpsc::SyncSender<Result<()>>) -> DashboardCommand,
513 ) -> Result<()> {
514 let (ack_sender, ack_receiver) = mpsc::sync_channel(0);
515 self.sender
516 .send(build(ack_sender))
517 .map_err(|err| eyre!("Failed to send dashboard command: {err}"))?;
518 ack_receiver
519 .recv()
520 .map_err(|err| eyre!("Failed to receive dashboard acknowledgement: {err}"))??;
521 Ok(())
522 }
523}
524
525impl Drop for RatatuiStreamRenderer {
526 fn drop(&mut self) {
527 let _ = self.shutdown();
528 }
529}
530
531enum StreamRenderer {
532 Tabled(TabledStreamRenderer),
533 Ratatui(RatatuiStreamRenderer),
534}
535
536impl StreamRenderer {
537 fn new(renderer: RendererOption) -> Self {
538 match renderer {
539 RendererOption::Tabled => Self::Tabled(TabledStreamRenderer::new()),
540 RendererOption::Ratatui => Self::Ratatui(RatatuiStreamRenderer::new()),
541 }
542 }
543
544 fn render(&mut self, update: StatusUpdate, tabled_block: &str) -> Result<()> {
545 match self {
546 Self::Tabled(renderer) => renderer.render(tabled_block),
547 Self::Ratatui(renderer) => renderer.render(update),
548 }
549 }
550
551 fn clear(&mut self) -> Result<()> {
552 match self {
553 Self::Tabled(renderer) => renderer.clear(),
554 Self::Ratatui(renderer) => renderer.suspend(),
555 }
556 }
557
558 fn shutdown(&mut self) -> Result<()> {
559 match self {
560 Self::Tabled(renderer) => renderer.clear(),
561 Self::Ratatui(renderer) => renderer.shutdown(),
562 }
563 }
564}
565
566struct StreamingDisplayController {
567 renderer_kind: RendererOption,
568 stream_updates: bool,
569 stream_iterations: bool,
570 tabled_options: TabledRenderOptions,
571 renderer: Option<StreamRenderer>,
572}
573
574impl StreamingDisplayController {
575 fn new(
576 renderer_kind: RendererOption,
577 stream_updates: bool,
578 stream_iterations: bool,
579 tabled_options: TabledRenderOptions,
580 ) -> Self {
581 Self {
582 renderer_kind,
583 stream_updates,
584 stream_iterations,
585 tabled_options,
586 renderer: (stream_updates || stream_iterations)
587 .then(|| StreamRenderer::new(renderer_kind)),
588 }
589 }
590
591 fn handle_status_update(&mut self, status_update: StatusUpdate) -> Result<()> {
592 let kind = status_update.kind();
593 let renderer_kind = self.renderer_kind;
594 let tabled_options = self.tabled_options;
595
596 if let Some(renderer) = self.renderer.as_mut() {
597 match kind {
598 IntegrationStatusKind::Live => {
599 if self.stream_updates || status_update.is_initial_live_status() {
600 let status_block = Self::render_stream_block(
601 renderer_kind,
602 tabled_options,
603 &status_update,
604 );
605 renderer.render(status_update, &status_block)?;
606 }
607 }
608 IntegrationStatusKind::Iteration => {
609 if self.stream_iterations {
610 let status_block = Self::render_stream_block(
611 renderer_kind,
612 tabled_options,
613 &status_update,
614 );
615 renderer.render(status_update, &status_block)?;
616 } else {
617 renderer.clear()?;
618 self.emit_tabled_status(kind, &status_update)?;
619 }
620 }
621 IntegrationStatusKind::Final => {
622 if let Some(mut renderer) = self.renderer.take() {
623 renderer.shutdown()?;
624 }
625 self.emit_tabled_status(kind, &status_update)?;
626 }
627 }
628 } else if kind != IntegrationStatusKind::Live {
629 self.emit_tabled_status(kind, &status_update)?;
630 }
631
632 Ok(())
633 }
634
635 fn emit_tabled_status(
636 &self,
637 kind: IntegrationStatusKind,
638 status_update: &StatusUpdate,
639 ) -> Result<()> {
640 let status_block = render_status_update_tabled(status_update, &self.tabled_options);
641 emit_integration_status_via_tracing(kind, &status_block)
642 }
643
644 fn render_stream_block(
645 renderer_kind: RendererOption,
646 tabled_options: TabledRenderOptions,
647 status_update: &StatusUpdate,
648 ) -> String {
649 if matches!(renderer_kind, RendererOption::Tabled) {
650 render_status_update_tabled(status_update, &tabled_options)
651 } else {
652 String::new()
653 }
654 }
655}
656
657fn stream_terminal_width() -> usize {
658 terminal::size()
659 .map(|(width, _)| width.saturating_sub(1).max(1) as usize)
660 .unwrap_or(120)
661}
662
663fn prepare_stream_block(block: &str, max_width: usize) -> String {
664 block
665 .lines()
666 .map(|line| truncate_ansi_line(line, max_width))
667 .collect::<Vec<_>>()
668 .join("\r\n")
671}
672
673fn truncate_ansi_line(line: &str, max_width: usize) -> String {
674 if max_width == 0 {
675 return String::new();
676 }
677
678 let mut truncated = String::new();
679 let mut chars = line.chars().peekable();
680 let mut visible_width = 0usize;
681 let mut saw_escape = false;
682 let mut was_truncated = false;
683
684 while let Some(ch) = chars.next() {
685 if ch == '\u{1b}' && chars.peek() == Some(&'[') {
686 saw_escape = true;
687 truncated.push(ch);
688 truncated.push(chars.next().unwrap());
689 for code in chars.by_ref() {
690 truncated.push(code);
691 if ('@'..='~').contains(&code) {
692 break;
693 }
694 }
695 continue;
696 }
697
698 let char_width = UnicodeWidthChar::width(ch).unwrap_or(0);
699 if visible_width + char_width > max_width {
700 was_truncated = true;
701 break;
702 }
703
704 truncated.push(ch);
705 visible_width += char_width;
706 }
707
708 if was_truncated && saw_escape && !truncated.ends_with("\u{1b}[0m") {
709 truncated.push_str("\u{1b}[0m");
710 }
711
712 truncated
713}
714
715fn tabled_control_event_loop(
716 receiver: mpsc::Receiver<TabledControlCommand>,
717 startup_sender: mpsc::SyncSender<std::result::Result<(), String>>,
718) -> Result<()> {
719 if let Err(err) = enable_raw_mode() {
720 let _ = startup_sender.send(Err(err.to_string()));
721 return Err(err.into());
722 }
723 let _ = startup_sender.send(Ok(()));
724
725 let mut shutdown_ack = None;
726 let result = (|| -> Result<()> {
727 loop {
728 match receiver.recv_timeout(Duration::from_millis(50)) {
729 Ok(TabledControlCommand::Shutdown(ack_sender)) => {
730 shutdown_ack = Some(ack_sender);
731 break;
732 }
733 Err(mpsc::RecvTimeoutError::Timeout) => {}
734 Err(mpsc::RecvTimeoutError::Disconnected) => break,
735 }
736
737 while event::poll(Duration::from_millis(0))? {
738 if let Event::Key(key) = event::read()? {
739 if key.kind != KeyEventKind::Press {
740 continue;
741 }
742
743 match handle_tabled_key_event(key) {
744 TabledKeyAction::Ignored => {}
745 TabledKeyAction::InterruptIntegration => request_interrupt(),
746 TabledKeyAction::AbortCurrentIteration => request_iteration_abort(),
747 }
748 }
749 }
750 }
751
752 Ok(())
753 })();
754
755 let cleanup_result: Result<()> = disable_raw_mode().map_err(|err| eyre!(err));
756 if let Some(ack_sender) = shutdown_ack.take() {
757 let ack_result = match cleanup_result.as_ref() {
758 Ok(()) => Ok(()),
759 Err(err) => Err(eyre!(err.to_string())),
760 };
761 let _ = ack_sender.send(ack_result);
762 }
763 cleanup_result?;
764 result
765}
766
767fn dashboard_event_loop(receiver: mpsc::Receiver<DashboardCommand>) -> Result<()> {
768 let mut dashboard = RatatuiDashboardState::new();
769 let mut terminal = None;
770 let mut dirty = false;
771
772 loop {
773 match receiver.recv_timeout(Duration::from_millis(50)) {
774 Ok(DashboardCommand::Update(update)) => {
775 dashboard.update(*update);
776 if terminal.is_none() {
777 terminal = Some(RatatuiTerminal::enter()?);
778 }
779 dirty = true;
780 }
781 Ok(DashboardCommand::Suspend(ack_sender)) => {
782 let result = match terminal.take() {
783 Some(terminal) => terminal.leave(),
784 None => Ok(()),
785 };
786 let _ = ack_sender.send(result);
787 dirty = false;
788 }
789 Ok(DashboardCommand::Shutdown(ack_sender)) => {
790 let result = match terminal.take() {
791 Some(terminal) => terminal.leave(),
792 None => Ok(()),
793 };
794 let _ = ack_sender.send(result);
795 break;
796 }
797 Err(mpsc::RecvTimeoutError::Timeout) => {}
798 Err(mpsc::RecvTimeoutError::Disconnected) => break,
799 }
800
801 while terminal.is_some() && event::poll(Duration::from_millis(0))? {
802 let mut handled = false;
803 match event::read()? {
804 Event::Key(key) if key.kind == KeyEventKind::Press => {
805 handled = match handle_dashboard_key_event(&mut dashboard, key) {
806 DashboardKeyAction::Redraw => true,
807 DashboardKeyAction::InterruptIntegration => {
808 request_interrupt();
809 true
810 }
811 DashboardKeyAction::AbortCurrentIteration => {
812 request_iteration_abort();
813 true
814 }
815 DashboardKeyAction::Ignored => false,
816 };
817 }
818 Event::Resize(_, _) => handled = true,
819 _ => {}
820 }
821 dirty |= handled;
822 }
823
824 if dirty {
825 if let Some(terminal) = terminal.as_mut() {
826 terminal.draw(&dashboard)?;
827 dirty = false;
828 }
829 }
830 }
831
832 Ok(())
833}
834
835#[derive(Clone, Copy, Debug, Eq, PartialEq)]
836enum DashboardKeyAction {
837 Ignored,
838 Redraw,
839 InterruptIntegration,
840 AbortCurrentIteration,
841}
842
843#[derive(Clone, Copy, Debug, Eq, PartialEq)]
844enum TabledKeyAction {
845 Ignored,
846 InterruptIntegration,
847 AbortCurrentIteration,
848}
849
850fn handle_tabled_key_event(key: KeyEvent) -> TabledKeyAction {
851 if key.modifiers.contains(KeyModifiers::CONTROL)
852 && matches!(key.code, KeyCode::Char('c') | KeyCode::Char('C'))
853 {
854 return TabledKeyAction::InterruptIntegration;
855 }
856
857 if matches!(key.code, KeyCode::Char('x') | KeyCode::Char('X')) {
858 return TabledKeyAction::AbortCurrentIteration;
859 }
860
861 TabledKeyAction::Ignored
862}
863
864fn handle_dashboard_key_event(
865 dashboard: &mut RatatuiDashboardState,
866 key: KeyEvent,
867) -> DashboardKeyAction {
868 if key.modifiers.contains(KeyModifiers::CONTROL)
869 && matches!(key.code, KeyCode::Char('c') | KeyCode::Char('C'))
870 {
871 return DashboardKeyAction::InterruptIntegration;
872 }
873
874 if matches!(key.code, KeyCode::Char('x') | KeyCode::Char('X')) {
875 return DashboardKeyAction::AbortCurrentIteration;
876 }
877
878 match key.code {
879 KeyCode::Char('1') => dashboard.select_tab(0),
880 KeyCode::Char('2') => dashboard.select_tab(1),
881 KeyCode::Char('3') => dashboard.select_tab(2),
882 KeyCode::Left => dashboard.previous_tab(),
883 KeyCode::Right => dashboard.next_tab(),
884 KeyCode::Char('[') => dashboard.focus_previous_slot(),
885 KeyCode::Char(']') => dashboard.focus_next_slot(),
886 KeyCode::Char('i') | KeyCode::Char('I') => dashboard.toggle_statistics_scope(),
887 KeyCode::Down | KeyCode::Char('j') => dashboard.select_next_discrete_row(),
888 KeyCode::Up | KeyCode::Char('k') => dashboard.select_previous_discrete_row(),
889 KeyCode::Char('s') => dashboard.cycle_discrete_sort(),
890 KeyCode::Char('v') => dashboard.toggle_discrete_sort_direction(),
891 KeyCode::Char('r') => dashboard.toggle_relative_error(),
892 KeyCode::Char('c') => dashboard.toggle_chi_sq(),
893 KeyCode::Char('w') => dashboard.toggle_max_weight_impact(),
894 KeyCode::Char('p') | KeyCode::Char('P') => dashboard.toggle_chart_component(),
895 KeyCode::Char('g') | KeyCode::Char('G') => dashboard.toggle_chart_history_window(),
896 KeyCode::Char('+') | KeyCode::Char('=') => dashboard.widen_chart_history_window(),
897 KeyCode::Char('-') | KeyCode::Char('_') => dashboard.narrow_chart_history_window(),
898 KeyCode::Char(',') | KeyCode::Char('<') => dashboard.narrow_chart_y_sigma_span(),
899 KeyCode::Char('.') | KeyCode::Char('>') => dashboard.widen_chart_y_sigma_span(),
900 KeyCode::Char('0') => dashboard.reset_chart_y_sigma_span(),
901 KeyCode::Esc | KeyCode::Char('?') => dashboard.toggle_help(),
902 _ => return DashboardKeyAction::Ignored,
903 }
904 DashboardKeyAction::Redraw
905}
906
907#[derive(Debug, Clone)]
908struct ResolvedIntegrandSlot {
909 process_id: usize,
910 slot_meta: SlotMeta,
911}
912
913fn slot_settings_path(workspace: &std::path::Path, slot_meta: &SlotMeta) -> PathBuf {
914 slot_workspace_path(workspace, slot_meta).join("settings.toml")
915}
916
917fn read_existing_workspace_state(
918 workspace_path: &std::path::Path,
919) -> Result<(IntegrationWorkspaceManifest, IntegrationState)> {
920 let manifest = IntegrationWorkspaceManifest::from_file(
921 workspace_manifest_path(workspace_path),
922 "integration manifest",
923 )?;
924 let state_bytes = fs::read(workspace_state_path(workspace_path)).map_err(|err| {
925 eyre!(
926 "Could not read integration workspace state from {}: {err}",
927 workspace_path.display()
928 )
929 })?;
930 let integration_state = bincode::decode_from_slice::<IntegrationState, _>(
931 &state_bytes,
932 bincode::config::standard(),
933 )
934 .map_err(|err| eyre!("Could not deserialize integration state: {err}"))?
935 .0;
936 Ok((manifest, integration_state))
937}
938
939fn grids_have_compatible_sample_shape(lhs: &Grid<F<f64>>, rhs: &Grid<F<f64>>) -> bool {
940 match (lhs, rhs) {
941 (Grid::Continuous(lhs), Grid::Continuous(rhs)) => {
942 lhs.continuous_dimensions.len() == rhs.continuous_dimensions.len()
943 }
944 (Grid::Discrete(lhs), Grid::Discrete(rhs)) => {
945 lhs.bins.len() == rhs.bins.len()
946 && lhs
947 .bins
948 .iter()
949 .zip(rhs.bins.iter())
950 .all(
951 |(lhs_bin, rhs_bin)| match (&lhs_bin.sub_grid, &rhs_bin.sub_grid) {
952 (Some(lhs_sub_grid), Some(rhs_sub_grid)) => {
953 grids_have_compatible_sample_shape(lhs_sub_grid, rhs_sub_grid)
954 }
955 (None, None) => true,
956 _ => false,
957 },
958 )
959 }
960 (
961 Grid::Uniform(lhs_discrete, lhs_continuous),
962 Grid::Uniform(rhs_discrete, rhs_continuous),
963 ) => {
964 lhs_discrete == rhs_discrete
965 && lhs_continuous.continuous_dimensions.len()
966 == rhs_continuous.continuous_dimensions.len()
967 }
968 _ => false,
969 }
970}
971
972impl Integrate {
973 pub fn from_slots<I, S>(slots: I) -> Self
974 where
975 I: IntoIterator<Item = (ProcessRef, S)>,
976 S: Into<String>,
977 {
978 let mut integrate = Self::default();
979 for (process, integrand_name) in slots {
980 integrate.process.push(process);
981 integrate.integrand_name.push(integrand_name.into());
982 }
983 integrate
984 }
985
986 pub fn with_single_target(mut self, target: Complex<F<f64>>) -> Self {
987 self.target = vec![target.re.to_string(), target.im.to_string()];
988 self
989 }
990
991 pub fn with_keyed_targets<I, S>(mut self, targets: I) -> Self
992 where
993 I: IntoIterator<Item = (S, Complex<F<f64>>)>,
994 S: Into<String>,
995 {
996 self.target = targets
997 .into_iter()
998 .map(|(slot_key, target)| format!("{}={},{}", slot_key.into(), target.re, target.im))
999 .collect();
1000 self
1001 }
1002
1003 fn default_workspace_path(&self, global_cli_settings: &CLISettings) -> PathBuf {
1004 if global_cli_settings.session.read_only_state {
1005 let workspace_name = global_cli_settings
1006 .state
1007 .name
1008 .as_deref()
1009 .map(str::trim)
1010 .filter(|name| !name.is_empty())
1011 .map(|name| format!("integration_workspace_{name}"))
1012 .unwrap_or_else(|| "integration_workspace".to_string());
1013 PathBuf::from(".").join(workspace_name)
1014 } else {
1015 global_cli_settings
1016 .state
1017 .folder
1018 .join("integration_workspace")
1019 }
1020 }
1021
1022 fn selected_training_phase_display(
1023 integrated_phase: IntegratedPhase,
1024 ) -> IntegrationStatusPhaseDisplay {
1025 match integrated_phase {
1026 IntegratedPhase::Real | IntegratedPhase::Both => IntegrationStatusPhaseDisplay::Real,
1027 IntegratedPhase::Imag => IntegrationStatusPhaseDisplay::Imag,
1028 }
1029 }
1030
1031 fn resolve_selected_slots(&self, state: &State) -> Result<Vec<ResolvedIntegrandSlot>> {
1032 let selections = if self.process.is_empty() && self.integrand_name.is_empty() {
1033 vec![state.find_integrand_ref(None, None)?]
1034 } else if self.process.len() == 1 && self.integrand_name.is_empty() {
1035 vec![state.find_integrand_ref(self.process.first(), None)?]
1036 } else if self.process.is_empty() && self.integrand_name.len() == 1 {
1037 vec![state.find_integrand_ref(None, self.integrand_name.first())?]
1038 } else if self.process.len() == self.integrand_name.len() {
1039 self.process
1040 .iter()
1041 .zip(self.integrand_name.iter())
1042 .map(|(process, integrand_name)| {
1043 state.find_integrand_ref(Some(process), Some(integrand_name))
1044 })
1045 .collect::<Result<Vec<_>>>()?
1046 } else {
1047 return Err(eyre!(
1048 "The integrate command expects repeated `-p/-i` pairs. Received {} process selector(s) and {} integrand selector(s).",
1049 self.process.len(),
1050 self.integrand_name.len()
1051 ));
1052 };
1053
1054 let mut seen = HashSet::new();
1055 let mut resolved = Vec::with_capacity(selections.len());
1056 for (process_id, integrand_name) in selections {
1057 let process_name = state.process_list.processes[process_id]
1058 .definition
1059 .folder_name
1060 .clone();
1061 let slot_meta = SlotMeta {
1062 process_name,
1063 integrand_name,
1064 };
1065 if !seen.insert(slot_meta.key()) {
1066 return Err(eyre!(
1067 "The same integrand '{}' was selected more than once",
1068 slot_meta.key()
1069 ));
1070 }
1071 resolved.push(ResolvedIntegrandSlot {
1072 process_id,
1073 slot_meta,
1074 });
1075 }
1076
1077 Ok(resolved)
1078 }
1079
1080 fn resolve_manifest_slots(
1081 &self,
1082 state: &State,
1083 manifest: &IntegrationWorkspaceManifest,
1084 ) -> Result<Vec<ResolvedIntegrandSlot>> {
1085 manifest
1086 .slots
1087 .iter()
1088 .map(|slot_meta| {
1089 let process_ref = ProcessRef::Name(slot_meta.process_name.clone());
1090 let integrand_name = slot_meta.integrand_name.clone();
1091 let (process_id, resolved_integrand_name) =
1092 state.find_integrand_ref(Some(&process_ref), Some(&integrand_name))?;
1093 Ok(ResolvedIntegrandSlot {
1094 process_id,
1095 slot_meta: SlotMeta {
1096 process_name: slot_meta.process_name.clone(),
1097 integrand_name: resolved_integrand_name,
1098 },
1099 })
1100 })
1101 .collect()
1102 }
1103
1104 fn build_render_options(
1105 &self,
1106 slot_settings: &[RuntimeSettings],
1107 show_statistics: bool,
1108 ) -> IntegrationStatusViewOptions {
1109 let settings = &slot_settings[0];
1110 IntegrationStatusViewOptions {
1111 phase_display: self
1112 .show_phase
1113 .resolve(settings.integrator.integrated_phase),
1114 training_phase_display: Self::selected_training_phase_display(
1115 settings.integrator.integrated_phase,
1116 ),
1117 training_slot: 0,
1118 slot_training_phase_displays: slot_settings
1119 .iter()
1120 .map(|slot_settings| {
1121 Self::selected_training_phase_display(slot_settings.integrator.integrated_phase)
1122 })
1123 .collect(),
1124 per_slot_training_phase: self.uncorrelated,
1125 target_relative_accuracy: settings.integrator.target_relative_accuracy,
1126 target_absolute_accuracy: settings.integrator.target_absolute_accuracy,
1127 show_statistics,
1128 show_max_weight_details: self.show_max_weight_info,
1129 show_top_discrete_grid: self.show_top_discrete_grid,
1130 show_discrete_contributions_sum: self.show_discrete_contributions_sum,
1131 contribution_sort: self.sort_contributions.into(),
1132 show_max_weight_info_for_discrete_bins: self.show_max_weight_info_for_discrete_bins,
1133 }
1134 }
1135
1136 fn build_tabled_render_options(&self) -> TabledRenderOptions {
1137 TabledRenderOptions {
1138 max_table_width: self.max_table_width,
1139 show_statistics: !self.no_show_integration_statistics,
1140 show_max_weight_details: self.show_max_weight_info,
1141 show_top_discrete_grid: self.show_top_discrete_grid,
1142 show_discrete_contributions_sum: self.show_discrete_contributions_sum,
1143 show_max_weight_info_for_discrete_bins: self.show_max_weight_info_for_discrete_bins,
1144 }
1145 }
1146
1147 fn workspace_snapshot_control(&self) -> WorkspaceSnapshotControl {
1148 WorkspaceSnapshotControl {
1149 write_iteration_archives: self.write_results_for_each_iteration,
1150 }
1151 }
1152
1153 fn sampling_correlation_mode(&self) -> SamplingCorrelationMode {
1154 if self.uncorrelated {
1155 SamplingCorrelationMode::Uncorrelated
1156 } else {
1157 SamplingCorrelationMode::Correlated
1158 }
1159 }
1160
1161 fn build_batching_settings(
1162 &self,
1163 emit_live_status_updates: bool,
1164 emit_initial_status_update: bool,
1165 ) -> IterationBatchingSettings {
1166 IterationBatchingSettings {
1167 batch_size: self.batch_size,
1168 batch_timing_seconds: self.batch_timing,
1169 min_time_between_status_updates_seconds: self.min_time_between_status_updates,
1170 emit_live_status_updates,
1171 emit_initial_status_update,
1172 }
1173 }
1174
1175 fn parse_target_components(first: &str, second: Option<&str>) -> Result<Complex<F<f64>>> {
1176 if let Some(second) = second {
1177 return Ok(Complex::new(F(first.parse()?), F(second.parse()?)));
1178 }
1179
1180 let (re, im) = first
1181 .split_once(',')
1182 .ok_or_else(|| eyre!("Targets must be provided as `re im` or `re,im`"))?;
1183 Ok(Complex::new(F(re.parse()?), F(im.parse()?)))
1184 }
1185
1186 fn resolve_targets(
1187 &self,
1188 selected_slots: &[ResolvedIntegrandSlot],
1189 ) -> Result<Vec<Option<Complex<F<f64>>>>> {
1190 let mut resolved = vec![None; selected_slots.len()];
1191 if self.target.is_empty() {
1192 return Ok(resolved);
1193 }
1194
1195 if self.target.iter().all(|target| !target.contains('=')) {
1196 let target = match self.target.as_slice() {
1197 [single] => Self::parse_target_components(single, None)?,
1198 [re, im] => Self::parse_target_components(re, Some(im))?,
1199 _ => {
1200 return Err(eyre!(
1201 "A shared target must be given as `--target re im` or `--target re,im`"
1202 ));
1203 }
1204 };
1205 resolved.fill(Some(target));
1206 return Ok(resolved);
1207 }
1208
1209 for target in &self.target {
1210 let (slot_key, values) = target.split_once('=').ok_or_else(|| {
1211 eyre!(
1212 "Multi-integrand targets must use the keyed form `--target process@integrand=re,im`"
1213 )
1214 })?;
1215 let parsed = Self::parse_target_components(values, None)?;
1216 let slot_index = selected_slots
1217 .iter()
1218 .position(|slot| slot.slot_meta.key() == slot_key)
1219 .ok_or_else(|| eyre!("Unknown target slot key '{}'", slot_key))?;
1220 if resolved[slot_index].is_some() {
1221 return Err(eyre!(
1222 "The target for '{}' was specified more than once",
1223 slot_key
1224 ));
1225 }
1226 resolved[slot_index] = Some(parsed);
1227 }
1228
1229 Ok(resolved)
1230 }
1231
1232 fn load_or_prepare_workspace_state(
1233 &self,
1234 state: &mut State,
1235 selected_slots: &[ResolvedIntegrandSlot],
1236 current_effective_model_parameters: &[SerializableInputParamCard<F<f64>>],
1237 current_integrand_fingerprints: &[String],
1238 workspace_path: &std::path::Path,
1239 targets: &mut Vec<Option<Complex<F<f64>>>>,
1240 ) -> Result<Option<IntegrationState>> {
1241 let path_to_state = workspace_state_path(workspace_path);
1242 let manifest_path = workspace_manifest_path(workspace_path);
1243 match fs::read(&path_to_state) {
1244 Ok(state_bytes) => {
1245 let manifest: IntegrationWorkspaceManifest =
1246 IntegrationWorkspaceManifest::from_file(
1247 &manifest_path,
1248 "integration manifest",
1249 )?;
1250 if manifest.sampling_correlation_mode != self.sampling_correlation_mode() {
1251 return Err(eyre!(
1252 "Workspace integration sampling mode does not match the requested mode; use --restart to switch between correlated and uncorrelated integration"
1253 ));
1254 }
1255 let expected_slots = selected_slots
1256 .iter()
1257 .map(|slot| slot.slot_meta.clone())
1258 .collect_vec();
1259 if manifest.slots != expected_slots {
1260 return Err(eyre!(
1261 "Workspace integration slots do not match the currently selected integrands"
1262 ));
1263 }
1264 if manifest.targets != *targets {
1265 warn!("targets have changed with respect to workspace, reverting changes");
1266 *targets = manifest.targets.clone();
1267 }
1268 if manifest.integrand_fingerprints.len() != selected_slots.len() {
1269 return Err(eyre!(
1270 "Workspace integrand fingerprint metadata is inconsistent with the selected slots"
1271 ));
1272 }
1273 if manifest.effective_model_parameters.len() != selected_slots.len() {
1274 return Err(eyre!(
1275 "Workspace effective model parameter metadata is inconsistent with the selected slots"
1276 ));
1277 }
1278 if current_effective_model_parameters.len() != selected_slots.len() {
1279 return Err(eyre!(
1280 "Current effective model parameter metadata is inconsistent with the selected slots"
1281 ));
1282 }
1283 let mismatched_slots = selected_slots
1284 .iter()
1285 .zip(manifest.effective_model_parameters.iter())
1286 .zip(current_effective_model_parameters.iter())
1287 .filter(|((_, saved_card), current_card)| saved_card != current_card)
1288 .map(|((slot, _), _)| slot.slot_meta.key())
1289 .collect_vec();
1290 if !mismatched_slots.is_empty() {
1291 return Err(eyre!(
1292 "Workspace effective model parameters do not match the current state for {}. Resume requires an exact match; use --restart or restore the shared/per-integrand model parameters.",
1293 mismatched_slots.join(", ")
1294 ));
1295 }
1296 let workspace_settings = selected_slots
1297 .iter()
1298 .map(|slot| {
1299 RuntimeSettings::from_file(
1300 slot_settings_path(workspace_path, &slot.slot_meta),
1301 &format!("workspace settings for {}", slot.slot_meta.key()),
1302 )
1303 })
1304 .collect::<Result<Vec<_>>>()?;
1305 let comparison_integrand_fingerprints = izip!(
1306 selected_slots.iter(),
1307 workspace_settings.iter(),
1308 current_integrand_fingerprints.iter(),
1309 )
1310 .map(|(slot, settings, current_fingerprint)| {
1311 if settings.sampling.selected_graph_names().is_empty() {
1312 return Ok(current_fingerprint.clone());
1313 }
1314
1315 let mut integrand = state
1319 .process_list
1320 .get_integrand(slot.process_id, &slot.slot_meta.integrand_name)?
1321 .require_generated()?
1322 .clone();
1323 let preserved_model_overrides = integrand.get_settings().model.clone();
1324 *integrand.get_mut_settings() = settings.clone();
1325 integrand.get_mut_settings().model = preserved_model_overrides;
1326 let model = state.resolve_model_for_integrand(
1327 slot.process_id,
1328 &slot.slot_meta.integrand_name,
1329 )?;
1330 integrand.warm_up(&model)?;
1331 integrand.resume_fingerprint()
1332 })
1333 .collect::<Result<Vec<_>>>()?;
1334 let mismatched_fingerprint_slots = selected_slots
1335 .iter()
1336 .zip(manifest.integrand_fingerprints.iter())
1337 .zip(comparison_integrand_fingerprints.iter())
1338 .filter(|&((_, saved), current)| saved != current)
1339 .map(|((slot, _), _)| slot.slot_meta.key())
1340 .collect_vec();
1341 if !mismatched_fingerprint_slots.is_empty() {
1342 return Err(eyre!(
1343 "Workspace integrand fingerprints do not match the current generated integrands for {}. Resume requires the exact same generated integrands; use --restart or restore the previous generation.",
1344 mismatched_fingerprint_slots.join(", ")
1345 ));
1346 }
1347
1348 info!(
1349 "{}",
1350 "Found integration state, result of previous integration:".yellow()
1351 );
1352 info!("");
1353
1354 let integration_state: IntegrationState =
1355 bincode::decode_from_slice::<IntegrationState, _>(
1356 &state_bytes,
1357 bincode::config::standard(),
1358 )
1359 .expect("Could not deserialize state")
1360 .0;
1361
1362 for ((slot, target), workspace_settings) in selected_slots
1363 .iter()
1364 .zip(targets.iter())
1365 .zip(workspace_settings)
1366 {
1367 let gloop_integrand = state
1368 .process_list
1369 .get_integrand_mut(slot.process_id, &slot.slot_meta.integrand_name)?;
1370 let current_settings = gloop_integrand.get_mut_settings();
1371 if *current_settings != workspace_settings {
1372 warn!(
1373 "settings for {} have changed with respect to workspace, reverting non-model changes",
1374 slot.slot_meta.key()
1375 );
1376 let preserved_model_overrides = current_settings.model.clone();
1377 *current_settings = workspace_settings;
1378 current_settings.model = preserved_model_overrides;
1379 }
1380
1381 let label = format!("itg {}", slot.slot_meta.key());
1382 let saved_slot = integration_state
1383 .all_integrals
1384 .get(
1385 selected_slots
1386 .iter()
1387 .position(|candidate| candidate.slot_meta == slot.slot_meta)
1388 .expect("selected slot must exist"),
1389 )
1390 .expect("saved slot must exist");
1391 print_integral_result(
1392 &saved_slot.re,
1393 &label,
1394 integration_state.iter,
1395 "re",
1396 target.as_ref().map(|value| value.re),
1397 );
1398 print_integral_result(
1399 &saved_slot.im,
1400 &label,
1401 integration_state.iter,
1402 "im",
1403 target.as_ref().map(|value| value.im),
1404 );
1405 }
1406 info!("");
1407 warn!(
1408 "Any changes to the settings will be ignored, integrate with the {} option for changes to take effect",
1409 "--restart".blue()
1410 );
1411 info!("{}", "Resuming integration".yellow());
1412
1413 Ok(Some(integration_state))
1414 }
1415 Err(_) => {
1416 info!("No integration state found, starting new integration");
1417 Ok(None)
1418 }
1419 }
1420 }
1421
1422 fn warm_and_clone_integrands(
1423 &self,
1424 state: &mut State,
1425 selected_slots: &[ResolvedIntegrandSlot],
1426 slot_models: &[Model],
1427 ) -> Result<Vec<gammalooprs::integrands::process::ProcessIntegrand>> {
1428 info!(
1429 "Gammaloop now integrates {}",
1430 selected_slots
1431 .iter()
1432 .map(|slot| slot.slot_meta.key().green().bold().to_string())
1433 .join(", ")
1434 );
1435
1436 selected_slots
1437 .iter()
1438 .zip(slot_models.iter())
1439 .map(|slot| {
1440 let (slot, model) = slot;
1441 let gloop_integrand = state
1442 .process_list
1443 .get_integrand_mut(slot.process_id, &slot.slot_meta.integrand_name)?;
1444 let selected_graph_names = gloop_integrand
1445 .get_settings()
1446 .sampling
1447 .selected_graph_names()
1448 .to_vec();
1449 if selected_graph_names.is_empty() {
1450 gloop_integrand.warm_up(model)?;
1451 return Ok(gloop_integrand.clone());
1452 }
1453
1454 gloop_integrand.warm_up(model)?;
1455 let full_group_count = gloop_integrand.graph_group_count();
1456 let mut integration_view =
1457 gloop_integrand.clone_with_selected_graph_groups(&selected_graph_names)?;
1458 info!(
1459 "Runtime graph-group subset for {}: {} of {} groups [{}]",
1460 slot.slot_meta.key().green().bold(),
1461 integration_view.graph_group_count(),
1462 full_group_count,
1463 integration_view.graph_group_master_names().join(", "),
1464 );
1465 integration_view.warm_up(model)?;
1466 Ok(integration_view)
1467 })
1468 .collect()
1469 }
1470
1471 fn restore_workspace_observables(
1472 &self,
1473 workspace_path: &std::path::Path,
1474 selected_slots: &[ResolvedIntegrandSlot],
1475 integration_state: Option<&IntegrationState>,
1476 slot_integrands: &mut [gammalooprs::integrands::process::ProcessIntegrand],
1477 ) -> Result<()> {
1478 let Some(integration_state) = integration_state else {
1479 return Ok(());
1480 };
1481 if integration_state.iter == 0 {
1482 return Ok(());
1483 }
1484
1485 for ((slot, integrand), slot_meta) in selected_slots
1486 .iter()
1487 .zip(slot_integrands.iter_mut())
1488 .zip(integration_state.slot_metas.iter())
1489 {
1490 debug_assert_eq!(slot.slot_meta, *slot_meta);
1491 if integrand.observable_snapshot_bundle().is_none() {
1492 continue;
1493 }
1494
1495 let snapshot_path =
1496 latest_observable_resume_state_path(workspace_path, &slot.slot_meta);
1497 let snapshot =
1498 ObservableSnapshotBundle::from_json_file(&snapshot_path).map_err(|err| {
1499 eyre!(
1500 "Could not restore observable checkpoint for {} from {}: {err}",
1501 slot.slot_meta.key(),
1502 snapshot_path.display()
1503 )
1504 })?;
1505 integrand.restore_observable_snapshot_bundle(&snapshot)?;
1506 }
1507
1508 Ok(())
1509 }
1510
1511 fn resolve_slot_models(
1512 &self,
1513 state: &State,
1514 selected_slots: &[ResolvedIntegrandSlot],
1515 ) -> Result<Vec<Model>> {
1516 selected_slots
1517 .iter()
1518 .map(|slot| {
1519 state.resolve_model_for_integrand(slot.process_id, &slot.slot_meta.integrand_name)
1520 })
1521 .collect()
1522 }
1523
1524 fn resolve_effective_model_parameters(
1525 &self,
1526 state: &State,
1527 selected_slots: &[ResolvedIntegrandSlot],
1528 ) -> Result<Vec<SerializableInputParamCard<F<f64>>>> {
1529 selected_slots
1530 .iter()
1531 .map(|slot| {
1532 state.resolve_effective_model_parameter_card_for_integrand(
1533 slot.process_id,
1534 &slot.slot_meta.integrand_name,
1535 )
1536 })
1537 .collect()
1538 }
1539
1540 fn resolve_integrand_fingerprints(
1541 &self,
1542 state: &mut State,
1543 selected_slots: &[ResolvedIntegrandSlot],
1544 ) -> Result<Vec<String>> {
1545 selected_slots
1546 .iter()
1547 .map(|slot| {
1548 state
1549 .process_list
1550 .get_integrand_mut(slot.process_id, &slot.slot_meta.integrand_name)?
1551 .resume_fingerprint()
1552 })
1553 .collect()
1554 }
1555
1556 fn validate_slot_compatibility(
1557 &self,
1558 selected_slots: &[ResolvedIntegrandSlot],
1559 slot_integrands: &[gammalooprs::integrands::process::ProcessIntegrand],
1560 ) -> Result<()> {
1561 if self.uncorrelated || slot_integrands.len() <= 1 {
1562 return Ok(());
1563 }
1564
1565 let reference = &slot_integrands[0];
1566 let reference_grid = reference.create_grid();
1567 let reference_sampling = &reference.get_settings().sampling;
1568
1569 for (slot, integrand) in selected_slots.iter().zip(slot_integrands.iter()).skip(1) {
1570 let sampling = &integrand.get_settings().sampling;
1571 let sampling_matches = match (reference_sampling, sampling) {
1572 (
1573 SamplingSettings::DiscreteGraphs(reference),
1574 SamplingSettings::DiscreteGraphs(candidate),
1575 ) if !reference.graph_names.is_empty() && !candidate.graph_names.is_empty() => {
1576 reference.sample_orientations == candidate.sample_orientations
1577 && reference.sampling_type == candidate.sampling_type
1578 }
1579 _ => sampling == reference_sampling,
1580 };
1581 if !sampling_matches {
1582 return Err(eyre!(
1583 "Integrand '{}' does not share the same sampling settings as the leading integrand '{}'",
1584 slot.slot_meta.key(),
1585 selected_slots[0].slot_meta.key()
1586 ));
1587 }
1588 if !grids_have_compatible_sample_shape(&reference_grid, &integrand.create_grid()) {
1589 return Err(eyre!(
1590 "Integrand '{}' does not have a sample shape compatible with the leading integrand '{}'",
1591 slot.slot_meta.key(),
1592 selected_slots[0].slot_meta.key()
1593 ));
1594 }
1595 }
1596
1597 Ok(())
1598 }
1599
1600 fn write_workspace_manifest_and_settings(
1601 &self,
1602 slot_integrands: &[gammalooprs::integrands::process::ProcessIntegrand],
1603 selected_slots: &[ResolvedIntegrandSlot],
1604 targets: &[Option<Complex<F<f64>>>],
1605 effective_model_parameters: &[SerializableInputParamCard<F<f64>>],
1606 integrand_fingerprints: &[String],
1607 workspace_path: &std::path::Path,
1608 ) -> Result<()> {
1609 if integrand_fingerprints.len() != selected_slots.len() {
1610 return Err(eyre!(
1611 "Resolved {} generated-integrand fingerprints for {} integration slots",
1612 integrand_fingerprints.len(),
1613 selected_slots.len(),
1614 ));
1615 }
1616 let manifest = IntegrationWorkspaceManifest {
1617 slots: selected_slots
1618 .iter()
1619 .map(|slot| slot.slot_meta.clone())
1620 .collect(),
1621 targets: targets.to_vec(),
1622 effective_model_parameters: effective_model_parameters.to_vec(),
1623 integrand_fingerprints: integrand_fingerprints.to_vec(),
1624 training_slot: 0,
1625 integrator_settings_slot: 0,
1626 sampling_correlation_mode: self.sampling_correlation_mode(),
1627 };
1628 manifest.to_file(workspace_manifest_path(workspace_path), true)?;
1629
1630 for (slot, integrand) in selected_slots.iter().zip(slot_integrands.iter()) {
1631 let slot_workspace = slot_workspace_path(workspace_path, &slot.slot_meta);
1632 fs::create_dir_all(&slot_workspace)?;
1633 integrand
1634 .get_settings()
1635 .to_file(slot_settings_path(workspace_path, &slot.slot_meta), true)?;
1636 }
1637
1638 Ok(())
1639 }
1640
1641 fn collect_workspace_observable_snapshots(
1642 &self,
1643 workspace_path: &std::path::Path,
1644 slot_metas: impl IntoIterator<Item = SlotMeta>,
1645 ) -> Result<BTreeMap<String, ObservableSnapshotBundle>> {
1646 let mut observables = BTreeMap::new();
1647 for slot_meta in slot_metas {
1648 let snapshot_path = latest_observable_resume_state_path(workspace_path, &slot_meta);
1649 if !snapshot_path.exists() {
1650 continue;
1651 }
1652 let snapshot =
1653 ObservableSnapshotBundle::from_json_file(&snapshot_path).map_err(|err| {
1654 eyre!(
1655 "Could not load observable snapshot for {} from {}: {err}",
1656 slot_meta.key(),
1657 snapshot_path.display()
1658 )
1659 })?;
1660 observables.insert(slot_meta.key(), snapshot);
1661 }
1662 Ok(observables)
1663 }
1664
1665 pub fn run(
1666 &self,
1667 state: &mut State,
1668 global_cli_settings: &CLISettings,
1669 ) -> Result<IntegrationOutput> {
1670 if self.show_summary_only && self.restart {
1671 return Err(eyre!(
1672 "The integrate options `--show-summary-only` and `--restart` cannot be used together"
1673 ));
1674 }
1675 if self.max_table_width == 0 {
1676 return Err(eyre!(
1677 "The integrate option `--max-table-width` must be greater than zero"
1678 ));
1679 }
1680 if self.batch_size == Some(0) {
1681 return Err(eyre!(
1682 "The integrate option `--batch-size` must be greater than zero"
1683 ));
1684 }
1685 if self.batch_timing < 0.0 {
1686 return Err(eyre!(
1687 "The integrate option `--batch-timing` must be greater than or equal to zero"
1688 ));
1689 }
1690 if self.min_time_between_status_updates < 0.0 {
1691 return Err(eyre!(
1692 "The integrate option `--min-time-between-status-updates` must be greater than or equal to zero"
1693 ));
1694 }
1695
1696 let default_workspace_path = self.default_workspace_path(global_cli_settings);
1697 let workspace_path = if let Some(p) = self.workspace_path.clone() {
1698 p
1699 } else {
1700 default_workspace_path.clone()
1701 };
1702
1703 if self.show_summary_only {
1704 let (manifest, integration_state) = read_existing_workspace_state(&workspace_path)?;
1705 let selected_slots = if self.process.is_empty() && self.integrand_name.is_empty() {
1706 self.resolve_manifest_slots(state, &manifest)?
1707 } else {
1708 self.resolve_selected_slots(state)?
1709 };
1710 let expected_slots = selected_slots
1711 .iter()
1712 .map(|slot| slot.slot_meta.clone())
1713 .collect_vec();
1714 if manifest.slots != expected_slots {
1715 return Err(eyre!(
1716 "Workspace integration slots do not match the currently selected integrands"
1717 ));
1718 }
1719
1720 let mut targets = if self.target.is_empty() {
1721 manifest.targets.clone()
1722 } else {
1723 self.resolve_targets(&selected_slots)?
1724 };
1725 if targets != manifest.targets {
1726 warn!("targets have changed with respect to workspace, reverting changes");
1727 targets = manifest.targets.clone();
1728 }
1729
1730 if integration_state.num_points == 0 {
1731 return Err(eyre!(
1732 "No completed integration iteration is available in {}",
1733 workspace_path.display()
1734 ));
1735 }
1736
1737 let slot0_meta = manifest.slots.first().ok_or_else(|| {
1738 eyre!("Integration workspace does not contain any integrand slots")
1739 })?;
1740 let slot0_settings: RuntimeSettings = RuntimeSettings::from_file(
1741 slot_settings_path(&workspace_path, slot0_meta),
1742 &format!("workspace settings for {}", slot0_meta.key()),
1743 )?;
1744 let view_options =
1745 self.build_render_options(std::slice::from_ref(&slot0_settings), true);
1746 let tabled_options = self.build_tabled_render_options();
1747 emit_integration_status_via_tracing(
1748 IntegrationStatusKind::Final,
1749 render_saved_integration_summary(
1750 &integration_state,
1751 &targets,
1752 &view_options,
1753 &tabled_options,
1754 ),
1755 )?;
1756
1757 return Ok(IntegrationOutput {
1758 result: build_integration_result(&integration_state, &targets),
1759 observables: self.collect_workspace_observable_snapshots(
1760 &workspace_path,
1761 selected_slots.iter().map(|slot| slot.slot_meta.clone()),
1762 )?,
1763 workspace_path,
1764 });
1765 }
1766
1767 global_cli_settings.ensure_write_target_outside_active_state(
1768 &workspace_path,
1769 "create or update the integration workspace",
1770 )?;
1771
1772 let selected_slots = self.resolve_selected_slots(state)?;
1773
1774 if self.restart && workspace_path.exists() {
1775 fs::remove_dir_all(&workspace_path)?;
1776 }
1777
1778 if !workspace_path.exists() {
1779 fs::create_dir_all(&workspace_path)?;
1780 info!(
1781 "Created workspace directory at {}",
1782 workspace_path.display()
1783 );
1784 }
1785 let mut targets = self.resolve_targets(&selected_slots)?;
1786 let slot_models = self.resolve_slot_models(state, &selected_slots)?;
1787 let effective_model_parameters =
1788 self.resolve_effective_model_parameters(state, &selected_slots)?;
1789 let current_integrand_fingerprints =
1790 self.resolve_integrand_fingerprints(state, &selected_slots)?;
1791 let integration_state = self.load_or_prepare_workspace_state(
1792 state,
1793 &selected_slots,
1794 &effective_model_parameters,
1795 ¤t_integrand_fingerprints,
1796 &workspace_path,
1797 &mut targets,
1798 )?;
1799 let mut slot_integrands =
1800 self.warm_and_clone_integrands(state, &selected_slots, &slot_models)?;
1801 let workspace_integrand_fingerprints =
1803 self.resolve_integrand_fingerprints(state, &selected_slots)?;
1804 self.restore_workspace_observables(
1805 &workspace_path,
1806 &selected_slots,
1807 integration_state.as_ref(),
1808 &mut slot_integrands,
1809 )?;
1810 self.validate_slot_compatibility(&selected_slots, &slot_integrands)?;
1811 let slot_settings = slot_integrands
1812 .iter()
1813 .map(|integrand| integrand.get_settings().clone())
1814 .collect_vec();
1815 let view_options =
1816 self.build_render_options(&slot_settings, !self.no_show_integration_statistics);
1817 let tabled_options = self.build_tabled_render_options();
1818 self.write_workspace_manifest_and_settings(
1819 &slot_integrands,
1820 &selected_slots,
1821 &targets,
1822 &effective_model_parameters,
1823 &workspace_integrand_fingerprints,
1824 &workspace_path,
1825 )?;
1826
1827 let n_cores = self
1828 .n_cores
1829 .unwrap_or(global_cli_settings.global.n_cores.integrate);
1830
1831 let stderr_is_tty = io::stderr().is_terminal();
1832 let stream_updates = !self.no_stream_updates && stderr_is_tty;
1833 let stream_iterations = !self.no_stream_iterations && stderr_is_tty;
1834 if !stderr_is_tty && (!self.no_stream_updates || !self.no_stream_iterations) {
1835 info!(
1836 "Streaming integration updates disabled because stderr is not a TTY; live updates will be skipped and iteration summaries will be logged."
1837 );
1838 }
1839 let stream_renderer_kind = self.renderer;
1840 let mut stream_controller = StreamingDisplayController::new(
1841 stream_renderer_kind,
1842 stream_updates,
1843 stream_iterations,
1844 tabled_options,
1845 );
1846 let slots = izip!(
1847 selected_slots.iter().map(|slot| slot.slot_meta.clone()),
1848 slot_settings,
1849 slot_models,
1850 slot_integrands,
1851 targets
1852 )
1853 .map(|(meta, settings, model, integrand, target)| {
1854 IntegrationSlot::new(
1855 meta,
1856 settings,
1857 model,
1858 Integrand::ProcessIntegrand(Box::new(integrand)),
1859 target,
1860 )
1861 })
1862 .collect();
1863
1864 let result = havana_integrate(
1865 HavanaIntegrateRequest {
1866 slots,
1867 sampling_correlation_mode: self.sampling_correlation_mode(),
1868 n_cores,
1869 state: integration_state,
1870 workspace: Some(workspace_path.clone()),
1871 output_control: self.workspace_snapshot_control(),
1872 batching: self
1873 .build_batching_settings(stream_updates, stream_updates || stream_iterations),
1874 view_options,
1875 },
1876 move |status_update: StatusUpdate| {
1877 stream_controller.handle_status_update(status_update)
1878 },
1879 )?;
1880
1881 Ok(IntegrationOutput {
1882 observables: self.collect_workspace_observable_snapshots(
1883 &workspace_path,
1884 selected_slots.iter().map(|slot| slot.slot_meta.clone()),
1885 )?,
1886 result,
1887 workspace_path,
1888 })
1889 }
1890}
1891
1892#[cfg(test)]
1893mod tests {
1894 use super::Integrate;
1895 use super::IntegrationOutput;
1896 use super::ResolvedIntegrandSlot;
1897 use super::{ContributionSortOption, DashboardKeyAction, ShowPhaseOption, TabledKeyAction};
1898 use crate::{
1899 state::{CommandHistory, ProcessRef, State},
1900 CLISettings, Commands, SessionSettings, StateSettings,
1901 };
1902 use clap::Parser;
1903 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
1904 use gammalooprs::{
1905 integrate::{ContributionSortMode, IntegrationStatusPhaseDisplay, SlotMeta},
1906 observables::ObservableSnapshotBundle,
1907 settings::{runtime::IntegratedPhase, IntegratorSettings, RuntimeSettings},
1908 utils::F,
1909 };
1910 use spenso::algebra::complex::Complex;
1911 use std::collections::BTreeMap;
1912 use std::path::PathBuf;
1913 use symbolica::numerical_integration::{ContinuousGrid, DiscreteGrid, Grid};
1914
1915 fn resolved_slot(process_name: &str, integrand_name: &str) -> ResolvedIntegrandSlot {
1916 ResolvedIntegrandSlot {
1917 process_id: 0,
1918 slot_meta: SlotMeta {
1919 process_name: process_name.to_string(),
1920 integrand_name: integrand_name.to_string(),
1921 },
1922 }
1923 }
1924
1925 #[derive(Debug, Parser)]
1926 struct IntegrateCli {
1927 #[command(flatten)]
1928 integrate: Integrate,
1929 }
1930
1931 #[test]
1932 fn read_only_state_uses_cwd_workspace_default() {
1933 let integrate = Integrate::default();
1934 let mut settings = CLISettings {
1935 state: StateSettings {
1936 folder: PathBuf::from("/tmp/saved_state"),
1937 name: Some("gg_hhh_1l".to_string()),
1938 },
1939 session: SessionSettings {
1940 read_only_state: true,
1941 ..SessionSettings::default()
1942 },
1943 ..Default::default()
1944 };
1945
1946 assert_eq!(
1947 integrate.default_workspace_path(&settings),
1948 PathBuf::from("./integration_workspace_gg_hhh_1l")
1949 );
1950
1951 settings.state.name = None;
1952 assert_eq!(
1953 integrate.default_workspace_path(&settings),
1954 PathBuf::from("./integration_workspace")
1955 );
1956 }
1957
1958 #[test]
1959 fn read_only_state_rejects_workspace_inside_active_state() {
1960 let integrate = Integrate {
1961 workspace_path: Some(PathBuf::from("/tmp/saved_state/integration_workspace")),
1962 ..Integrate::default()
1963 };
1964 let settings = CLISettings {
1965 state: StateSettings {
1966 folder: PathBuf::from("/tmp/saved_state"),
1967 ..StateSettings::default()
1968 },
1969 session: SessionSettings {
1970 read_only_state: true,
1971 ..SessionSettings::default()
1972 },
1973 ..Default::default()
1974 };
1975 let err = integrate
1976 .run(&mut State::new_test(), &settings)
1977 .unwrap_err();
1978 assert!(format!("{err:?}").contains("--read-only-state"));
1979 }
1980
1981 #[test]
1982 fn integrate_defaults_to_showing_overall_max_weight_info() {
1983 assert!(Integrate::default().show_max_weight_info);
1984 assert!(!Integrate::default().show_max_weight_info_for_discrete_bins);
1985 }
1986
1987 #[test]
1988 fn truncate_ansi_line_limits_visible_width() {
1989 let line = "\u{1b}[32mhello\u{1b}[0m world";
1990 let truncated = super::truncate_ansi_line(line, 7);
1991
1992 assert!(truncated.contains('\u{1b}'));
1993 assert!(truncated.ends_with("\u{1b}[0m"));
1994 assert!(!truncated.contains("world"));
1995 }
1996
1997 #[test]
1998 fn prepare_stream_block_truncates_each_line_independently() {
1999 let block = "123456789\nabcdefghi";
2000 let prepared = super::prepare_stream_block(block, 5);
2001
2002 assert_eq!(prepared, "12345\r\nabcde");
2003 }
2004
2005 #[test]
2006 fn resolve_targets_supports_keyed_multi_slot_targets() {
2007 let integrate = Integrate {
2008 target: vec![
2009 "triangle@LO=1.0,2.0".to_string(),
2010 "box@scalar_box=3.0,4.0".to_string(),
2011 ],
2012 ..Integrate::default()
2013 };
2014 let slots = vec![
2015 resolved_slot("triangle", "LO"),
2016 resolved_slot("box", "scalar_box"),
2017 ];
2018
2019 let targets = integrate.resolve_targets(&slots).unwrap();
2020
2021 assert_eq!(
2022 targets,
2023 vec![
2024 Some(Complex::new(F(1.0), F(2.0))),
2025 Some(Complex::new(F(3.0), F(4.0))),
2026 ]
2027 );
2028 }
2029
2030 #[test]
2031 fn resolve_targets_preserves_single_slot_legacy_target_format() {
2032 let integrate = Integrate {
2033 target: vec!["1.0".to_string(), "2.0".to_string()],
2034 ..Integrate::default()
2035 };
2036 let slots = vec![resolved_slot("triangle", "LO")];
2037
2038 let targets = integrate.resolve_targets(&slots).unwrap();
2039
2040 assert_eq!(targets, vec![Some(Complex::new(F(1.0), F(2.0)))]);
2041 }
2042
2043 #[test]
2044 fn resolve_targets_applies_shared_legacy_target_to_all_selected_slots() {
2045 let integrate = Integrate {
2046 target: vec!["1.0".to_string(), "2.0".to_string()],
2047 ..Integrate::default()
2048 };
2049 let slots = vec![
2050 resolved_slot("triangle", "LO"),
2051 resolved_slot("box", "scalar_box"),
2052 ];
2053
2054 let targets = integrate.resolve_targets(&slots).unwrap();
2055
2056 assert_eq!(
2057 targets,
2058 vec![
2059 Some(Complex::new(F(1.0), F(2.0))),
2060 Some(Complex::new(F(1.0), F(2.0))),
2061 ]
2062 );
2063 }
2064
2065 #[test]
2066 fn clap_parses_repeated_keyed_targets() {
2067 let parsed = IntegrateCli::try_parse_from([
2068 "gammaloop",
2069 "--target",
2070 "triangle@LO=1.0,2.0",
2071 "--target",
2072 "box@scalar_box=3.0,4.0",
2073 ])
2074 .unwrap();
2075
2076 assert_eq!(
2077 parsed.integrate.target,
2078 vec![
2079 "triangle@LO=1.0,2.0".to_string(),
2080 "box@scalar_box=3.0,4.0".to_string(),
2081 ]
2082 );
2083 }
2084
2085 #[test]
2086 fn clap_still_parses_legacy_single_slot_target_components() {
2087 let parsed =
2088 IntegrateCli::try_parse_from(["gammaloop", "--target", "-1.0", "2.0"]).unwrap();
2089
2090 assert_eq!(
2091 parsed.integrate.target,
2092 vec!["-1.0".to_string(), "2.0".to_string()]
2093 );
2094 }
2095
2096 #[test]
2097 fn clap_parses_single_token_negative_target_when_attached_to_flag() {
2098 let parsed = IntegrateCli::try_parse_from(["gammaloop", "--target=-1.0,-2.0"]).unwrap();
2099
2100 assert_eq!(parsed.integrate.target, vec!["-1.0,-2.0".to_string()]);
2101 }
2102
2103 #[test]
2104 fn command_history_parses_shared_target_for_multi_integrand_integration() {
2105 let raw = "integrate -p aa_aa -i 1L -p aa_aa -i 1L_m_uv_UP -p aa_aa -i 1L_m_uv_DOWN -p aa_aa -i 1L_mu_r_UP -p aa_aa -i 1L_mu_r_DOWN --n-cores 5 --target -1.0214510394091818e-6 0.0 --renderer ratatui --batch-size 10000 --show-phase both --show-max-weight-info --show-top-discrete-grid --show-discrete-contributions-sum --write-results-for-each-iteration --restart";
2106 let parsed = CommandHistory::from_raw_string(raw).unwrap();
2107
2108 let Commands::Integrate(integrate) = parsed.command else {
2109 panic!("expected integrate command");
2110 };
2111
2112 assert_eq!(
2113 integrate.target,
2114 vec!["-1.0214510394091818e-6,0.0".to_string()]
2115 );
2116 }
2117
2118 #[test]
2119 fn from_slots_preserves_slot_order() {
2120 let integrate = Integrate::from_slots([
2121 (ProcessRef::Id(2), "first"),
2122 (ProcessRef::Name("triangle".to_string()), "second"),
2123 ]);
2124
2125 assert_eq!(
2126 integrate.process,
2127 vec![ProcessRef::Id(2), ProcessRef::Name("triangle".to_string())]
2128 );
2129 assert_eq!(
2130 integrate.integrand_name,
2131 vec!["first".to_string(), "second".to_string()]
2132 );
2133 }
2134
2135 #[test]
2136 fn integration_output_single_slot_observables_returns_only_bundle() {
2137 let mut observables = BTreeMap::new();
2138 observables.insert(
2139 "box@scalar_box".to_string(),
2140 ObservableSnapshotBundle::default(),
2141 );
2142 let output = IntegrationOutput {
2143 observables,
2144 ..IntegrationOutput::default()
2145 };
2146
2147 assert!(output.single_slot_observables().is_some());
2148 }
2149
2150 #[test]
2151 fn sample_shape_compatibility_requires_matching_grid_topology() {
2152 let continuous_2d = Grid::Continuous(ContinuousGrid::new(2, 8, 0, None, false));
2153 let continuous_3d = Grid::Continuous(ContinuousGrid::new(3, 8, 0, None, false));
2154 let discrete_2 = Grid::Discrete(DiscreteGrid::new(vec![None, None], F(10.0), false));
2155 let discrete_nested = Grid::Discrete(DiscreteGrid::new(
2156 vec![
2157 Some(Grid::Continuous(ContinuousGrid::new(2, 8, 0, None, false))),
2158 Some(Grid::Continuous(ContinuousGrid::new(2, 8, 0, None, false))),
2159 ],
2160 F(10.0),
2161 false,
2162 ));
2163
2164 assert!(super::grids_have_compatible_sample_shape(
2165 &continuous_2d,
2166 &Grid::Continuous(ContinuousGrid::new(2, 4, 0, None, false))
2167 ));
2168 assert!(!super::grids_have_compatible_sample_shape(
2169 &continuous_2d,
2170 &continuous_3d
2171 ));
2172 assert!(!super::grids_have_compatible_sample_shape(
2173 &continuous_2d,
2174 &discrete_2
2175 ));
2176 assert!(!super::grids_have_compatible_sample_shape(
2177 &discrete_2,
2178 &discrete_nested
2179 ));
2180 }
2181
2182 #[test]
2183 fn selected_show_phase_resolves_from_slot_zero_phase() {
2184 assert_eq!(
2185 ShowPhaseOption::Selected.resolve(IntegratedPhase::Real),
2186 IntegrationStatusPhaseDisplay::Real
2187 );
2188 assert_eq!(
2189 ShowPhaseOption::Selected.resolve(IntegratedPhase::Imag),
2190 IntegrationStatusPhaseDisplay::Imag
2191 );
2192 }
2193
2194 #[test]
2195 fn build_render_options_propagates_discrete_monitoring_flags() {
2196 let integrate = Integrate {
2197 show_phase: ShowPhaseOption::Imag,
2198 show_max_weight_info: true,
2199 show_top_discrete_grid: true,
2200 show_discrete_contributions_sum: true,
2201 sort_contributions: ContributionSortOption::Integral,
2202 show_max_weight_info_for_discrete_bins: true,
2203 ..Integrate::default()
2204 };
2205 let settings = RuntimeSettings {
2206 integrator: IntegratorSettings {
2207 integrated_phase: IntegratedPhase::Real,
2208 ..Default::default()
2209 },
2210 ..RuntimeSettings::default()
2211 };
2212
2213 let render_options = integrate.build_render_options(std::slice::from_ref(&settings), false);
2214
2215 assert_eq!(
2216 render_options.phase_display,
2217 IntegrationStatusPhaseDisplay::Imag
2218 );
2219 assert!(!render_options.show_statistics);
2220 assert!(render_options.show_max_weight_details);
2221 assert!(render_options.show_top_discrete_grid);
2222 assert!(render_options.show_discrete_contributions_sum);
2223 assert_eq!(
2224 render_options.contribution_sort,
2225 ContributionSortMode::Integral
2226 );
2227 assert!(render_options.show_max_weight_info_for_discrete_bins);
2228 }
2229
2230 #[test]
2231 fn build_render_options_defaults_training_phase_to_real_for_both() {
2232 let integrate = Integrate::default();
2233 let settings = RuntimeSettings {
2234 integrator: IntegratorSettings {
2235 integrated_phase: IntegratedPhase::Both,
2236 ..Default::default()
2237 },
2238 ..RuntimeSettings::default()
2239 };
2240
2241 let render_options = integrate.build_render_options(&[settings], false);
2242
2243 assert_eq!(
2244 render_options.training_phase_display,
2245 IntegrationStatusPhaseDisplay::Real
2246 );
2247 assert_eq!(
2248 render_options.slot_training_phase_displays,
2249 vec![IntegrationStatusPhaseDisplay::Real]
2250 );
2251 }
2252
2253 #[test]
2254 fn dashboard_ctrl_c_requests_integration_interrupt() {
2255 let mut dashboard = gammalooprs::integrate::RatatuiDashboardState::new();
2256
2257 assert_eq!(
2258 super::handle_dashboard_key_event(
2259 &mut dashboard,
2260 KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
2261 ),
2262 DashboardKeyAction::InterruptIntegration
2263 );
2264 }
2265
2266 #[test]
2267 fn dashboard_x_requests_iteration_abort() {
2268 let mut dashboard = gammalooprs::integrate::RatatuiDashboardState::new();
2269
2270 assert_eq!(
2271 super::handle_dashboard_key_event(
2272 &mut dashboard,
2273 KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE),
2274 ),
2275 DashboardKeyAction::AbortCurrentIteration
2276 );
2277 }
2278
2279 #[test]
2280 fn dashboard_g_toggles_chart_history_window() {
2281 let mut dashboard = gammalooprs::integrate::RatatuiDashboardState::new();
2282
2283 assert_eq!(
2284 super::handle_dashboard_key_event(
2285 &mut dashboard,
2286 KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE),
2287 ),
2288 DashboardKeyAction::Redraw
2289 );
2290 }
2291
2292 #[test]
2293 fn dashboard_plus_adjusts_chart_history_window() {
2294 let mut dashboard = gammalooprs::integrate::RatatuiDashboardState::new();
2295
2296 assert_eq!(
2297 super::handle_dashboard_key_event(
2298 &mut dashboard,
2299 KeyEvent::new(KeyCode::Char('+'), KeyModifiers::NONE),
2300 ),
2301 DashboardKeyAction::Redraw
2302 );
2303 }
2304
2305 #[test]
2306 fn dashboard_comma_adjusts_chart_y_range() {
2307 let mut dashboard = gammalooprs::integrate::RatatuiDashboardState::new();
2308
2309 assert_eq!(
2310 super::handle_dashboard_key_event(
2311 &mut dashboard,
2312 KeyEvent::new(KeyCode::Char(','), KeyModifiers::NONE),
2313 ),
2314 DashboardKeyAction::Redraw
2315 );
2316 }
2317
2318 #[test]
2319 fn dashboard_zero_resets_chart_y_range() {
2320 let mut dashboard = gammalooprs::integrate::RatatuiDashboardState::new();
2321
2322 assert_eq!(
2323 super::handle_dashboard_key_event(
2324 &mut dashboard,
2325 KeyEvent::new(KeyCode::Char('0'), KeyModifiers::NONE),
2326 ),
2327 DashboardKeyAction::Redraw
2328 );
2329 }
2330
2331 #[test]
2332 fn dashboard_p_toggles_chart_phase() {
2333 let mut dashboard = gammalooprs::integrate::RatatuiDashboardState::new();
2334
2335 assert_eq!(
2336 super::handle_dashboard_key_event(
2337 &mut dashboard,
2338 KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE),
2339 ),
2340 DashboardKeyAction::Redraw
2341 );
2342 }
2343
2344 #[test]
2345 fn dashboard_i_toggles_statistics_scope() {
2346 let mut dashboard = gammalooprs::integrate::RatatuiDashboardState::new();
2347
2348 assert_eq!(
2349 super::handle_dashboard_key_event(
2350 &mut dashboard,
2351 KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE),
2352 ),
2353 DashboardKeyAction::Redraw
2354 );
2355 }
2356
2357 #[test]
2358 fn tabled_ctrl_c_requests_integration_interrupt() {
2359 assert_eq!(
2360 super::handle_tabled_key_event(KeyEvent::new(
2361 KeyCode::Char('c'),
2362 KeyModifiers::CONTROL,
2363 )),
2364 TabledKeyAction::InterruptIntegration
2365 );
2366 }
2367
2368 #[test]
2369 fn batching_settings_can_emit_initial_status_without_live_updates() {
2370 let integrate = Integrate::default();
2371 let batching = integrate.build_batching_settings(false, true);
2372
2373 assert!(!batching.emit_live_status_updates);
2374 assert!(batching.emit_initial_status_update);
2375 }
2376}