1#![allow(dead_code)]
2
3mod display;
9mod render_ratatui;
10mod render_tabled;
11mod status_update;
12
13use bincode::Decode;
14use bincode::Encode;
15use color_eyre::{Report, Result};
16use colored::Colorize;
17use itertools::Itertools;
18use itertools::izip;
19use linnet::half_edge::involution::{EdgeVec, Orientation};
20use rayon::ThreadPoolBuilder;
21use serde::Deserialize;
22use serde::Serialize;
23use spenso::algebra::algebraic_traits::IsZero;
24use symbolica::domains::float::Constructible;
25use symbolica::numerical_integration::{
26 DiscreteGrid, Grid, MonteCarloRng, Sample, StatisticsAccumulator,
27};
28
29use crate::Integrand;
30use crate::graph::{GroupId, LoopMomentumBasis};
31use crate::integrands::HasIntegrand;
32use crate::integrands::evaluation::EvaluationResult;
33use crate::integrands::evaluation::StatisticsCounter;
34use crate::integrands::process::ProcessIntegrand;
35use crate::model::{Model, SerializableInputParamCard};
36use crate::observables::{EventGroupList, ObservableAccumulatorBundle, ObservableFileFormat};
37use crate::settings::IntegratorSettings;
38use crate::settings::RuntimeSettings;
39use crate::settings::runtime::{
40 ComponentDiscreteBreakdown, DiscreteBreakdown, DiscreteBreakdownEntry, DiscreteCoordinate,
41 DiscreteGraphSamplingType, IntegralEstimate, IntegratedPhase, IntegrationResult,
42 IntegrationTableComponentResult, MaxWeightInfoEntry, SamplingSettings, SlotIntegrationResult,
43};
44use crate::utils;
45use crate::utils::F;
46use crate::{
47 clear_iteration_abort_request, is_interrupted, is_iteration_abort_requested, set_interrupted,
48};
49use rayon::prelude::*;
50pub use render_ratatui::RatatuiDashboardState;
51pub use render_tabled::TabledRenderOptions;
52use spenso::algebra::complex::Complex;
53pub use status_update::{
54 ContributionSortMode, IntegrationStatusKind, IntegrationStatusPhaseDisplay,
55 IntegrationStatusViewOptions, StatusUpdate,
56};
57use status_update::{
58 StatusUpdateBuildRequest, build_saved_status_update, build_status_update,
59 evaluate_target_accuracy,
60};
61use std::fs;
62use std::path::Path;
63use std::path::PathBuf;
64use std::time::Duration;
65use std::time::Instant;
66use tabled::{
67 Tabled,
68 builder::Builder,
69 settings::{
70 Alignment, Modify, Panel, Style,
71 object::{Columns, Rows},
72 style::HorizontalLine,
73 },
74};
75#[allow(unused_imports)]
76use tracing::{debug, error, info, trace, warn};
77
78#[derive(Clone, Copy, Debug, Default)]
79pub struct WorkspaceSnapshotControl {
80 pub write_iteration_archives: bool,
81}
82
83#[derive(Clone, Copy, Debug)]
84pub struct IterationBatchingSettings {
85 pub batch_size: Option<usize>,
86 pub batch_timing_seconds: f64,
87 pub min_time_between_status_updates_seconds: f64,
88 pub emit_live_status_updates: bool,
89 pub emit_initial_status_update: bool,
90}
91
92impl Default for IterationBatchingSettings {
93 fn default() -> Self {
94 Self {
95 batch_size: None,
96 batch_timing_seconds: 5.0,
97 min_time_between_status_updates_seconds: 0.0,
98 emit_live_status_updates: true,
99 emit_initial_status_update: true,
100 }
101 }
102}
103
104pub struct IntegrationSlot {
105 pub meta: SlotMeta,
106 pub settings: RuntimeSettings,
107 pub model: Model,
108 pub integrand: Integrand,
109 pub target: Option<Complex<F<f64>>>,
110}
111
112impl IntegrationSlot {
113 pub fn new(
114 meta: SlotMeta,
115 settings: RuntimeSettings,
116 model: Model,
117 integrand: Integrand,
118 target: Option<Complex<F<f64>>>,
119 ) -> Self {
120 Self {
121 meta,
122 settings,
123 model,
124 integrand,
125 target,
126 }
127 }
128}
129
130pub struct HavanaIntegrateRequest {
131 pub slots: Vec<IntegrationSlot>,
132 pub sampling_correlation_mode: SamplingCorrelationMode,
133 pub n_cores: usize,
134 pub state: Option<IntegrationState>,
135 pub workspace: Option<PathBuf>,
136 pub output_control: WorkspaceSnapshotControl,
137 pub batching: IterationBatchingSettings,
138 pub view_options: IntegrationStatusViewOptions,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
144pub struct SlotMeta {
145 pub process_name: String,
146 pub integrand_name: String,
147}
148
149impl SlotMeta {
150 pub fn key(&self) -> String {
151 format!("{}@{}", self.process_name, self.integrand_name)
152 }
153}
154
155#[derive(Serialize, Deserialize, Clone)]
156pub struct IntegrationWorkspaceManifest {
157 pub slots: Vec<SlotMeta>,
158 pub targets: Vec<Option<Complex<F<f64>>>>,
159 pub effective_model_parameters: Vec<SerializableInputParamCard<F<f64>>>,
160 pub integrand_fingerprints: Vec<String>,
161 pub training_slot: usize,
162 pub integrator_settings_slot: usize,
163 pub sampling_correlation_mode: SamplingCorrelationMode,
164}
165
166impl crate::utils::serde_utils::SmartSerde for IntegrationWorkspaceManifest {}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
169#[serde(rename_all = "snake_case")]
170pub enum SamplingCorrelationMode {
171 Correlated,
172 Uncorrelated,
173}
174
175impl SamplingCorrelationMode {
176 fn state_index(self, slot_index: usize) -> usize {
177 match self {
178 Self::Correlated => 0,
179 Self::Uncorrelated => slot_index,
180 }
181 }
182
183 fn state_count(self, n_slots: usize) -> usize {
184 match self {
185 Self::Correlated => 1,
186 Self::Uncorrelated => n_slots,
187 }
188 }
189}
190
191#[derive(Serialize, Deserialize, Encode, Decode, Clone)]
192struct DiscreteGridAccumulatorSummary {
193 #[bincode(with_serde)]
194 bins: Vec<DiscreteGridBinAccumulatorSummary>,
195}
196
197#[derive(Serialize, Deserialize, Encode, Decode, Clone)]
198struct DiscreteGridBinAccumulatorSummary {
199 #[bincode(with_serde)]
200 accumulator: StatisticsAccumulator<F<f64>>,
201 sub_summary: Option<Box<DiscreteGridAccumulatorSummary>>,
202}
203
204#[derive(Serialize, Deserialize, Encode, Decode, Clone, Default)]
205struct PersistedDiscreteBreakdownMetadata {
206 axis_label: String,
207 #[bincode(with_serde)]
208 fixed_coordinates: Vec<DiscreteCoordinate>,
209 bin_labels: Vec<String>,
210}
211
212#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
213struct SamplingSlotState {
214 #[bincode(with_serde)]
215 grid: Grid<F<f64>>,
216 discrete_axis_labels: Vec<String>,
217}
218
219impl SamplingSlotState {
220 fn new(grid: Grid<F<f64>>, discrete_axis_labels: Vec<String>) -> Self {
221 Self {
222 grid,
223 discrete_axis_labels,
224 }
225 }
226
227 fn from_integrand(integrand: &Integrand) -> Self {
228 let grid = integrand.create_grid();
229 let discrete_axis_labels = match integrand {
230 Integrand::ProcessIntegrand(process_integrand) => {
231 discrete_axis_labels(&process_integrand.get_settings().sampling)
232 .into_iter()
233 .map(str::to_string)
234 .collect_vec()
235 }
236 _ => Vec::new(),
237 };
238 Self::new(grid, discrete_axis_labels)
239 }
240}
241
242#[derive(Default)]
243struct MonitoredDiscreteSetup {
244 path: Option<Vec<usize>>,
245 axis_label: Option<String>,
246 bin_descriptions: Option<Vec<String>>,
247 warning: Option<String>,
248}
249
250#[derive(Tabled)]
251pub struct IntegralResult {
252 id: String,
253 n_samples: String,
254 #[tabled(rename = "n_samples[%]")]
255 n_samples_perc: String,
256 #[tabled(rename = "<I>")]
257 integral: String,
258 #[tabled(rename = "sqrt(σ)")]
259 variance: String,
260 err: String,
261 #[tabled(rename = "err[%]")]
262 err_perc: String,
263 #[tabled(rename = "PDF")]
264 pdf: String,
265}
266
267#[derive(Serialize, Deserialize, Encode, Decode, Clone)]
268pub struct ComplexAccumulator {
269 #[bincode(with_serde)]
270 pub re: StatisticsAccumulator<F<f64>>,
271 #[bincode(with_serde)]
272 pub im: StatisticsAccumulator<F<f64>>,
273}
274
275impl ComplexAccumulator {
276 pub(crate) fn new() -> Self {
277 Self {
278 re: StatisticsAccumulator::new(),
279 im: StatisticsAccumulator::new(),
280 }
281 }
282
283 pub(crate) fn get_worst_case(&self) -> Complex<F<f64>> {
285 Complex::new(
286 self.re
287 .max_eval_positive
288 .abs()
289 .max(self.re.max_eval_negative.abs()),
290 self.im
291 .max_eval_positive
292 .abs()
293 .max(self.im.max_eval_negative.abs()),
294 )
295 }
296
297 pub(crate) fn add_sample(
299 &mut self,
300 result: Complex<F<f64>>,
301 sample_weight: F<f64>,
302 sample: Option<&Sample<F<f64>>>,
303 ) {
304 self.re.add_sample(result.re * sample_weight, sample);
305 self.im.add_sample(result.im * sample_weight, sample);
306 }
307
308 pub(crate) fn merge(&mut self, other: &Self) {
309 self.re.merge_samples_no_reset(&other.re);
310 self.im.merge_samples_no_reset(&other.im);
311 }
312
313 pub(crate) fn update_iter(&mut self, use_weighted_average: bool) {
314 self.re.update_iter(use_weighted_average);
315 self.im.update_iter(use_weighted_average);
316 }
317
318 pub(crate) fn max_weight_rows(
319 &self,
320 slot_meta: &SlotMeta,
321 discrete_axis_labels: &[String],
322 ) -> Vec<[String; 3]> {
323 let max_evals = [
324 &self.re.max_eval_positive,
325 &self.re.max_eval_negative,
326 &self.im.max_eval_positive,
327 &self.im.max_eval_negative,
328 ];
329
330 let max_eval_samples = [
331 &self.re.max_eval_positive_xs,
332 &self.re.max_eval_negative_xs,
333 &self.im.max_eval_positive_xs,
334 &self.im.max_eval_negative_xs,
335 ];
336
337 let sign_strs = ["+", "-", "+", "-"];
338 let phase_strs = ["re", "re", "im", "im"];
339
340 izip!(
341 max_evals.iter(),
342 max_eval_samples.iter(),
343 sign_strs.iter(),
344 phase_strs.iter()
345 )
346 .filter_map(|(max_eval, max_eval_sample, sign_str, phase_str)| {
347 if max_eval.is_non_zero() {
348 Some([
349 format!(
350 "{} {} [{}] ",
351 slot_label(slot_meta),
352 format!("{:<2}", phase_str).blue(),
353 format!("{:<1}", sign_str).blue()
354 ),
355 format!("{:+.16e}", max_eval),
356 if let Some(sample) = max_eval_sample {
357 format_max_eval_sample(sample, discrete_axis_labels, &[])
358 } else {
359 "N/A".to_string()
360 },
361 ])
362 } else {
363 None
364 }
365 })
366 .collect()
367 }
368}
369
370impl DiscreteGridAccumulatorSummary {
371 fn from_grid(grid: &Grid<F<f64>>) -> Option<Self> {
372 match grid {
373 Grid::Discrete(discrete_grid) => Some(Self {
374 bins: discrete_grid
375 .bins
376 .iter()
377 .map(|bin| DiscreteGridBinAccumulatorSummary {
378 accumulator: StatisticsAccumulator::new(),
379 sub_summary: bin
380 .sub_grid
381 .as_ref()
382 .and_then(Self::from_grid)
383 .map(Box::new),
384 })
385 .collect(),
386 }),
387 Grid::Continuous(_) | Grid::Uniform(_, _) => None,
388 }
389 }
390
391 fn merge_iteration_grid(&mut self, grid: &Grid<F<f64>>) {
392 let Grid::Discrete(discrete_grid) = grid else {
393 return;
394 };
395
396 for (summary_bin, grid_bin) in self.bins.iter_mut().zip(discrete_grid.bins.iter()) {
397 summary_bin
398 .accumulator
399 .merge_samples_no_reset(&grid_bin.accumulator);
400 if let (Some(sub_summary), Some(sub_grid)) =
401 (summary_bin.sub_summary.as_mut(), grid_bin.sub_grid.as_ref())
402 {
403 sub_summary.merge_iteration_grid(sub_grid);
404 }
405 }
406 }
407
408 fn update_iter(&mut self) {
409 for bin in &mut self.bins {
410 bin.accumulator.update_iter(false);
411 if let Some(sub_summary) = bin.sub_summary.as_mut() {
412 sub_summary.update_iter();
413 }
414 }
415 }
416
417 fn first_non_trivial_breakdown(
418 &self,
419 metadata: &PersistedDiscreteBreakdownMetadata,
420 pdfs: &[F<f64>],
421 ) -> Option<DiscreteBreakdown> {
422 (!self.bins.is_empty()).then(|| DiscreteBreakdown {
423 axis_label: metadata.axis_label.clone(),
424 fixed_coordinates: metadata.fixed_coordinates.clone(),
425 entries: self
426 .bins
427 .iter()
428 .enumerate()
429 .map(|(bin_index, bin)| DiscreteBreakdownEntry {
430 bin_index,
431 bin_label: metadata.bin_labels.get(bin_index).cloned(),
432 pdf: pdfs.get(bin_index).copied().unwrap_or(F(0.0)),
433 value: bin.accumulator.avg,
434 error: bin.accumulator.err,
435 chi_sq: bin.accumulator.chi_sq,
436 processed_samples: bin.accumulator.processed_samples,
437 })
438 .collect(),
439 })
440 }
441}
442
443struct IntegralResultCells {
444 integrand: String,
445 value: String,
446 relative_error: String,
447 chi_sq: String,
448 delta_sigma: Option<String>,
449 delta_percent: Option<String>,
450 mwi: String,
451}
452
453const DEFAULT_MAX_SHARED_TABLE_WIDTH: usize = 250;
454
455fn slot_label(slot_meta: &SlotMeta) -> String {
456 format!("itg {}", slot_meta.key())
457}
458
459fn slot_key_label(slot_meta: &SlotMeta) -> String {
460 slot_meta.key()
461}
462
463fn format_max_eval_coordinate(value: F<f64>) -> String {
464 let formatted = format!("{:.16e}", value.0);
465 let Some((mantissa, exponent)) = formatted.rsplit_once('e') else {
466 return formatted;
467 };
468 let exponent = exponent.parse::<i32>().unwrap_or_default();
469 format!("{mantissa}e{exponent:+03}")
470}
471
472fn format_max_eval_coordinates(xs: &[F<f64>]) -> String {
473 if xs.len() <= 3 {
474 return format!(
475 "[ {} ]",
476 xs.iter()
477 .map(|value| format_max_eval_coordinate(*value))
478 .join(" ")
479 );
480 }
481
482 let rows = xs
483 .chunks(3)
484 .map(|chunk| {
485 chunk
486 .iter()
487 .map(|value| format_max_eval_coordinate(*value))
488 .join(" ")
489 })
490 .join("\n");
491 format!("[\n{rows} ]")
492}
493
494pub(crate) fn discrete_axis_label(axis_labels: &[String], depth: usize) -> &str {
495 axis_labels.get(depth).map(String::as_str).unwrap_or("idx")
496}
497
498fn append_max_eval_sample_parts(
499 sample: &Sample<F<f64>>,
500 axis_labels: &[String],
501 depth: usize,
502 parts: &mut Vec<String>,
503) {
504 match sample {
505 Sample::Continuous(_, xs) => {
506 parts.push(format!("xs: {}", format_max_eval_coordinates(xs)));
507 }
508 Sample::Discrete(_, index, Some(nested_sample)) => {
509 parts.push(format!(
510 "{}: {}",
511 discrete_axis_label(axis_labels, depth),
512 index
513 ));
514 append_max_eval_sample_parts(nested_sample, axis_labels, depth + 1, parts);
515 }
516 Sample::Discrete(_, index, None) => {
517 parts.push(format!(
518 "{}: {}",
519 discrete_axis_label(axis_labels, depth),
520 index
521 ));
522 }
523 Sample::Uniform(_, indices, xs) => {
524 for (offset, index) in indices.iter().enumerate() {
525 parts.push(format!(
526 "{}: {}",
527 discrete_axis_label(axis_labels, depth + offset),
528 index
529 ));
530 }
531 parts.push(format!("xs: {}", format_max_eval_coordinates(xs)));
532 }
533 }
534}
535
536fn format_max_eval_sample(
537 sample: &Sample<F<f64>>,
538 axis_labels: &[String],
539 prefix_path: &[usize],
540) -> String {
541 let mut parts = prefix_path
542 .iter()
543 .enumerate()
544 .map(|(depth, index)| format!("{}: {}", discrete_axis_label(axis_labels, depth), index))
545 .collect_vec();
546 append_max_eval_sample_parts(sample, axis_labels, prefix_path.len(), &mut parts);
547 if parts.is_empty() {
548 String::from("N/A")
549 } else {
550 parts.join(", ")
551 }
552}
553
554fn format_iteration_points(points: usize) -> String {
555 format_abbreviated_count(points)
556}
557
558fn format_total_points(points: usize) -> String {
559 format_abbreviated_count(points)
560}
561
562#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
563enum ComponentKind {
564 Real,
565 Imag,
566}
567
568impl ComponentKind {
569 fn all_for_display(display: IntegrationStatusPhaseDisplay) -> Vec<Self> {
570 let mut components = Vec::new();
571 if display.shows_real() {
572 components.push(Self::Real);
573 }
574 if display.shows_imag() {
575 components.push(Self::Imag);
576 }
577 components
578 }
579
580 fn tag(self) -> &'static str {
581 match self {
582 Self::Real => "re",
583 Self::Imag => "im",
584 }
585 }
586
587 fn colorized_tag(self) -> String {
588 self.tag().blue().bold().to_string()
589 }
590}
591
592#[derive(Clone, Copy, Debug, Eq, PartialEq)]
593enum ContributionKind {
594 All,
595 Sum,
596 Bin(usize),
597}
598
599impl ContributionKind {
600 fn label(self, integration_state: &IntegrationState) -> String {
601 match self {
602 Self::All => "All".bold().green().to_string(),
603 Self::Sum => "Sum".bold().green().to_string(),
604 Self::Bin(bin_index) => contribution_bin_label(integration_state, bin_index),
605 }
606 }
607}
608
609#[derive(Clone, Debug)]
610struct DiscreteLevelContext {
611 path: Vec<usize>,
612 pdfs: Vec<F<f64>>,
613}
614
615fn first_non_trivial_discrete_path(grid: &Grid<F<f64>>) -> Option<Vec<usize>> {
616 match grid {
617 Grid::Discrete(discrete_grid) => {
618 if discrete_grid.bins.len() > 1 {
619 Some(Vec::new())
620 } else {
621 discrete_grid
622 .bins
623 .first()
624 .and_then(|bin| bin.sub_grid.as_ref())
625 .and_then(first_non_trivial_discrete_path)
626 .map(|mut path| {
627 path.insert(0, 0);
628 path
629 })
630 }
631 }
632 Grid::Continuous(_) | Grid::Uniform(_, _) => None,
633 }
634}
635
636fn discrete_grid_at_path<'a>(
637 grid: &'a Grid<F<f64>>,
638 path: &[usize],
639) -> Option<&'a DiscreteGrid<F<f64>>> {
640 match (grid, path.split_first()) {
641 (Grid::Discrete(discrete_grid), None) => Some(discrete_grid),
642 (Grid::Discrete(discrete_grid), Some((bin_index, rest))) => discrete_grid
643 .bins
644 .get(*bin_index)?
645 .sub_grid
646 .as_ref()
647 .and_then(|sub_grid| discrete_grid_at_path(sub_grid, rest)),
648 _ => None,
649 }
650}
651
652fn summary_at_path<'a>(
653 summary: &'a DiscreteGridAccumulatorSummary,
654 path: &[usize],
655) -> Option<&'a DiscreteGridAccumulatorSummary> {
656 match path.split_first() {
657 None => Some(summary),
658 Some((bin_index, rest)) => summary
659 .bins
660 .get(*bin_index)?
661 .sub_summary
662 .as_deref()
663 .and_then(|sub_summary| summary_at_path(sub_summary, rest)),
664 }
665}
666
667fn first_non_trivial_discrete_context(
668 sampling_grid: &Grid<F<f64>>,
669) -> Option<DiscreteLevelContext> {
670 let path = first_non_trivial_discrete_path(sampling_grid)?;
671 let discrete_grid = discrete_grid_at_path(sampling_grid, &path)?;
672 Some(DiscreteLevelContext {
673 path,
674 pdfs: discrete_grid.bins.iter().map(|bin| bin.pdf).collect(),
675 })
676}
677
678fn discrete_axis_labels(sampling: &SamplingSettings) -> Vec<&'static str> {
679 match sampling {
680 SamplingSettings::Default(_) | SamplingSettings::MultiChanneling(_) => Vec::new(),
681 SamplingSettings::DiscreteGraphs(settings) => {
682 let mut labels = vec!["graph"];
683 match &settings.sampling_type {
684 DiscreteGraphSamplingType::Default(_)
685 | DiscreteGraphSamplingType::MultiChanneling(_)
686 | DiscreteGraphSamplingType::TropicalSampling(_) => {
687 if settings.sample_orientations {
688 labels.push("orientation");
689 }
690 }
691 DiscreteGraphSamplingType::DiscreteMultiChanneling(_) => {
692 if settings.sample_orientations {
693 labels.push("orientation");
694 }
695 labels.push("LMB channel");
696 }
697 }
698 labels
699 }
700 }
701}
702
703fn orientation_description(orientation: &EdgeVec<Orientation>) -> String {
704 orientation
705 .iter()
706 .map(|(_, orientation)| match *orientation {
707 Orientation::Default => '+',
708 Orientation::Reversed => '-',
709 Orientation::Undirected => '0',
710 })
711 .collect()
712}
713
714fn graph_group_description<I>(graph_names: I) -> String
715where
716 I: IntoIterator<Item = String>,
717{
718 let graph_names = graph_names.into_iter().collect_vec();
719 if graph_names.len() <= 1 {
720 return graph_names.into_iter().next().unwrap_or_default();
721 }
722
723 format!("[{}]", graph_names.join(","))
724}
725
726fn lmb_channel_description(lmb: &LoopMomentumBasis) -> String {
727 format!(
728 "({})",
729 lmb.loop_edges
730 .iter()
731 .map(|edge_id| edge_id.0.to_string())
732 .join(",")
733 )
734}
735
736fn first_non_trivial_discrete_bin_descriptions_for_process_integrand(
737 integrand: &ProcessIntegrand,
738 path: &[usize],
739 axis_label: &str,
740) -> Option<Vec<String>> {
741 match (integrand, axis_label) {
742 (ProcessIntegrand::Amplitude(integrand), "graph") => {
743 Some(
744 integrand
745 .data
746 .graph_group_structure
747 .iter()
748 .map(|group| {
749 graph_group_description(group.into_iter().map(|graph_id| {
750 integrand.data.graph_terms[graph_id].graph.name.clone()
751 }))
752 })
753 .collect(),
754 )
755 }
756 (ProcessIntegrand::CrossSection(integrand), "graph") => {
757 Some(
758 integrand
759 .data
760 .graph_group_structure
761 .iter()
762 .map(|group| {
763 graph_group_description(group.into_iter().map(|graph_id| {
764 integrand.data.graph_terms[graph_id].graph.name.clone()
765 }))
766 })
767 .collect(),
768 )
769 }
770 (ProcessIntegrand::Amplitude(integrand), "orientation") => {
771 let group_id = GroupId(*path.first()?);
772 let group = integrand.data.graph_group_structure.get(group_id)?;
773 let master = group.master();
774 Some(
775 integrand.data.graph_terms[master]
776 .orientations
777 .iter()
778 .map(orientation_description)
779 .collect(),
780 )
781 }
782 (ProcessIntegrand::CrossSection(integrand), "orientation") => {
783 let group_id = GroupId(*path.first()?);
784 let group = integrand.data.graph_group_structure.get(group_id)?;
785 let master = group.master();
786 Some(
787 integrand.data.graph_terms[master]
788 .orientations
789 .iter()
790 .map(orientation_description)
791 .collect(),
792 )
793 }
794 (ProcessIntegrand::Amplitude(integrand), "LMB channel") => {
795 let group_id = GroupId(*path.first()?);
796 let group = integrand.data.graph_group_structure.get(group_id)?;
797 let master = group.master();
798 let graph_term = &integrand.data.graph_terms[master];
799 let parameterization_settings = integrand
800 .settings
801 .sampling
802 .get_parameterization_settings()
803 .unwrap_or_default();
804 let effective_channels = graph_term
805 .multi_channeling_setup
806 .effective_channels(&graph_term.graph.name, ¶meterization_settings)
807 .ok()?;
808 Some(
809 effective_channels
810 .iter()
811 .map(|&channel_lmb| {
812 lmb_channel_description(
813 &graph_term.multi_channeling_setup.all_bases[channel_lmb],
814 )
815 })
816 .collect(),
817 )
818 }
819 (ProcessIntegrand::CrossSection(integrand), "LMB channel") => {
820 let group_id = GroupId(*path.first()?);
821 let group = integrand.data.graph_group_structure.get(group_id)?;
822 let master = group.master();
823 let graph_term = &integrand.data.graph_terms[master];
824 let parameterization_settings = integrand
825 .settings
826 .sampling
827 .get_parameterization_settings()
828 .unwrap_or_default();
829 let effective_channels = graph_term
830 .multi_channeling_setup
831 .effective_channels(&graph_term.graph.name, ¶meterization_settings)
832 .ok()?;
833 Some(
834 effective_channels
835 .iter()
836 .map(|&channel_lmb| {
837 lmb_channel_description(
838 &graph_term.multi_channeling_setup.all_bases[channel_lmb],
839 )
840 })
841 .collect(),
842 )
843 }
844 _ => None,
845 }
846}
847
848fn first_non_trivial_discrete_bin_descriptions_for_integrand(
849 integrand: &Integrand,
850 path: &[usize],
851 axis_label: &str,
852) -> Option<Vec<String>> {
853 match integrand {
854 Integrand::ProcessIntegrand(process_integrand) => {
855 first_non_trivial_discrete_bin_descriptions_for_process_integrand(
856 process_integrand,
857 path,
858 axis_label,
859 )
860 }
861 _ => None,
862 }
863}
864
865fn discrete_coordinate_label(
866 integrand: &Integrand,
867 path_prefix: &[usize],
868 axis_label: &str,
869 bin_index: usize,
870) -> Option<String> {
871 first_non_trivial_discrete_bin_descriptions_for_integrand(integrand, path_prefix, axis_label)?
872 .get(bin_index)
873 .cloned()
874}
875
876fn build_persisted_discrete_breakdown_metadata(
877 integrand: &Integrand,
878 path: &[usize],
879 discrete_axis_labels: &[String],
880) -> Option<PersistedDiscreteBreakdownMetadata> {
881 let axis_label = discrete_axis_labels.get(path.len())?.clone();
882 let bin_labels =
883 first_non_trivial_discrete_bin_descriptions_for_integrand(integrand, path, &axis_label)?;
884
885 let fixed_coordinates = path
886 .iter()
887 .enumerate()
888 .map(|(axis_index, &bin_index)| {
889 let fixed_axis_label = discrete_axis_labels.get(axis_index)?.clone();
890 Some(DiscreteCoordinate {
891 axis_label: fixed_axis_label.clone(),
892 bin_index,
893 bin_label: discrete_coordinate_label(
894 integrand,
895 &path[..axis_index],
896 &fixed_axis_label,
897 bin_index,
898 ),
899 })
900 })
901 .collect::<Option<Vec<_>>>()?;
902
903 Some(PersistedDiscreteBreakdownMetadata {
904 axis_label,
905 fixed_coordinates,
906 bin_labels,
907 })
908}
909
910fn monitored_discrete_layout(
911 grid: &Grid<F<f64>>,
912 discrete_axis_labels: &[String],
913 monitor_explicit_graph_subset: bool,
914) -> Option<(Vec<usize>, String, usize)> {
915 let path = if monitor_explicit_graph_subset
916 && discrete_axis_labels
917 .first()
918 .is_some_and(|label| label == "graph")
919 && matches!(grid, Grid::Discrete(_))
920 {
921 Vec::new()
922 } else {
923 first_non_trivial_discrete_path(grid)?
924 };
925 let axis_label = discrete_axis_labels.get(path.len())?.clone();
926 let discrete_grid = discrete_grid_at_path(grid, &path)?;
927 Some((path, axis_label, discrete_grid.bins.len()))
928}
929
930fn coalesce_first_non_trivial_discrete_bin_descriptions(
931 axis_label: &str,
932 slot_descriptions: &[(String, Vec<String>)],
933) -> (Option<Vec<String>>, Option<String>) {
934 let Some((reference_slot, reference_descriptions)) = slot_descriptions.first() else {
935 return (None, None);
936 };
937
938 if let Some((slot_key, _)) = slot_descriptions
939 .iter()
940 .skip(1)
941 .find(|(_, descriptions)| descriptions != reference_descriptions)
942 {
943 return (
944 None,
945 Some(format!(
946 "Selected integrands do not share the same semantic labels for the monitored discrete dimension '{axis_label}' ({} vs {}). Falling back to raw bin indices in integration reporting.",
947 reference_slot.blue(),
948 slot_key.blue()
949 )),
950 );
951 }
952
953 (Some(reference_descriptions.clone()), None)
954}
955
956fn resolve_first_non_trivial_discrete_bin_descriptions(
957 slots: &[IntegrationSlot],
958 monitored_path: &[usize],
959 axis_label: &str,
960) -> (Option<Vec<String>>, Option<String>) {
961 let Some(slot_descriptions) = slots
962 .iter()
963 .map(|slot| {
964 first_non_trivial_discrete_bin_descriptions_for_integrand(
965 &slot.integrand,
966 monitored_path,
967 axis_label,
968 )
969 .map(|descriptions| (slot.meta.key(), descriptions))
970 })
971 .collect::<Option<Vec<_>>>()
972 else {
973 return (None, None);
974 };
975
976 coalesce_first_non_trivial_discrete_bin_descriptions(axis_label, &slot_descriptions)
977}
978
979fn resolve_monitored_discrete_setup(
980 sampling_correlation_mode: SamplingCorrelationMode,
981 slots: &[IntegrationSlot],
982 sampling_states: &[SamplingSlotState],
983) -> MonitoredDiscreteSetup {
984 let Some(reference_state) = sampling_states.first() else {
985 return MonitoredDiscreteSetup::default();
986 };
987 let Some(reference_slot) = slots.first() else {
988 return MonitoredDiscreteSetup::default();
989 };
990 let monitor_explicit_graph_subset = !reference_slot
991 .settings
992 .sampling
993 .selected_graph_names()
994 .is_empty();
995
996 let Some((reference_path, reference_axis_label, reference_bin_count)) =
997 monitored_discrete_layout(
998 &reference_state.grid,
999 &reference_state.discrete_axis_labels,
1000 monitor_explicit_graph_subset,
1001 )
1002 else {
1003 return MonitoredDiscreteSetup::default();
1004 };
1005
1006 if sampling_correlation_mode == SamplingCorrelationMode::Uncorrelated {
1007 for (slot_index, sampling_state) in sampling_states.iter().enumerate().skip(1) {
1008 let Some((path, axis_label, bin_count)) = monitored_discrete_layout(
1009 &sampling_state.grid,
1010 &sampling_state.discrete_axis_labels,
1011 !slots[slot_index]
1012 .settings
1013 .sampling
1014 .selected_graph_names()
1015 .is_empty(),
1016 ) else {
1017 return MonitoredDiscreteSetup {
1018 warning: Some(
1019 "Selected integrands do not all expose a compatible first non-trivial monitored discrete dimension. Shared discrete-bin monitoring tables will be disabled."
1020 .to_string(),
1021 ),
1022 ..MonitoredDiscreteSetup::default()
1023 };
1024 };
1025
1026 if path != reference_path
1027 || axis_label != reference_axis_label
1028 || bin_count != reference_bin_count
1029 {
1030 return MonitoredDiscreteSetup {
1031 warning: Some(format!(
1032 "Selected integrands do not share a compatible first non-trivial monitored discrete layout (mismatch at {}). Shared discrete-bin monitoring tables will be disabled.",
1033 slots[slot_index].meta.key().blue()
1034 )),
1035 ..MonitoredDiscreteSetup::default()
1036 };
1037 }
1038 }
1039 }
1040
1041 let (descriptions, label_warning) = resolve_first_non_trivial_discrete_bin_descriptions(
1042 slots,
1043 &reference_path,
1044 &reference_axis_label,
1045 );
1046
1047 MonitoredDiscreteSetup {
1048 path: Some(reference_path),
1049 axis_label: Some(reference_axis_label),
1050 bin_descriptions: descriptions,
1051 warning: label_warning,
1052 }
1053}
1054
1055fn render_orientation_description(description: &str) -> String {
1056 description
1057 .chars()
1058 .map(|sign| match sign {
1059 '+' => "+".green().bold().to_string(),
1060 '-' => "-".red().bold().to_string(),
1061 '0' => "0".dimmed().to_string(),
1062 other => other.to_string(),
1063 })
1064 .join("")
1065}
1066
1067fn render_graph_description(description: &str) -> String {
1068 if let Some(inner) = description
1069 .strip_prefix('[')
1070 .and_then(|trimmed| trimmed.strip_suffix(']'))
1071 {
1072 let names = inner
1073 .split(',')
1074 .filter(|name| !name.is_empty())
1075 .collect_vec();
1076 if names.is_empty() {
1077 return description.bold().green().to_string();
1078 }
1079
1080 let rendered = names
1081 .into_iter()
1082 .enumerate()
1083 .map(|(index, name)| {
1084 if index == 0 {
1085 name.bold().green().to_string()
1086 } else {
1087 name.bold().blue().to_string()
1088 }
1089 })
1090 .join(",");
1091 return format!("[{rendered}]");
1092 }
1093
1094 description.bold().green().to_string()
1095}
1096
1097fn render_bin_description(axis_label: &str, description: &str) -> String {
1098 match axis_label {
1099 "orientation" => render_orientation_description(description),
1100 "graph" => render_graph_description(description),
1101 _ => description.bold().green().to_string(),
1102 }
1103}
1104
1105fn contribution_bin_label(integration_state: &IntegrationState, bin_index: usize) -> String {
1106 if let (Some(axis_label), Some(descriptions)) = (
1107 integration_state
1108 .first_non_trivial_discrete_label
1109 .as_deref(),
1110 integration_state
1111 .first_non_trivial_discrete_bin_descriptions
1112 .as_ref(),
1113 ) && let Some(description) = descriptions.get(bin_index)
1114 {
1115 return format!(
1116 "#{bin_index}: {}",
1117 render_bin_description(axis_label, description)
1118 );
1119 }
1120
1121 format!("#{bin_index}")
1122}
1123
1124fn contribution_header_label(
1125 integration_state: &IntegrationState,
1126 discrete_monitoring_enabled: bool,
1127) -> String {
1128 if discrete_monitoring_enabled
1129 && let Some(label) = integration_state
1130 .first_non_trivial_discrete_label
1131 .as_deref()
1132 {
1133 return format!("Contribution (idx={label})")
1134 .bold()
1135 .blue()
1136 .to_string();
1137 }
1138
1139 "Contribution".bold().blue().to_string()
1140}
1141
1142fn format_percentage_sig(value: f64, significant_digits: usize) -> String {
1143 if !value.is_finite() {
1144 return "None".red().to_string();
1145 }
1146
1147 if value == 0.0 {
1148 let decimals = significant_digits.saturating_sub(1);
1149 return format!("{:.*}%", decimals, 0.0);
1150 }
1151
1152 let abs_value = value.abs();
1153 let exponent = abs_value.log10().floor() as i32;
1154 if exponent < -2 || exponent >= significant_digits as i32 {
1155 return format!("{:.*e}%", significant_digits.saturating_sub(1), value);
1156 }
1157
1158 let decimals = (significant_digits as i32 - exponent - 1).max(0) as usize;
1159 format!("{value:.decimals$}%")
1160}
1161
1162#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1163enum UncertaintyNotation {
1164 Dynamic,
1165 Scientific,
1166}
1167
1168fn format_significant_percentage(
1169 value: f64,
1170 significant_digits: usize,
1171 scientific_threshold: Option<(f64, f64)>,
1172) -> String {
1173 if !value.is_finite() {
1174 return "None".red().to_string();
1175 }
1176
1177 if value == 0.0 {
1178 return format!("{:.*}%", significant_digits.saturating_sub(1), 0.0);
1179 }
1180
1181 let abs_value = value.abs();
1182 if scientific_threshold.is_some_and(|(upper, lower)| abs_value >= upper || abs_value < lower) {
1183 return format!("{:.*e}%", significant_digits.saturating_sub(1), value);
1184 }
1185
1186 let exponent = abs_value.log10().floor() as i32;
1187 let decimals = (significant_digits as i32 - exponent - 1).max(0) as usize;
1188 format!("{value:.decimals$}%")
1189}
1190
1191fn should_use_scientific_uncertainty_notation(avg: F<f64>, err: F<f64>) -> bool {
1192 let avg_abs = avg.abs().0;
1193 let err_abs = err.abs().0;
1194 avg_abs >= 1e6
1195 || (avg_abs != 0.0 && avg_abs < 1e-5)
1196 || (avg.is_zero() && !(1e-4..1e5).contains(&err_abs))
1197}
1198
1199fn format_uncertainty_with_notation(
1200 avg: F<f64>,
1201 err: F<f64>,
1202 notation: UncertaintyNotation,
1203) -> String {
1204 if !matches!(notation, UncertaintyNotation::Scientific)
1205 && !should_use_scientific_uncertainty_notation(avg, err)
1206 {
1207 return utils::format_uncertainty(avg, err);
1208 }
1209
1210 if !avg.0.is_finite() || !err.0.is_finite() {
1211 return utils::format_uncertainty(avg, err);
1212 }
1213
1214 let exponent = if avg.is_non_zero() {
1215 avg.abs().0.log10().floor() as i32
1216 } else if err.is_non_zero() {
1217 err.abs().0.log10().floor() as i32
1218 } else {
1219 0
1220 };
1221 let scale = 10_f64.powi(exponent);
1222 let mantissa = utils::format_uncertainty(F(avg.0 / scale), F(err.0 / scale));
1223 format!("{mantissa}e{exponent}")
1224}
1225
1226fn format_signed_uncertainty(avg: F<f64>, err: F<f64>, notation: UncertaintyNotation) -> String {
1227 let formatted = format_uncertainty_with_notation(avg, err, notation);
1228 if avg.0.is_sign_negative() {
1229 formatted
1230 } else {
1231 format!("+{formatted}")
1232 }
1233}
1234
1235fn format_abbreviated_count(value: usize) -> String {
1236 if value < 1_000 {
1237 return value.to_string();
1238 }
1239
1240 let value = value as f64;
1241 if value < 1_000_000.0 {
1242 return format!("{:.2}K", value / 1_000.0);
1243 }
1244 if value < 1_000_000_000.0 {
1245 return format!("{:.2}M", value / 1_000_000.0);
1246 }
1247 if value < 1_000_000_000_000.0 {
1248 return format!("{:.2}B", value / 1_000_000_000.0);
1249 }
1250
1251 format!("{:.3}T", value / 1_000_000_000_000.0)
1252}
1253
1254fn build_integral_result_cells(
1255 itg: &StatisticsAccumulator<F<f64>>,
1256 slot_meta: &SlotMeta,
1257 i_iter: usize,
1258 tag: &str,
1259 trgt: Option<F<f64>>,
1260) -> IntegralResultCells {
1261 let relative_error = format_relative_error_cell(itg);
1262 let chi_sq = format_chi_sq_cell(itg, i_iter);
1263 let (delta_sigma, delta_percent) = format_delta_cells(itg, trgt);
1264 let mwi = format_mwi_cell(itg);
1265
1266 IntegralResultCells {
1267 integrand: format!(
1268 "{} {}:",
1269 slot_label(slot_meta),
1270 format!("{:-2}", tag).blue().bold(),
1271 ),
1272 value: format_signed_uncertainty(itg.avg, itg.err, UncertaintyNotation::Scientific)
1273 .blue()
1274 .bold()
1275 .to_string(),
1276 relative_error,
1277 chi_sq,
1278 delta_sigma,
1279 delta_percent,
1280 mwi,
1281 }
1282}
1283
1284fn format_relative_error_cell(itg: &StatisticsAccumulator<F<f64>>) -> String {
1285 format_relative_error_from_estimate(itg.avg, itg.err)
1286}
1287
1288fn format_relative_error_from_estimate(avg: F<f64>, err: F<f64>) -> String {
1289 if avg.is_zero() {
1290 return String::new();
1291 }
1292
1293 let formatted =
1294 format_significant_percentage((err / avg).abs().0 * 100.0, 3, Some((1.0e4, 1.0e-4)));
1295 if (err / avg).abs().0 > 0.01 {
1296 formatted.red().to_string()
1297 } else {
1298 formatted.green().to_string()
1299 }
1300}
1301
1302fn format_chi_sq_cell(itg: &StatisticsAccumulator<F<f64>>, i_iter: usize) -> String {
1303 let chi_sq = format!("{:.3}", itg.chi_sq.0 / (i_iter as f64));
1304 if itg.chi_sq / F::<f64>::new_from_usize(i_iter) > F(5.) {
1305 chi_sq.red().to_string()
1306 } else {
1307 chi_sq
1308 }
1309}
1310
1311fn format_delta_cells(
1312 itg: &StatisticsAccumulator<F<f64>>,
1313 trgt: Option<F<f64>>,
1314) -> (Option<String>, Option<String>) {
1315 format_delta_cells_from_estimate(itg.avg, itg.err, trgt)
1316}
1317
1318fn format_delta_cells_from_estimate(
1319 avg: F<f64>,
1320 err: F<f64>,
1321 trgt: Option<F<f64>>,
1322) -> (Option<String>, Option<String>) {
1323 let Some(t) = trgt else {
1324 return (None, None);
1325 };
1326
1327 let delta_in_sigmas = if err.is_zero() {
1328 0.0
1329 } else {
1330 (t - avg).abs().0 / err.0
1331 };
1332 let delta_in_percent = if t.abs().is_non_zero() {
1333 (t - avg).abs().0 / t.abs().0 * 100.
1334 } else {
1335 0.
1336 };
1337 let is_outside_target =
1338 delta_in_sigmas > 5. || (t.abs().is_non_zero() && ((t - avg).abs() / t.abs()).0 > 0.01);
1339 let sigma_text = format!("Δ = {:.3}σ", delta_in_sigmas);
1340 let percent_text = format!("Δ = {:.3}%", delta_in_percent);
1341 if is_outside_target {
1342 (
1343 Some(sigma_text.red().to_string()),
1344 Some(percent_text.red().to_string()),
1345 )
1346 } else {
1347 (
1348 Some(sigma_text.green().to_string()),
1349 Some(percent_text.green().to_string()),
1350 )
1351 }
1352}
1353
1354fn format_mwi_cell(itg: &StatisticsAccumulator<F<f64>>) -> String {
1355 let mwi_value = max_weight_impact(itg);
1356 let formatted = format!("{:.4e}", mwi_value.0);
1357 if mwi_value > F(1.) {
1358 formatted.red().to_string()
1359 } else {
1360 formatted
1361 }
1362}
1363
1364fn max_weight_impact(itg: &StatisticsAccumulator<F<f64>>) -> F<f64> {
1365 if itg.avg.abs().0 == 0. || itg.processed_samples == 0 {
1366 return F(0.0);
1367 }
1368
1369 itg.max_eval_negative.abs().max(itg.max_eval_positive.abs())
1370 / (itg.avg.abs() * F::<f64>::new_from_usize(itg.processed_samples))
1371}
1372
1373fn build_table_result_summary(
1374 slot_meta: &SlotMeta,
1375 accumulator: &ComplexAccumulator,
1376 iter: usize,
1377 target: Option<Complex<F<f64>>>,
1378) -> Vec<IntegrationTableComponentResult> {
1379 [
1380 ("re", &accumulator.re, target.as_ref().map(|value| value.re)),
1381 ("im", &accumulator.im, target.as_ref().map(|value| value.im)),
1382 ]
1383 .into_iter()
1384 .map(|(component, accumulator, target_component)| {
1385 let cells =
1386 build_integral_result_cells(accumulator, slot_meta, iter, component, target_component);
1387 IntegrationTableComponentResult {
1388 component: component.to_string(),
1389 value: accumulator.avg,
1390 error: accumulator.err,
1391 relative_error_percent: accumulator
1392 .avg
1393 .is_non_zero()
1394 .then(|| (accumulator.err / accumulator.avg).abs().0 * 100.0),
1395 chi_sq_per_dof: if iter > 0 {
1396 accumulator.chi_sq.0 / (iter as f64)
1397 } else {
1398 0.0
1399 },
1400 target_delta_sigma: cells.delta_sigma.as_ref().map(|_| {
1401 if accumulator.err.is_zero() {
1402 0.0
1403 } else if let Some(target_value) = target_component {
1404 (target_value - accumulator.avg).abs().0 / accumulator.err.0
1405 } else {
1406 0.0
1407 }
1408 }),
1409 target_delta_percent: target_component.map(|target_value| {
1410 if target_value.is_zero() {
1411 0.0
1412 } else {
1413 (target_value - accumulator.avg).abs().0 / target_value.abs().0 * 100.0
1414 }
1415 }),
1416 max_weight_impact: max_weight_impact(accumulator).0,
1417 }
1418 })
1419 .collect()
1420}
1421
1422fn build_max_weight_info_summary(
1423 discrete_axis_labels: &[String],
1424 accumulator: &ComplexAccumulator,
1425) -> Vec<MaxWeightInfoEntry> {
1426 [
1427 (
1428 "re",
1429 "+",
1430 accumulator.re.max_eval_positive,
1431 accumulator.re.max_eval_positive_xs.as_ref(),
1432 ),
1433 (
1434 "re",
1435 "-",
1436 accumulator.re.max_eval_negative,
1437 accumulator.re.max_eval_negative_xs.as_ref(),
1438 ),
1439 (
1440 "im",
1441 "+",
1442 accumulator.im.max_eval_positive,
1443 accumulator.im.max_eval_positive_xs.as_ref(),
1444 ),
1445 (
1446 "im",
1447 "-",
1448 accumulator.im.max_eval_negative,
1449 accumulator.im.max_eval_negative_xs.as_ref(),
1450 ),
1451 ]
1452 .into_iter()
1453 .filter_map(|(component, sign, max_eval, sample)| {
1454 if max_eval.is_zero() {
1455 return None;
1456 }
1457
1458 Some(MaxWeightInfoEntry {
1459 component: component.to_string(),
1460 sign: sign.to_string(),
1461 max_eval,
1462 coordinates: sample
1463 .map(|sample| format_max_eval_sample(sample, discrete_axis_labels, &[])),
1464 })
1465 })
1466 .collect()
1467}
1468
1469#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1470struct LiveIterationProgress {
1471 completed_points: usize,
1472 target_points: usize,
1473}
1474
1475fn build_iteration_status_header_left(
1476 elapsed_time: Duration,
1477 iter: usize,
1478 live_progress: Option<LiveIterationProgress>,
1479) -> String {
1480 let iteration_label = if live_progress.is_some() {
1481 format!("Iteration #{:-4} ( running )", iter)
1482 .bold()
1483 .green()
1484 } else {
1485 format!("Iteration #{:-4} ( completed )", iter)
1486 .bold()
1487 .green()
1488 };
1489
1490 format!(
1491 "[ {} ] {}",
1492 format!(
1493 "{:^7}",
1494 utils::format_wdhms(elapsed_time.as_secs() as usize)
1495 )
1496 .bold(),
1497 iteration_label,
1498 )
1499}
1500
1501fn build_iteration_status_header_middle(
1502 cur_points: usize,
1503 total_points: usize,
1504 live_progress: Option<LiveIterationProgress>,
1505) -> String {
1506 let per_iteration = if let Some(progress) = live_progress {
1507 let percentage = if progress.target_points == 0 {
1508 String::from("0.0%")
1509 } else {
1510 format!(
1511 "{:.1}%",
1512 (progress.completed_points as f64) / (progress.target_points as f64) * 100.0
1513 )
1514 .green()
1515 .to_string()
1516 };
1517 format!(
1518 "Iteration progress {}/{} {}",
1519 format_iteration_points(progress.completed_points),
1520 format_iteration_points(progress.target_points),
1521 percentage
1522 )
1523 } else if cur_points > 0 {
1524 format!(
1525 "# samples per iteration = {}",
1526 format_iteration_points(cur_points)
1527 )
1528 .blue()
1529 .bold()
1530 .to_string()
1531 } else {
1532 String::new()
1533 };
1534
1535 let total = format!("# samples total = {}", format_total_points(total_points))
1536 .bold()
1537 .green()
1538 .to_string();
1539 if per_iteration.is_empty() {
1540 total
1541 } else {
1542 format!("{per_iteration} {total}")
1543 }
1544}
1545
1546fn build_iteration_status_header_tail(
1547 cores: usize,
1548 elapsed_time: Duration,
1549 n_samples_evaluated: usize,
1550) -> String {
1551 let sample_core_time = if n_samples_evaluated == 0 {
1552 "N/A /sample/core".red().to_string()
1553 } else {
1554 format!(
1555 "{} /sample/core",
1556 utils::format_evaluation_time_from_f64(
1557 elapsed_time.as_secs_f64() / (n_samples_evaluated as f64) * (cores as f64),
1558 )
1559 .bold()
1560 .green()
1561 )
1562 };
1563
1564 format!(
1565 "{sample_core_time} {}",
1566 format!("({cores} cores)").bold().blue()
1567 )
1568}
1569
1570fn max_weight_row_descriptors(
1571 phase_display: IntegrationStatusPhaseDisplay,
1572) -> Vec<(ComponentKind, &'static str, bool)> {
1573 let mut rows = Vec::new();
1574 if phase_display.shows_real() {
1575 rows.push((ComponentKind::Real, "+", true));
1576 rows.push((ComponentKind::Real, "-", false));
1577 }
1578 if phase_display.shows_imag() {
1579 rows.push((ComponentKind::Imag, "+", true));
1580 rows.push((ComponentKind::Imag, "-", false));
1581 }
1582 rows
1583}
1584
1585type MaxEvalEntry<'a> = Option<(F<f64>, Option<&'a Sample<F<f64>>>)>;
1586
1587fn max_eval_entry(accumulator: &StatisticsAccumulator<F<f64>>, positive: bool) -> MaxEvalEntry<'_> {
1588 let (value, sample) = if positive {
1589 (
1590 accumulator.max_eval_positive,
1591 accumulator.max_eval_positive_xs.as_ref(),
1592 )
1593 } else {
1594 (
1595 accumulator.max_eval_negative,
1596 accumulator.max_eval_negative_xs.as_ref(),
1597 )
1598 };
1599
1600 if value.is_zero() {
1601 None
1602 } else {
1603 Some((value, sample))
1604 }
1605}
1606
1607#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
1610pub struct IntegrationState {
1611 pub num_points: usize,
1612 #[bincode(with_serde)]
1613 pub all_integrals: Vec<ComplexAccumulator>,
1614 #[bincode(with_serde)]
1615 slot_re_summaries: Vec<Option<DiscreteGridAccumulatorSummary>>,
1616 #[bincode(with_serde)]
1617 slot_im_summaries: Vec<Option<DiscreteGridAccumulatorSummary>>,
1618 pub stats: StatisticsCounter,
1619 pub slot_stats: Vec<StatisticsCounter>,
1620 pub slot_metas: Vec<SlotMeta>,
1621 pub sampling_correlation_mode: SamplingCorrelationMode,
1622 sampling_states: Vec<SamplingSlotState>,
1623 monitored_discrete_path: Option<Vec<usize>>,
1624 pub first_non_trivial_discrete_label: Option<String>,
1625 pub first_non_trivial_discrete_bin_descriptions: Option<Vec<String>>,
1626 slot_first_non_trivial_discrete_breakdown_metadata:
1627 Vec<Option<PersistedDiscreteBreakdownMetadata>>,
1628 pub iter: usize,
1629 pub elapsed_seconds: f64,
1630 pub n_cores: usize,
1631}
1632
1633impl IntegrationState {
1634 fn new_from_settings(
1635 sampling_correlation_mode: SamplingCorrelationMode,
1636 sampling_states: Vec<SamplingSlotState>,
1637 slot_metas: Vec<SlotMeta>,
1638 monitored_discrete_path: Option<Vec<usize>>,
1639 first_non_trivial_discrete_label: Option<String>,
1640 first_non_trivial_discrete_bin_descriptions: Option<Vec<String>>,
1641 slot_first_non_trivial_discrete_breakdown_metadata: Vec<
1642 Option<PersistedDiscreteBreakdownMetadata>,
1643 >,
1644 ) -> Self {
1645 let num_points = 0;
1646 let iter = 0;
1647 let all_integrals = vec![ComplexAccumulator::new(); slot_metas.len()];
1648 let slot_re_summaries = (0..slot_metas.len())
1649 .map(|slot_index| {
1650 DiscreteGridAccumulatorSummary::from_grid(
1651 &sampling_states[sampling_correlation_mode.state_index(slot_index)].grid,
1652 )
1653 })
1654 .collect();
1655 let slot_im_summaries = (0..slot_metas.len())
1656 .map(|slot_index| {
1657 DiscreteGridAccumulatorSummary::from_grid(
1658 &sampling_states[sampling_correlation_mode.state_index(slot_index)].grid,
1659 )
1660 })
1661 .collect();
1662 let stats = StatisticsCounter::new_empty();
1663 let slot_stats = vec![StatisticsCounter::new_empty(); slot_metas.len()];
1664
1665 Self {
1666 num_points,
1667 all_integrals,
1668 slot_re_summaries,
1669 slot_im_summaries,
1670 stats,
1671 slot_stats,
1672 slot_metas,
1673 sampling_correlation_mode,
1674 sampling_states,
1675 monitored_discrete_path,
1676 first_non_trivial_discrete_label,
1677 first_non_trivial_discrete_bin_descriptions,
1678 slot_first_non_trivial_discrete_breakdown_metadata,
1679 iter,
1680 elapsed_seconds: 0.0,
1681 n_cores: 1,
1682 }
1683 }
1684
1685 fn update_iter(&mut self, use_weighted_average: bool) {
1686 self.all_integrals
1687 .iter_mut()
1688 .for_each(|acc| acc.update_iter(use_weighted_average));
1689 }
1690
1691 fn sampling_state_for_slot(&self, slot_index: usize) -> &SamplingSlotState {
1692 &self.sampling_states[self.sampling_correlation_mode.state_index(slot_index)]
1693 }
1694
1695 fn sampling_state_for_slot_mut(&mut self, slot_index: usize) -> &mut SamplingSlotState {
1696 let sampling_state_index = self.sampling_correlation_mode.state_index(slot_index);
1697 &mut self.sampling_states[sampling_state_index]
1698 }
1699
1700 fn monitored_discrete_context_for_slot(
1701 &self,
1702 slot_index: usize,
1703 ) -> Option<DiscreteLevelContext> {
1704 let path = self.monitored_discrete_path.clone()?;
1705 let discrete_grid =
1706 discrete_grid_at_path(&self.sampling_state_for_slot(slot_index).grid, &path)?;
1707 Some(DiscreteLevelContext {
1708 path,
1709 pdfs: discrete_grid.bins.iter().map(|bin| bin.pdf).collect(),
1710 })
1711 }
1712
1713 fn monitored_discrete_context(&self) -> Option<DiscreteLevelContext> {
1714 self.monitored_discrete_context_for_slot(0)
1715 }
1716}
1717
1718struct CoreIterationState {
1719 slot_integrands: Vec<Integrand>,
1720 stats: StatisticsCounter,
1721 slot_stats: Vec<StatisticsCounter>,
1722 integrals: Vec<ComplexAccumulator>,
1723 sampling_correlation_mode: SamplingCorrelationMode,
1724 sampling_states: Vec<CoreSamplingSlotState>,
1725 slot_re_grids: Vec<Grid<F<f64>>>,
1726 slot_im_grids: Vec<Grid<F<f64>>>,
1727 remaining_points: usize,
1728 completed_points: usize,
1729}
1730
1731struct CoreSamplingSlotState {
1732 sampling_grid: Grid<F<f64>>,
1733 rng: MonteCarloRng,
1734}
1735
1736impl CoreIterationState {
1737 fn new(
1738 slot_integrands: Vec<Integrand>,
1739 sampling_correlation_mode: SamplingCorrelationMode,
1740 sampling_grid_templates: &[Grid<F<f64>>],
1741 seed: u64,
1742 sample_skip: usize,
1743 remaining_points: usize,
1744 ) -> Self {
1745 let n_slots = slot_integrands.len();
1746 let sampling_states = (0..sampling_correlation_mode.state_count(n_slots))
1747 .map(|sampling_state_index| {
1748 let slot_index = match sampling_correlation_mode {
1749 SamplingCorrelationMode::Correlated => 0,
1750 SamplingCorrelationMode::Uncorrelated => sampling_state_index,
1751 };
1752 let mut rng = MonteCarloRng::new(slot_seed(seed, slot_index), 0);
1753 let mut sampling_grid =
1754 sampling_grid_templates[sampling_state_index].clone_without_samples();
1755
1756 for _ in 0..sample_skip {
1757 let mut sample = Sample::new();
1758 sampling_grid.sample(&mut rng, &mut sample);
1759 }
1760
1761 CoreSamplingSlotState { sampling_grid, rng }
1762 })
1763 .collect_vec();
1764
1765 Self {
1766 slot_integrands,
1767 stats: StatisticsCounter::new_empty(),
1768 slot_stats: vec![StatisticsCounter::new_empty(); n_slots],
1769 integrals: vec![ComplexAccumulator::new(); n_slots],
1770 sampling_correlation_mode,
1771 sampling_states,
1772 slot_re_grids: (0..n_slots)
1773 .map(|slot_index| {
1774 sampling_grid_templates[sampling_correlation_mode.state_index(slot_index)]
1775 .clone_without_samples()
1776 })
1777 .collect(),
1778 slot_im_grids: (0..n_slots)
1779 .map(|slot_index| {
1780 sampling_grid_templates[sampling_correlation_mode.state_index(slot_index)]
1781 .clone_without_samples()
1782 })
1783 .collect(),
1784 remaining_points,
1785 completed_points: 0,
1786 }
1787 }
1788
1789 fn evaluate_chunk(
1790 &mut self,
1791 slot_settings: &[&RuntimeSettings],
1792 slot_models: &[&Model],
1793 iter: usize,
1794 current_max_evals: &[Complex<F<f64>>],
1795 chunk_size: usize,
1796 ) -> Result<usize> {
1797 let n_points = chunk_size.min(self.remaining_points);
1798 if n_points == 0 {
1799 return Ok(0);
1800 }
1801
1802 let chunk_start = Instant::now();
1803 let mut batch_evaluation_time = Duration::ZERO;
1804 let mut batch_stats = StatisticsCounter::new_empty();
1805 let mut processed_points = 0;
1806 let mut total_sample_evaluations = 0;
1807
1808 match self.sampling_correlation_mode {
1809 SamplingCorrelationMode::Correlated => {
1810 let mut samples = Vec::with_capacity(n_points);
1811 for _ in 0..n_points {
1812 if is_interrupted() {
1813 break;
1814 }
1815
1816 let mut sample = Sample::new();
1817 let sampling_state = &mut self.sampling_states[0];
1818 sampling_state
1819 .sampling_grid
1820 .sample(&mut sampling_state.rng, &mut sample);
1821 processed_points += 1;
1822 samples.push(sample);
1823 }
1824
1825 if processed_points == 0 {
1826 return Ok(0);
1827 }
1828
1829 let mut slot_results = Vec::with_capacity(self.slot_integrands.len());
1830
1831 for (slot_index, integrand) in self.slot_integrands.iter_mut().enumerate() {
1832 let evaluation_start = Instant::now();
1833 let raw_batch = integrand.evaluate_samples_raw(
1834 &samples,
1835 slot_models[slot_index],
1836 iter,
1837 false,
1838 true,
1839 current_max_evals[slot_index],
1840 )?;
1841 if raw_batch.samples.len() < samples.len() {
1842 return Ok(0);
1843 }
1844 batch_evaluation_time += evaluation_start.elapsed();
1845 total_sample_evaluations += raw_batch.samples.len();
1846 batch_stats = batch_stats.merged(&raw_batch.statistics);
1847 self.slot_stats[slot_index] =
1848 self.slot_stats[slot_index].merged(&raw_batch.statistics);
1849 slot_results.push(raw_batch.samples);
1850 }
1851
1852 for (sample_index, sample) in samples.iter().enumerate() {
1853 for (slot_index, (((core_accumulator, re_grid), im_grid), results)) in self
1854 .integrals
1855 .iter_mut()
1856 .zip(self.slot_re_grids.iter_mut())
1857 .zip(self.slot_im_grids.iter_mut())
1858 .zip(slot_results.iter())
1859 .enumerate()
1860 {
1861 let result = &results[sample_index];
1862 let jacobian = result.parameterization_jacobian.unwrap_or(F(1.0));
1863 let effective_integrand_result =
1864 result.integrand_result * Complex::new_re(jacobian);
1865
1866 core_accumulator.add_sample(
1867 effective_integrand_result,
1868 sample.get_weight(),
1869 Some(sample),
1870 );
1871 re_grid
1872 .add_training_sample(sample, effective_integrand_result.re)
1873 .map_err(Report::msg)?;
1874 im_grid
1875 .add_training_sample(sample, effective_integrand_result.im)
1876 .map_err(Report::msg)?;
1877
1878 if slot_index == 0 {
1879 let training_eval = match slot_settings[0].integrator.integrated_phase {
1880 IntegratedPhase::Real => effective_integrand_result.re,
1881 IntegratedPhase::Imag => effective_integrand_result.im,
1882 IntegratedPhase::Both => unimplemented!(),
1883 };
1884
1885 self.sampling_states[0]
1886 .sampling_grid
1887 .add_training_sample(sample, training_eval)
1888 .map_err(Report::msg)?;
1889 }
1890 }
1891 }
1892 }
1893 SamplingCorrelationMode::Uncorrelated => {
1894 for slot_index in 0..self.slot_integrands.len() {
1895 let mut samples = Vec::with_capacity(n_points);
1896 for _ in 0..n_points {
1897 if is_interrupted() {
1898 break;
1899 }
1900
1901 let mut sample = Sample::new();
1902 let sampling_state = &mut self.sampling_states[slot_index];
1903 sampling_state
1904 .sampling_grid
1905 .sample(&mut sampling_state.rng, &mut sample);
1906 samples.push(sample);
1907 }
1908
1909 if slot_index == 0 {
1910 processed_points = samples.len();
1911 } else if samples.len() != processed_points {
1912 return Err(Report::msg(
1913 "uncorrelated slot batch sizes diverged unexpectedly",
1914 ));
1915 }
1916
1917 if samples.is_empty() {
1918 continue;
1919 }
1920
1921 let evaluation_start = Instant::now();
1922 let raw_batch = self.slot_integrands[slot_index].evaluate_samples_raw(
1923 &samples,
1924 slot_models[slot_index],
1925 iter,
1926 false,
1927 true,
1928 current_max_evals[slot_index],
1929 )?;
1930 if raw_batch.samples.len() < samples.len() {
1931 return Ok(0);
1932 }
1933 batch_evaluation_time += evaluation_start.elapsed();
1934 total_sample_evaluations += raw_batch.samples.len();
1935 batch_stats = batch_stats.merged(&raw_batch.statistics);
1936 self.slot_stats[slot_index] =
1937 self.slot_stats[slot_index].merged(&raw_batch.statistics);
1938
1939 for (sample, result) in samples.iter().zip(raw_batch.samples.iter()) {
1940 let jacobian = result.parameterization_jacobian.unwrap_or(F(1.0));
1941 let effective_integrand_result =
1942 result.integrand_result * Complex::new_re(jacobian);
1943
1944 self.integrals[slot_index].add_sample(
1945 effective_integrand_result,
1946 sample.get_weight(),
1947 Some(sample),
1948 );
1949 self.slot_re_grids[slot_index]
1950 .add_training_sample(sample, effective_integrand_result.re)
1951 .map_err(Report::msg)?;
1952 self.slot_im_grids[slot_index]
1953 .add_training_sample(sample, effective_integrand_result.im)
1954 .map_err(Report::msg)?;
1955
1956 let training_eval =
1957 match slot_settings[slot_index].integrator.integrated_phase {
1958 IntegratedPhase::Real => effective_integrand_result.re,
1959 IntegratedPhase::Imag => effective_integrand_result.im,
1960 IntegratedPhase::Both => unimplemented!(),
1961 };
1962 self.sampling_states[slot_index]
1963 .sampling_grid
1964 .add_training_sample(sample, training_eval)
1965 .map_err(Report::msg)?;
1966 }
1967 }
1968
1969 if processed_points == 0 {
1970 return Ok(0);
1971 }
1972 }
1973 }
1974
1975 self.remaining_points -= processed_points;
1976 self.completed_points += processed_points;
1977 self.stats = self.stats.merged(&batch_stats);
1978 self.stats.add_integrator_overhead(
1979 chunk_start.elapsed().saturating_sub(batch_evaluation_time),
1980 total_sample_evaluations,
1981 );
1982
1983 Ok(processed_points)
1984 }
1985}
1986
1987fn initial_batch_size(batching: IterationBatchingSettings) -> usize {
1988 batching.batch_size.unwrap_or(100).max(1)
1989}
1990
1991fn next_batch_size(
1992 batching: IterationBatchingSettings,
1993 current_batch_size: usize,
1994 round_elapsed: Duration,
1995) -> usize {
1996 if let Some(batch_size) = batching.batch_size {
1997 return batch_size.max(1);
1998 }
1999
2000 let round_seconds = round_elapsed.as_secs_f64();
2001 if round_seconds <= 0.0 {
2002 return current_batch_size.max(1);
2003 }
2004
2005 let estimated = ((current_batch_size as f64) * batching.batch_timing_seconds / round_seconds)
2006 .round() as usize;
2007 estimated.max(1)
2008}
2009
2010fn total_completed_points(core_states: &[CoreIterationState]) -> usize {
2011 core_states.iter().map(|state| state.completed_points).sum()
2012}
2013
2014fn total_remaining_points(core_states: &[CoreIterationState]) -> usize {
2015 core_states.iter().map(|state| state.remaining_points).sum()
2016}
2017
2018fn slot_seed(global_seed: u64, slot_index: usize) -> u64 {
2019 let slot = (slot_index as u64).wrapping_add(1);
2020 let mut mixed = global_seed.wrapping_add(0x9e37_79b9_7f4a_7c15u64.wrapping_mul(slot));
2021 mixed ^= mixed >> 30;
2022 mixed = mixed.wrapping_mul(0xbf58_476d_1ce4_e5b9);
2023 mixed ^= mixed >> 27;
2024 mixed = mixed.wrapping_mul(0x94d0_49bb_1331_11eb);
2025 mixed ^ (mixed >> 31)
2026}
2027
2028fn apply_iteration_core_states(
2029 integration_state: &mut IntegrationState,
2030 slots: &[IntegrationSlot],
2031 cores: usize,
2032 cur_points: usize,
2033 elapsed_seconds: f64,
2034 core_states: &[CoreIterationState],
2035) {
2036 let n_slots = integration_state.slot_metas.len();
2037
2038 for core_state in core_states {
2039 integration_state.stats = integration_state.stats.merged(&core_state.stats);
2040 for (integral, core_integral) in integration_state
2041 .all_integrals
2042 .iter_mut()
2043 .zip(core_state.integrals.iter())
2044 {
2045 integral.merge(core_integral);
2046 }
2047 for (slot_stats, core_slot_stats) in integration_state
2048 .slot_stats
2049 .iter_mut()
2050 .zip(core_state.slot_stats.iter())
2051 {
2052 *slot_stats = slot_stats.merged(core_slot_stats);
2053 }
2054 }
2055
2056 let mut merged_sampling_grids = integration_state
2057 .sampling_states
2058 .iter()
2059 .map(|sampling_state| sampling_state.grid.clone_without_samples())
2060 .collect_vec();
2061 let mut merged_re_grids = (0..n_slots)
2062 .map(|slot_index| {
2063 integration_state
2064 .sampling_state_for_slot(slot_index)
2065 .grid
2066 .clone_without_samples()
2067 })
2068 .collect_vec();
2069 let mut merged_im_grids = (0..n_slots)
2070 .map(|slot_index| {
2071 integration_state
2072 .sampling_state_for_slot(slot_index)
2073 .grid
2074 .clone_without_samples()
2075 })
2076 .collect_vec();
2077
2078 for core_state in core_states {
2079 for (sampling_state, merged_grid) in core_state
2080 .sampling_states
2081 .iter()
2082 .zip(merged_sampling_grids.iter_mut())
2083 {
2084 merged_grid
2085 .merge(&sampling_state.sampling_grid)
2086 .expect("could not merge grids");
2087 }
2088 for slot_index in 0..n_slots {
2089 merged_re_grids[slot_index]
2090 .merge(&core_state.slot_re_grids[slot_index])
2091 .expect("could not merge real accumulation grids");
2092 merged_im_grids[slot_index]
2093 .merge(&core_state.slot_im_grids[slot_index])
2094 .expect("could not merge imaginary accumulation grids");
2095 }
2096 }
2097
2098 for (summary, grid) in integration_state
2099 .slot_re_summaries
2100 .iter_mut()
2101 .zip(merged_re_grids.iter())
2102 {
2103 if let Some(summary) = summary.as_mut() {
2104 summary.merge_iteration_grid(grid);
2105 summary.update_iter();
2106 }
2107 }
2108 for (summary, grid) in integration_state
2109 .slot_im_summaries
2110 .iter_mut()
2111 .zip(merged_im_grids.iter())
2112 {
2113 if let Some(summary) = summary.as_mut() {
2114 summary.merge_iteration_grid(grid);
2115 summary.update_iter();
2116 }
2117 }
2118
2119 for (slot_index, merged_grid) in merged_sampling_grids.into_iter().enumerate() {
2120 let actual_slot_index = match integration_state.sampling_correlation_mode {
2121 SamplingCorrelationMode::Correlated => 0,
2122 SamplingCorrelationMode::Uncorrelated => slot_index,
2123 };
2124 let discrete_axis_labels = integration_state
2125 .sampling_state_for_slot(actual_slot_index)
2126 .discrete_axis_labels
2127 .clone();
2128 let sampling_state = integration_state.sampling_state_for_slot_mut(actual_slot_index);
2129 *sampling_state = SamplingSlotState {
2130 grid: merged_grid,
2131 discrete_axis_labels,
2132 };
2133 sampling_state.grid.update(
2134 F(slots[actual_slot_index]
2135 .settings
2136 .integrator
2137 .discrete_dim_learning_rate),
2138 F(slots[actual_slot_index]
2139 .settings
2140 .integrator
2141 .continuous_dim_learning_rate),
2142 );
2143 }
2144
2145 integration_state.update_iter(false);
2146 integration_state.iter += 1;
2147 integration_state.elapsed_seconds = elapsed_seconds;
2148 integration_state.n_cores = cores;
2149 integration_state.num_points += cur_points;
2150}
2151
2152fn build_preview_integration_state(
2153 integration_state: &IntegrationState,
2154 slots: &[IntegrationSlot],
2155 cores: usize,
2156 completed_points: usize,
2157 elapsed_seconds: f64,
2158 core_states: &[CoreIterationState],
2159) -> IntegrationState {
2160 let mut preview = integration_state.clone();
2161 apply_iteration_core_states(
2162 &mut preview,
2163 slots,
2164 cores,
2165 completed_points,
2166 elapsed_seconds,
2167 core_states,
2168 );
2169 preview
2170}
2171
2172pub fn havana_integrate<S>(
2174 request: HavanaIntegrateRequest,
2175 mut status_emitter: S,
2176) -> Result<IntegrationResult>
2177where
2178 S: FnMut(StatusUpdate) -> Result<()>,
2179{
2180 let HavanaIntegrateRequest {
2181 mut slots,
2182 sampling_correlation_mode,
2183 n_cores,
2184 state,
2185 workspace,
2186 output_control,
2187 batching,
2188 view_options,
2189 } = request;
2190
2191 if slots.is_empty() {
2192 return Err(Report::msg(
2193 "At least one integrand must be selected for integration",
2194 ));
2195 }
2196
2197 let slot_metas = slots.iter().map(|slot| slot.meta.clone()).collect_vec();
2198 let targets = slots.iter().map(|slot| slot.target).collect_vec();
2199 let sampling_states = match sampling_correlation_mode {
2200 SamplingCorrelationMode::Correlated => {
2201 vec![SamplingSlotState::from_integrand(&slots[0].integrand)]
2202 }
2203 SamplingCorrelationMode::Uncorrelated => slots
2204 .iter()
2205 .map(|slot| SamplingSlotState::from_integrand(&slot.integrand))
2206 .collect_vec(),
2207 };
2208 let monitored_discrete_setup =
2209 resolve_monitored_discrete_setup(sampling_correlation_mode, &slots, &sampling_states);
2210 let slot_first_non_trivial_discrete_breakdown_metadata =
2211 if let Some(path) = monitored_discrete_setup.path.as_deref() {
2212 slots
2213 .iter()
2214 .enumerate()
2215 .map(|(slot_index, slot)| {
2216 build_persisted_discrete_breakdown_metadata(
2217 &slot.integrand,
2218 path,
2219 &sampling_states[sampling_correlation_mode.state_index(slot_index)]
2220 .discrete_axis_labels,
2221 )
2222 })
2223 .collect_vec()
2224 } else {
2225 vec![None; slot_metas.len()]
2226 };
2227 if let Some(label_warning) = monitored_discrete_setup.warning.as_ref() {
2228 warn!("{label_warning}");
2229 }
2230
2231 let mut integration_state = if let Some(integration_state) = state {
2232 integration_state
2233 } else {
2234 IntegrationState::new_from_settings(
2235 sampling_correlation_mode,
2236 sampling_states.clone(),
2237 slot_metas.clone(),
2238 monitored_discrete_setup.path.clone(),
2239 monitored_discrete_setup.axis_label.clone(),
2240 monitored_discrete_setup.bin_descriptions.clone(),
2241 slot_first_non_trivial_discrete_breakdown_metadata,
2242 )
2243 };
2244 integration_state.monitored_discrete_path = monitored_discrete_setup.path;
2245 integration_state.first_non_trivial_discrete_label = monitored_discrete_setup.axis_label;
2246 integration_state.first_non_trivial_discrete_bin_descriptions =
2247 monitored_discrete_setup.bin_descriptions;
2248 if integration_state.slot_metas != slot_metas {
2249 return Err(Report::msg(
2250 "Saved integration state slots do not match the currently selected integrands",
2251 ));
2252 }
2253 if integration_state.sampling_correlation_mode != sampling_correlation_mode {
2254 return Err(Report::msg(
2255 "Saved integration state sampling mode does not match the current integration mode",
2256 ));
2257 }
2258 if integration_state.sampling_states.len()
2259 != sampling_correlation_mode.state_count(slot_metas.len())
2260 {
2261 return Err(Report::msg(
2262 "Saved integration state sampling metadata is inconsistent with the selected slots",
2263 ));
2264 }
2265 if integration_state
2266 .slot_first_non_trivial_discrete_breakdown_metadata
2267 .len()
2268 != slot_metas.len()
2269 {
2270 return Err(Report::msg(
2271 "Saved integration state discrete breakdown metadata is inconsistent with the selected slots",
2272 ));
2273 }
2274
2275 let primary = &slots[0];
2276 let sampling_str = primary.settings.sampling.describe_settings();
2277 let dimension = primary.integrand.get_n_dim();
2278 let discrete_depth = primary.settings.sampling.discrete_depth();
2279 let is_tropical_sampling = primary
2280 .settings
2281 .sampling
2282 .get_parameterization_settings()
2283 .is_none();
2284 let use_ltd = primary.settings.general.use_ltd;
2285 let integration_seed = primary.settings.integrator.seed;
2286 let integrated_phase = primary.settings.integrator.integrated_phase;
2287 let target_relative_accuracy = primary.settings.integrator.target_relative_accuracy;
2288 let target_absolute_accuracy = primary.settings.integrator.target_absolute_accuracy;
2289 let n_start = primary.settings.integrator.n_start;
2290 let n_increase = primary.settings.integrator.n_increase;
2291 let n_max = primary.settings.integrator.n_max;
2292
2293 let cont_dim_str = if is_tropical_sampling {
2294 format!("a median continious dimension of {}", dimension)
2295 } else {
2296 format!("{} continuous dimensions", dimension)
2297 };
2298
2299 let graph_string = if discrete_depth > 0 {
2300 let num_graphs = match &integration_state.sampling_state_for_slot(0).grid {
2301 Grid::Discrete(g) => g.bins.len(),
2302 _ => unreachable!(),
2303 };
2304 format!(
2305 "{discrete_depth} nested discrete grids with {} {} and ",
2306 num_graphs,
2307 if num_graphs > 1 { "graphs" } else { "graph" }
2308 )
2309 } else {
2310 String::new()
2311 };
2312
2313 let correlation_str = match sampling_correlation_mode {
2314 SamplingCorrelationMode::Correlated => "correlated",
2315 SamplingCorrelationMode::Uncorrelated => "uncorrelated",
2316 };
2317 let grid_str = format!("{graph_string}{cont_dim_str} using {sampling_str} ({correlation_str})");
2318
2319 let cores = n_cores.max(1);
2320 let n_slots = integration_state.slot_metas.len();
2321 let elapsed_seconds_offset = integration_state.elapsed_seconds;
2322
2323 let t_start = Instant::now();
2324
2325 info!(
2326 "Integrating using {} ltd with {} {} over {} ...",
2327 if use_ltd { "naive" } else { "cff" },
2328 cores,
2329 if cores > 1 { "cores" } else { "core" },
2330 grid_str
2331 );
2332 info!("");
2333
2334 let pool = ThreadPoolBuilder::new().num_threads(cores).build().unwrap();
2335
2336 let mut n_samples_evaluated = 0;
2337 let mut emitted_latest_observable_paths = vec![Vec::new(); n_slots];
2338 clear_iteration_abort_request();
2339 'integrateLoop: while integration_state.num_points < n_max {
2340 let cur_points = {
2342 let cur_points_not_final_iter = n_start + n_increase * integration_state.iter;
2343 if cur_points_not_final_iter + integration_state.num_points > n_max {
2344 n_max - integration_state.num_points
2345 } else {
2346 cur_points_not_final_iter
2347 }
2348 };
2349
2350 let target_points_per_core = (cur_points - 1) / cores + 1;
2352 let n_points_per_core = (0..cores)
2353 .map(|core_id| {
2354 if core_id + 1 == cores {
2355 cur_points - target_points_per_core * (cores - 1)
2356 } else {
2357 target_points_per_core
2358 }
2359 })
2360 .collect_vec();
2361
2362 let iteration_start = Instant::now();
2363 let mut current_batch_size = initial_batch_size(batching);
2364 let mut last_live_status = None;
2365 if batching.emit_initial_status_update && !is_interrupted() {
2366 status_emitter(build_status_update(StatusUpdateBuildRequest {
2367 kind: IntegrationStatusKind::Live,
2368 integration_state: &integration_state,
2369 cores,
2370 elapsed_time: t_start.elapsed(),
2371 iteration_elapsed_time: iteration_start.elapsed(),
2372 cur_points: 0,
2373 total_points_display: integration_state.num_points,
2374 n_samples_evaluated,
2375 targets: &targets,
2376 render_options: &view_options,
2377 live_progress: Some(status_update::LiveIterationProgress {
2378 completed_points: 0,
2379 target_points: cur_points,
2380 }),
2381 }))?;
2382 }
2383
2384 let current_max_evals = integration_state
2385 .all_integrals
2386 .iter()
2387 .map(ComplexAccumulator::get_worst_case)
2388 .collect_vec();
2389
2390 let mut worker_states = n_points_per_core
2391 .iter()
2392 .enumerate()
2393 .map(|(core_id, &n_points)| {
2394 CoreIterationState::new(
2395 slots.iter().map(|slot| slot.integrand.clone()).collect(),
2396 integration_state.sampling_correlation_mode,
2397 &integration_state
2398 .sampling_states
2399 .iter()
2400 .map(|sampling_state| sampling_state.grid.clone())
2401 .collect_vec(),
2402 integration_seed + integration_state.iter as u64,
2403 target_points_per_core * core_id,
2404 n_points,
2405 )
2406 })
2407 .collect_vec();
2408 while total_remaining_points(&worker_states) > 0 {
2409 let round_started_at = Instant::now();
2410 let processed_per_core: Vec<Result<usize>> = {
2411 let slot_settings = slots.iter().map(|slot| &slot.settings).collect_vec();
2412 let slot_models = slots.iter().map(|slot| &slot.model).collect_vec();
2413 pool.install(|| {
2414 worker_states
2415 .par_iter_mut()
2416 .map(|worker_state| {
2417 worker_state.evaluate_chunk(
2418 &slot_settings,
2419 &slot_models,
2420 integration_state.iter,
2421 ¤t_max_evals,
2422 current_batch_size,
2423 )
2424 })
2425 .collect()
2426 })
2427 };
2428 let processed_this_round = processed_per_core
2429 .into_iter()
2430 .collect::<Result<Vec<_>>>()?
2431 .into_iter()
2432 .sum::<usize>();
2433
2434 if processed_this_round == 0 {
2435 break;
2436 }
2437
2438 let completed_points = total_completed_points(&worker_states);
2439 if batching.emit_live_status_updates
2440 && completed_points < cur_points
2441 && !is_interrupted()
2442 && !is_iteration_abort_requested()
2443 {
2444 let now = Instant::now();
2445 let should_emit_live_status = last_live_status.is_none_or(|previous| {
2446 now.duration_since(previous).as_secs_f64()
2447 >= batching.min_time_between_status_updates_seconds
2448 });
2449 if should_emit_live_status {
2450 let preview_state = build_preview_integration_state(
2451 &integration_state,
2452 &slots,
2453 cores,
2454 completed_points,
2455 elapsed_seconds_offset + t_start.elapsed().as_secs_f64(),
2456 &worker_states,
2457 );
2458 status_emitter(build_status_update(StatusUpdateBuildRequest {
2459 kind: IntegrationStatusKind::Live,
2460 integration_state: &preview_state,
2461 cores,
2462 elapsed_time: t_start.elapsed(),
2463 iteration_elapsed_time: iteration_start.elapsed(),
2464 cur_points: completed_points,
2465 total_points_display: integration_state.num_points + completed_points,
2466 n_samples_evaluated: n_samples_evaluated + completed_points,
2467 targets: &targets,
2468 render_options: &view_options,
2469 live_progress: Some(status_update::LiveIterationProgress {
2470 completed_points,
2471 target_points: cur_points,
2472 }),
2473 }))?;
2474 last_live_status = Some(now);
2475 }
2476 }
2477
2478 if is_interrupted() {
2479 warn!("{}", "Integration interrupted by user".yellow());
2480 break 'integrateLoop;
2481 }
2482
2483 if is_iteration_abort_requested() {
2484 break;
2485 }
2486
2487 if total_remaining_points(&worker_states) > 0 {
2488 current_batch_size =
2489 next_batch_size(batching, current_batch_size, round_started_at.elapsed());
2490 }
2491 }
2492
2493 let abort_current_iteration = is_iteration_abort_requested();
2494 clear_iteration_abort_request();
2495
2496 if is_interrupted() {
2497 warn!("{}", "Integration interrupted by user".yellow());
2498 break 'integrateLoop;
2499 }
2500
2501 let completed_points = total_completed_points(&worker_states);
2502 if abort_current_iteration && completed_points < cur_points {
2503 if completed_points == 0 {
2504 warn!(
2505 "{}",
2506 "Current iteration abort requested before any samples completed; restarting the iteration."
2507 .yellow()
2508 );
2509 continue;
2510 }
2511 warn!(
2512 "{}",
2513 format!(
2514 "Current iteration aborted by user after {} completed samples.",
2515 completed_points
2516 )
2517 .yellow()
2518 );
2519 continue;
2520 }
2521
2522 n_samples_evaluated += completed_points;
2523 apply_iteration_core_states(
2524 &mut integration_state,
2525 &slots,
2526 cores,
2527 completed_points,
2528 elapsed_seconds_offset + t_start.elapsed().as_secs_f64(),
2529 &worker_states,
2530 );
2531
2532 let (owner_core_slice, other_cores) = worker_states.split_at_mut(1);
2534 let owner_core = &mut owner_core_slice[0].slot_integrands;
2535 for other_core in other_cores {
2536 for (owner_slot, other_slot) in owner_core
2537 .iter_mut()
2538 .zip(other_core.slot_integrands.iter_mut())
2539 {
2540 owner_slot.merge_runtime_results(other_slot)?;
2541 }
2542 }
2543 for (slot, integrand) in slots.iter_mut().zip(owner_core.iter()) {
2544 slot.integrand = integrand.clone();
2545 }
2546
2547 for slot in &mut slots {
2550 slot.integrand
2551 .update_runtime_results(integration_state.iter);
2552 }
2553
2554 if let Some(ref workspace_path) = workspace {
2555 for (slot_index, slot) in slots.iter().enumerate() {
2556 write_observable_resume_state(
2557 &slot.integrand,
2558 Some(workspace_path.as_path()),
2559 &integration_state.slot_metas[slot_index],
2560 integration_state.iter,
2561 output_control,
2562 )?;
2563 }
2564 write_integration_state_to_workspace(workspace_path, &integration_state)?;
2565 write_integration_result_snapshots(
2566 workspace_path,
2567 &integration_state,
2568 &targets,
2569 output_control,
2570 )?;
2571 }
2572
2573 for (slot_index, slot) in slots.iter().enumerate() {
2574 let workspace_path = workspace
2575 .as_deref()
2576 .map(|root| slot_workspace_path(root, &integration_state.slot_metas[slot_index]));
2577 emitted_latest_observable_paths[slot_index] =
2578 write_latest_observables_output(&slot.integrand, workspace_path.as_deref())?;
2579 write_observable_snapshot_archive(
2580 &slot.integrand,
2581 workspace_path.as_deref(),
2582 integration_state.iter,
2583 output_control,
2584 )?;
2585 }
2586
2587 status_emitter(build_status_update(StatusUpdateBuildRequest {
2588 kind: IntegrationStatusKind::Iteration,
2589 integration_state: &integration_state,
2590 cores,
2591 elapsed_time: t_start.elapsed(),
2592 iteration_elapsed_time: iteration_start.elapsed(),
2593 cur_points: completed_points,
2594 total_points_display: integration_state.num_points,
2595 n_samples_evaluated,
2596 targets: &targets,
2597 render_options: &view_options,
2598 live_progress: None,
2599 }))?;
2600
2601 let target_accuracy_status = evaluate_target_accuracy(
2602 &integration_state,
2603 integration_state.num_points,
2604 t_start.elapsed(),
2605 &targets,
2606 match integrated_phase {
2607 IntegratedPhase::Real | IntegratedPhase::Both => {
2608 IntegrationStatusPhaseDisplay::Real
2609 }
2610 IntegratedPhase::Imag => IntegrationStatusPhaseDisplay::Imag,
2611 },
2612 target_relative_accuracy,
2613 target_absolute_accuracy,
2614 );
2615 if target_accuracy_status.is_reached() {
2616 let reached_target = match (
2617 target_accuracy_status.relative_reached,
2618 target_accuracy_status.absolute_reached,
2619 ) {
2620 (true, true) => "relative and absolute",
2621 (true, false) => "relative",
2622 (false, true) => "absolute",
2623 (false, false) => unreachable!(),
2624 };
2625 info!(
2626 "Stopping integration after reaching the configured {reached_target} accuracy target."
2627 );
2628 break;
2629 }
2630 }
2631 set_interrupted(false);
2633 clear_iteration_abort_request();
2634
2635 if integration_state.num_points > 0 {
2636 let final_view_options = view_options.for_final();
2637 status_emitter(build_status_update(StatusUpdateBuildRequest {
2638 kind: IntegrationStatusKind::Final,
2639 integration_state: &integration_state,
2640 cores,
2641 elapsed_time: t_start.elapsed(),
2642 iteration_elapsed_time: Duration::ZERO,
2643 cur_points: 0,
2644 total_points_display: integration_state.num_points,
2645 n_samples_evaluated,
2646 targets: &targets,
2647 render_options: &final_view_options,
2648 live_progress: None,
2649 }))?;
2650 } else {
2651 info!("");
2652 warn!(
2653 "{}",
2654 "No final integration results to display since no iteration completed.".yellow()
2655 );
2656 info!("");
2657 }
2658
2659 if !slots.is_empty() {
2660 emit_results_output_summary(
2661 workspace.as_deref(),
2662 &slots,
2663 &emitted_latest_observable_paths,
2664 );
2665 }
2666
2667 Ok(build_integration_result(&integration_state, &targets))
2668}
2669
2670pub(crate) fn batch_integrate(
2673 integrand: &mut Integrand,
2674 model: &Model,
2675 input: BatchIntegrateInput,
2676) -> Result<BatchResult> {
2677 let samples = match input.samples {
2678 SampleInput::SampleList { samples } => samples,
2679 SampleInput::Grid {
2680 mut grid,
2681 num_points,
2682 seed,
2683 thread_id,
2684 } => {
2685 let mut rng = MonteCarloRng::new(seed, thread_id);
2686
2687 (0..num_points)
2688 .map(|_| {
2689 let mut sample = Sample::new();
2690 grid.sample(&mut rng, &mut sample);
2691 sample
2692 })
2693 .collect_vec()
2694 }
2695 };
2696
2697 let (evaluation_results, metadata_statistics) = evaluate_sample_list(
2698 integrand,
2699 &samples,
2700 model,
2701 input.num_cores,
2702 input.iter,
2703 input.max_eval,
2704 )?;
2705
2706 let integrand_output = generate_integrand_output(
2707 integrand,
2708 &evaluation_results,
2709 &samples,
2710 input.integrand_output_settings,
2711 input.settings.integrator.integrated_phase,
2712 );
2713
2714 let event_output = generate_event_output(
2715 integrand,
2716 evaluation_results,
2717 input.event_output_settings,
2718 input.settings,
2719 );
2720
2721 Ok(BatchResult {
2722 statistics: metadata_statistics,
2723 integrand_data: integrand_output,
2724 event_data: event_output,
2725 })
2726}
2727
2728fn generate_integrand_output(
2730 integrand: &Integrand,
2731 evaluation_results: &[EvaluationResult],
2732 samples: &[Sample<F<f64>>],
2733 integrand_output_settings: IntegralOutputSettings,
2734 integrated_phase: IntegratedPhase,
2735) -> BatchIntegrateOutput {
2736 fn effective_integrand_result(result: &EvaluationResult) -> Complex<F<f64>> {
2737 let jacobian = result.parameterization_jacobian.unwrap_or(F(1.0));
2738 result.integrand_result * Complex::new_re(jacobian)
2739 }
2740
2741 match integrand_output_settings {
2742 IntegralOutputSettings::Default => {
2743 let integrand_values = evaluation_results
2744 .iter()
2745 .map(effective_integrand_result)
2746 .collect();
2747
2748 BatchIntegrateOutput::Default(integrand_values, samples.to_vec())
2749 }
2750 IntegralOutputSettings::Accumulator => {
2751 let mut real_accumulator = StatisticsAccumulator::new();
2752 let mut imag_accumulator = StatisticsAccumulator::new();
2753 let mut grid = integrand.create_grid();
2754
2755 for (result, sample) in evaluation_results.iter().zip(samples.iter()) {
2756 let effective_result = effective_integrand_result(result);
2757 real_accumulator
2758 .add_sample(effective_result.re * sample.get_weight(), Some(sample));
2759 imag_accumulator
2760 .add_sample(effective_result.im * sample.get_weight(), Some(sample));
2761
2762 match integrated_phase {
2763 IntegratedPhase::Real => {
2764 grid.add_training_sample(sample, effective_result.re)
2765 .unwrap();
2766 }
2767 IntegratedPhase::Imag => {
2768 grid.add_training_sample(sample, effective_result.im)
2769 .unwrap();
2770 }
2771 IntegratedPhase::Both => {
2772 unimplemented!()
2773 }
2774 }
2775 }
2776
2777 BatchIntegrateOutput::Accumulator(
2778 Box::new((real_accumulator, imag_accumulator)),
2779 Box::new(grid),
2780 )
2781 }
2782 }
2783}
2784
2785fn generate_event_output(
2787 integrand: &Integrand,
2788 evaluation_results: Vec<EvaluationResult>,
2789 event_output_settings: EventOutputSettings,
2790 _settings: &RuntimeSettings,
2791) -> EventOutput {
2792 match event_output_settings {
2793 EventOutputSettings::None => EventOutput::None,
2794
2795 EventOutputSettings::EventList => {
2796 let mut event_groups = EventGroupList::default();
2797 for mut result in evaluation_results {
2798 event_groups.append(&mut result.event_groups);
2799 }
2800 EventOutput::EventList { event_groups }
2801 }
2802 EventOutputSettings::Histogram => integrand
2803 .observable_accumulator_bundle()
2804 .map(|histograms| EventOutput::Histogram { histograms })
2805 .unwrap_or(EventOutput::None),
2806 }
2807}
2808
2809pub fn slot_workspace_path(workspace: &Path, slot_meta: &SlotMeta) -> PathBuf {
2810 workspace.join("integrands").join(slot_meta.key())
2811}
2812
2813pub fn workspace_manifest_path(workspace: &Path) -> PathBuf {
2814 workspace.join("manifest.json")
2815}
2816
2817pub fn workspace_state_dir(workspace: &Path) -> PathBuf {
2818 workspace.join("state")
2819}
2820
2821pub fn workspace_state_path(workspace: &Path) -> PathBuf {
2822 workspace_state_dir(workspace).join("integration_state.bin")
2823}
2824
2825fn workspace_observable_state_dir(workspace: &Path, slot_meta: &SlotMeta) -> PathBuf {
2826 workspace_state_dir(workspace)
2827 .join("observables")
2828 .join(slot_meta.key())
2829}
2830
2831pub fn latest_observable_resume_state_path(workspace: &Path, slot_meta: &SlotMeta) -> PathBuf {
2832 workspace_observable_state_dir(workspace, slot_meta).join("latest.json")
2833}
2834
2835fn archived_observable_resume_state_path(
2836 workspace: &Path,
2837 slot_meta: &SlotMeta,
2838 iter: usize,
2839) -> PathBuf {
2840 workspace_observable_state_dir(workspace, slot_meta).join(format!("iter_{iter:04}.json"))
2841}
2842
2843pub fn workspace_result_snapshot_path(workspace: &Path) -> PathBuf {
2844 workspace.join("integration_result.json")
2845}
2846
2847fn workspace_result_archive_path(workspace: &Path, iter: usize) -> PathBuf {
2848 workspace
2849 .join("results")
2850 .join(format!("integration_result_iter_{iter:04}.json"))
2851}
2852
2853fn observable_output_extension(format: ObservableFileFormat) -> Option<&'static str> {
2854 match format {
2855 ObservableFileFormat::None => None,
2856 ObservableFileFormat::Hwu => Some("hwu"),
2857 ObservableFileFormat::Json => Some("json"),
2858 }
2859}
2860
2861fn latest_observable_output_path(workspace: &Path, format: ObservableFileFormat) -> PathBuf {
2862 let extension = observable_output_extension(format)
2863 .expect("user-facing observable outputs must use a real file format");
2864 workspace.join(format!("observables_final.{extension}"))
2865}
2866
2867fn archived_observable_output_path(
2868 workspace: &Path,
2869 format: ObservableFileFormat,
2870 iter: usize,
2871) -> PathBuf {
2872 let extension = observable_output_extension(format)
2873 .expect("user-facing observable outputs must use a real file format");
2874 workspace.join(format!("observables_final_iter_{iter:04}.{extension}"))
2875}
2876
2877fn user_facing_observables_output_formats(integrand: &Integrand) -> Vec<ObservableFileFormat> {
2878 let Integrand::ProcessIntegrand(process_integrand) = integrand else {
2879 return Vec::new();
2880 };
2881 if !integrand.has_observables() {
2882 return Vec::new();
2883 }
2884
2885 process_integrand
2886 .get_settings()
2887 .integrator
2888 .observables_output
2889 .resolved_formats()
2890}
2891
2892fn user_facing_observables_output_enabled(integrand: &Integrand) -> bool {
2893 let Integrand::ProcessIntegrand(process_integrand) = integrand else {
2894 return false;
2895 };
2896 if !integrand.has_observables() {
2897 return false;
2898 }
2899
2900 !process_integrand
2901 .get_settings()
2902 .integrator
2903 .observables_output
2904 .resolved_formats()
2905 .is_empty()
2906}
2907
2908fn write_atomic_bytes(path: &Path, bytes: &[u8]) -> Result<()> {
2909 let parent = path
2910 .parent()
2911 .ok_or_else(|| Report::msg("Atomic write target is missing a parent directory"))?;
2912 fs::create_dir_all(parent)?;
2913 let tmp_path = path.with_extension(format!(
2914 "{}.tmp",
2915 path.extension()
2916 .and_then(|ext| ext.to_str())
2917 .unwrap_or("tmp")
2918 ));
2919 fs::write(&tmp_path, bytes)?;
2920 fs::rename(&tmp_path, path)?;
2921 Ok(())
2922}
2923
2924fn write_atomic_json<T: Serialize>(path: &Path, value: &T) -> Result<()> {
2925 write_atomic_bytes(path, &serde_json::to_vec_pretty(value)?)
2926}
2927
2928fn write_observable_resume_state(
2929 integrand: &Integrand,
2930 workspace: Option<&Path>,
2931 slot_meta: &SlotMeta,
2932 iter: usize,
2933 output_control: WorkspaceSnapshotControl,
2934) -> Result<Option<PathBuf>> {
2935 let Some(bundle) = integrand.observable_snapshot_bundle() else {
2936 return Ok(None);
2937 };
2938 let Some(workspace) = workspace else {
2939 return Ok(None);
2940 };
2941 let latest_path = latest_observable_resume_state_path(workspace, slot_meta);
2942 write_atomic_json(&latest_path, &bundle)?;
2943 if output_control.write_iteration_archives {
2944 let archived_path = archived_observable_resume_state_path(workspace, slot_meta, iter);
2945 write_atomic_json(&archived_path, &bundle)?;
2946 }
2947 Ok(Some(latest_path))
2948}
2949
2950fn write_observable_snapshot_archive(
2951 integrand: &Integrand,
2952 workspace: Option<&Path>,
2953 iter: usize,
2954 output_control: WorkspaceSnapshotControl,
2955) -> Result<()> {
2956 if !user_facing_observables_output_enabled(integrand) {
2957 return Ok(());
2958 }
2959 if !output_control.write_iteration_archives {
2960 return Ok(());
2961 }
2962 let Some(workspace) = workspace else {
2963 return Ok(());
2964 };
2965
2966 for format in user_facing_observables_output_formats(integrand) {
2967 let path = archived_observable_output_path(workspace, format, iter);
2968 integrand.write_observable_snapshots(&path, format)?;
2969 }
2970
2971 Ok(())
2972}
2973
2974fn write_latest_observables_output(
2975 integrand: &Integrand,
2976 workspace: Option<&Path>,
2977) -> Result<Vec<PathBuf>> {
2978 if !user_facing_observables_output_enabled(integrand) {
2979 return Ok(Vec::new());
2980 }
2981 let Some(workspace) = workspace else {
2982 return Ok(Vec::new());
2983 };
2984
2985 let mut emitted_paths = Vec::new();
2986 for format in user_facing_observables_output_formats(integrand) {
2987 let path = latest_observable_output_path(workspace, format);
2988 integrand.write_observable_snapshots(&path, format)?;
2989 emitted_paths.push(path);
2990 }
2991
2992 Ok(emitted_paths)
2993}
2994
2995fn write_integration_state_to_workspace(
2996 workspace_path: &Path,
2997 integration_state: &IntegrationState,
2998) -> Result<()> {
2999 write_atomic_bytes(
3000 &workspace_state_path(workspace_path),
3001 &bincode::encode_to_vec(integration_state, bincode::config::standard())
3002 .unwrap_or_else(|_| panic!("failed to serialize the integration state")),
3003 )?;
3004 Ok(())
3005}
3006
3007fn write_integration_result_snapshots(
3008 workspace_path: &Path,
3009 integration_state: &IntegrationState,
3010 targets: &[Option<Complex<F<f64>>>],
3011 output_control: WorkspaceSnapshotControl,
3012) -> Result<()> {
3013 let result = build_integration_result(integration_state, targets);
3014 write_atomic_json(&workspace_result_snapshot_path(workspace_path), &result)?;
3015 if output_control.write_iteration_archives {
3016 write_atomic_json(
3017 &workspace_result_archive_path(workspace_path, integration_state.iter),
3018 &result,
3019 )?;
3020 }
3021 Ok(())
3022}
3023
3024fn workspace_relative_display_path(root: &Path, path: &Path) -> String {
3025 path.strip_prefix(root)
3026 .unwrap_or(path)
3027 .display()
3028 .to_string()
3029}
3030
3031fn emit_results_output_summary(
3032 workspace: Option<&Path>,
3033 slots: &[IntegrationSlot],
3034 emitted_paths: &[Vec<PathBuf>],
3035) {
3036 let rendered = render_results_output_summary_table(workspace, slots, emitted_paths);
3037 let Some(rendered) = rendered else {
3038 return;
3039 };
3040
3041 info!("\n{rendered}");
3042}
3043
3044fn render_results_output_summary_table(
3045 workspace: Option<&Path>,
3046 slots: &[IntegrationSlot],
3047 emitted_paths: &[Vec<PathBuf>],
3048) -> Option<String> {
3049 let mut summary_rows = Vec::new();
3050 if let Some(workspace) = workspace {
3051 summary_rows.push((
3052 "workspace path".to_string(),
3053 workspace.display().to_string(),
3054 ));
3055 summary_rows.push((
3056 "results".to_string(),
3057 workspace_relative_display_path(workspace, &workspace_result_snapshot_path(workspace)),
3058 ));
3059 }
3060
3061 for (slot, slot_emitted_paths) in slots.iter().zip(emitted_paths.iter()) {
3062 for final_path in slot_emitted_paths {
3063 if !final_path.exists() {
3064 continue;
3065 }
3066 let display_path = workspace
3067 .map(|root| workspace_relative_display_path(root, final_path))
3068 .unwrap_or_else(|| final_path.display().to_string());
3069 let row_label = if slot_emitted_paths.len() > 1 {
3070 let format_label = final_path
3071 .extension()
3072 .and_then(|extension| extension.to_str())
3073 .unwrap_or("output");
3074 format!("{} ({format_label})", slot_key_label(&slot.meta))
3075 } else {
3076 slot_key_label(&slot.meta)
3077 };
3078 summary_rows.push((row_label, display_path));
3079 }
3080 }
3081
3082 if summary_rows.is_empty() {
3083 return None;
3084 }
3085
3086 let mut builder = Builder::default();
3087 builder.push_record(["type".to_string(), "path".to_string()]);
3088 for (row_type, row_path) in summary_rows {
3089 builder.push_record([
3090 row_type.blue().bold().to_string(),
3091 row_path.green().to_string(),
3092 ]);
3093 }
3094
3095 let mut table = builder.build();
3096 table.with(Panel::header(
3097 "Integration results emitted".green().bold().to_string(),
3098 ));
3099 table.with(
3100 Style::rounded().horizontals([(
3101 1,
3102 HorizontalLine::new('─')
3103 .intersection('┼')
3104 .left('├')
3105 .right('┤'),
3106 )]),
3107 );
3108 table.with(Modify::new(Rows::first()).with(Alignment::left()));
3109 table.with(Modify::new(Columns::new(0..2)).with(Alignment::left()));
3110
3111 Some(utils::normalize_tabled_separator_rows(&table.to_string()))
3112}
3113
3114fn evaluate_sample_list(
3116 integrand: &mut Integrand,
3117 samples: &[Sample<F<f64>>],
3118 model: &Model,
3119 num_cores: usize,
3120 iter: usize,
3121 max_eval: Complex<F<f64>>,
3122) -> Result<(Vec<EvaluationResult>, StatisticsCounter)> {
3123 let list_size = samples.len();
3124 let nvec_per_core = (list_size - 1) / num_cores + 1;
3125 let num_chunks = samples.chunks(nvec_per_core).len();
3126
3127 let sample_chunks = samples.par_chunks(nvec_per_core);
3128 let integrands = (0..num_chunks)
3129 .map(|_| integrand.clone())
3130 .collect_vec()
3131 .into_par_iter();
3132
3133 let evaluation_results_per_core: Vec<
3134 Result<(Vec<EvaluationResult>, StatisticsCounter, Integrand)>,
3135 > = sample_chunks
3136 .zip(integrands)
3137 .map(|(chunk, mut integrand)| {
3138 let raw_batch =
3139 integrand.evaluate_samples_raw(chunk, model, iter, false, false, max_eval)?;
3140 Ok((raw_batch.samples, raw_batch.statistics, integrand))
3141 })
3142 .collect();
3143 let mut evaluation_results_per_core: Vec<(
3144 Vec<EvaluationResult>,
3145 StatisticsCounter,
3146 Integrand,
3147 )> = evaluation_results_per_core
3148 .into_iter()
3149 .collect::<Result<_>>()?;
3150
3151 for (_, _, worker_integrand) in evaluation_results_per_core.iter_mut() {
3152 integrand.merge_runtime_results(worker_integrand)?;
3153 }
3154
3155 let (evaluation_results, meta_data_statistics) = evaluation_results_per_core.into_iter().fold(
3156 (Vec::new(), StatisticsCounter::new_empty()),
3157 |(mut all_results, stats), (results, worker_stats, _)| {
3158 all_results.extend(results);
3159 (all_results, stats.merged(&worker_stats))
3160 },
3161 );
3162
3163 Ok((evaluation_results, meta_data_statistics))
3164}
3165
3166#[derive(Serialize, Deserialize)]
3170pub enum SampleInput {
3171 SampleList {
3172 samples: Vec<Sample<F<f64>>>,
3173 },
3174 Grid {
3175 grid: Box<Grid<F<f64>>>,
3176 num_points: usize,
3177 seed: u64,
3178 thread_id: usize,
3179 },
3180}
3181
3182type AccumulatorPair = (StatisticsAccumulator<F<f64>>, StatisticsAccumulator<F<f64>>);
3183
3184#[derive(Serialize, Deserialize)]
3185pub enum BatchIntegrateOutput {
3186 Default(Vec<Complex<F<f64>>>, Vec<Sample<F<f64>>>),
3187 Accumulator(Box<AccumulatorPair>, Box<Grid<F<f64>>>),
3188}
3189
3190#[derive(Serialize, Deserialize)]
3193pub enum EventOutput {
3194 None,
3195 EventList {
3196 event_groups: EventGroupList,
3197 },
3198 Histogram {
3199 histograms: ObservableAccumulatorBundle,
3200 },
3201}
3202
3203#[derive(Serialize, Deserialize, Encode, Decode)]
3205pub struct BatchResult {
3206 pub statistics: StatisticsCounter,
3207 #[bincode(with_serde)]
3208 pub integrand_data: BatchIntegrateOutput,
3209 #[bincode(with_serde)]
3210 pub event_data: EventOutput,
3211}
3212
3213pub struct BatchIntegrateInput<'a> {
3215 pub max_eval: Complex<F<f64>>,
3217 pub iter: usize,
3218 pub settings: &'a RuntimeSettings,
3219 pub samples: SampleInput,
3221 pub integrand_output_settings: IntegralOutputSettings,
3222 pub event_output_settings: EventOutputSettings,
3223 pub num_cores: usize,
3224}
3225
3226#[derive(Serialize, Deserialize)]
3228pub enum IntegralOutputSettings {
3229 Default,
3230 Accumulator,
3231}
3232
3233#[derive(Serialize, Deserialize)]
3235pub enum EventOutputSettings {
3236 None,
3237 EventList,
3238 Histogram,
3239}
3240
3241#[derive(Serialize, Deserialize, Encode, Decode)]
3242pub struct SerializableBatchIntegrateInput {
3243 #[bincode(with_serde)]
3244 pub max_eval: Complex<F<f64>>,
3245 pub iter: usize,
3246 #[bincode(with_serde)]
3247 pub samples: SampleInput,
3248 #[bincode(with_serde)]
3249 pub integrand_output_settings: IntegralOutputSettings,
3250 #[bincode(with_serde)]
3251 pub event_output_settings: EventOutputSettings,
3252 pub num_cores: usize,
3253}
3254
3255impl SerializableBatchIntegrateInput {
3256 pub(crate) fn into_batch_integrate_input(
3257 self,
3258 settings: &RuntimeSettings,
3259 ) -> BatchIntegrateInput<'_> {
3260 BatchIntegrateInput {
3261 max_eval: self.max_eval,
3262 iter: self.iter,
3263 samples: self.samples,
3264 integrand_output_settings: self.integrand_output_settings,
3265 event_output_settings: self.event_output_settings,
3266 num_cores: self.num_cores,
3267 settings,
3268 }
3269 }
3270}
3271
3272#[derive(Clone)]
3274pub struct MasterNode {
3275 grid: Grid<F<f64>>,
3276 integrator_settings: IntegratorSettings,
3277 master_accumulator_re: StatisticsAccumulator<F<f64>>,
3278 master_accumulator_im: StatisticsAccumulator<F<f64>>,
3279 statistics: StatisticsCounter,
3280 observable_accumulators: Option<ObservableAccumulatorBundle>,
3281 event_groups: EventGroupList,
3282 current_iter: usize,
3283}
3284
3285impl MasterNode {
3286 pub(crate) fn new(grid: Grid<F<f64>>, integrator_settings: IntegratorSettings) -> Self {
3287 MasterNode {
3288 grid,
3289 integrator_settings,
3290 master_accumulator_im: StatisticsAccumulator::new(),
3291 master_accumulator_re: StatisticsAccumulator::new(),
3292 statistics: StatisticsCounter::new_empty(),
3293 observable_accumulators: None,
3294 event_groups: EventGroupList::default(),
3295 current_iter: 0,
3296 }
3297 }
3298
3299 fn update_grid_with_grid(&mut self, other_grid: &Grid<F<f64>>) -> Result<(), String> {
3301 self.grid.merge(other_grid)
3302 }
3303
3304 fn update_grid_with_samples(
3306 &mut self,
3307 samples_points: &[Sample<F<f64>>],
3308 results: &[Complex<F<f64>>],
3309 ) -> Result<(), String> {
3310 let integrated_phase = self.integrator_settings.integrated_phase;
3311
3312 for (sample_point, result) in samples_points.iter().zip(results.iter()) {
3313 match integrated_phase {
3314 IntegratedPhase::Real => self.grid.add_training_sample(sample_point, result.re)?,
3315 IntegratedPhase::Imag => self.grid.add_training_sample(sample_point, result.im)?,
3316 IntegratedPhase::Both => {
3317 unimplemented!("integrated phase both not yet implemented")
3318 }
3319 }
3320 }
3321
3322 Ok(())
3323 }
3324
3325 pub(crate) fn update_accumulators_with_accumulators(
3327 &mut self,
3328 mut real_accumulator: StatisticsAccumulator<F<f64>>,
3329 mut imaginary_accumulator: StatisticsAccumulator<F<f64>>,
3330 ) {
3331 self.master_accumulator_re
3332 .merge_samples(&mut real_accumulator);
3333
3334 self.master_accumulator_im
3335 .merge_samples(&mut imaginary_accumulator);
3336 }
3337
3338 fn update_accumuators_with_samples(
3340 &mut self,
3341 sample_points: &[Sample<F<f64>>],
3342 results: &[Complex<F<f64>>],
3343 ) {
3344 for (sample_point, result) in sample_points.iter().zip(results.iter()) {
3345 self.master_accumulator_re
3346 .add_sample(result.re * sample_point.get_weight(), Some(sample_point));
3347
3348 self.master_accumulator_im
3349 .add_sample(result.im * sample_point.get_weight(), Some(sample_point));
3350 }
3351 }
3352
3353 fn update_metadata_statistics(&mut self, statistics: StatisticsCounter) {
3355 self.statistics = self.statistics.merged(&statistics);
3356 }
3357
3358 pub(crate) fn update_iter(&mut self) {
3360 self.grid.update(
3361 F(self.integrator_settings.discrete_dim_learning_rate),
3362 F(self.integrator_settings.continuous_dim_learning_rate),
3363 );
3364 self.master_accumulator_re.update_iter(false);
3365 self.master_accumulator_im.update_iter(false);
3366 if let Some(observable_accumulators) = self.observable_accumulators.as_mut() {
3367 observable_accumulators.update_results();
3368 }
3369
3370 self.current_iter += 1;
3371 }
3372
3373 pub(crate) fn write_batch_input(
3375 &mut self,
3376 num_cores: usize,
3377 num_samples: usize,
3378 export_grid: bool,
3379 output_accumulator: bool,
3380 workspace_path: &str,
3381 job_id: usize,
3382 ) -> Result<(), Report> {
3383 let max_eval = Complex::new(
3384 self.master_accumulator_re
3385 .max_eval_positive
3386 .max(self.master_accumulator_re.max_eval_negative),
3387 self.master_accumulator_im
3388 .max_eval_positive
3389 .max(self.master_accumulator_im.max_eval_negative),
3390 );
3391
3392 let samples = if export_grid {
3393 SampleInput::Grid {
3394 grid: Box::new(self.grid.clone()),
3395 num_points: num_samples,
3396 seed: self.integrator_settings.seed,
3397 thread_id: job_id,
3398 }
3399 } else {
3400 let mut rng = rand::rng();
3401 let mut samples_temp = vec![Sample::new(); num_samples];
3402 for sample in samples_temp.iter_mut() {
3403 self.grid.sample(&mut rng, sample);
3404 }
3405 SampleInput::SampleList {
3406 samples: samples_temp,
3407 }
3408 };
3409
3410 let integrand_output_settings = if output_accumulator {
3411 IntegralOutputSettings::Accumulator
3412 } else {
3413 IntegralOutputSettings::Default
3414 };
3415
3416 let input = SerializableBatchIntegrateInput {
3417 num_cores,
3418 max_eval,
3419 iter: self.current_iter,
3420 samples,
3421 integrand_output_settings,
3422 event_output_settings: EventOutputSettings::None,
3423 };
3424
3425 let input_bytes = bincode::encode_to_vec(&input, bincode::config::standard())?;
3426 let job_name = format!("job_{}", job_id);
3427 let job_path = std::path::Path::new(workspace_path).join(job_name);
3428
3429 std::fs::write(job_path, input_bytes)?;
3430
3431 Ok(())
3432 }
3433
3434 pub(crate) fn process_batch_output(&mut self, output: BatchResult) -> Result<(), String> {
3436 self.update_metadata_statistics(output.statistics);
3437
3438 match output.integrand_data {
3439 BatchIntegrateOutput::Default(results, samples) => {
3440 self.update_accumuators_with_samples(&samples, &results);
3441 self.update_grid_with_samples(&samples, &results)?;
3442 }
3443 BatchIntegrateOutput::Accumulator(accumulators, grid) => {
3444 let (real_accumulator, imag_accumulator) = *accumulators;
3445 self.update_accumulators_with_accumulators(real_accumulator, imag_accumulator);
3446 self.update_grid_with_grid(&grid)?;
3447 }
3448 }
3449
3450 match output.event_data {
3451 EventOutput::None => {}
3452 EventOutput::EventList { mut event_groups } => {
3453 self.event_groups.append(&mut event_groups);
3454 }
3455 EventOutput::Histogram { mut histograms } => {
3456 if let Some(existing) = self.observable_accumulators.as_mut() {
3457 existing
3458 .merge_samples(&mut histograms)
3459 .map_err(|err| err.to_string())?;
3460 } else {
3461 self.observable_accumulators = Some(histograms);
3462 }
3463 }
3464 }
3465
3466 Ok(())
3467 }
3468
3469 pub(crate) fn display_status(&self) {
3471 let status_block = [
3472 render_integral_result(
3473 &self.master_accumulator_re,
3474 "itg",
3475 self.current_iter,
3476 "re",
3477 None,
3478 ),
3479 render_integral_result(
3480 &self.master_accumulator_im,
3481 "itg",
3482 self.current_iter,
3483 "im",
3484 None,
3485 ),
3486 self.statistics.render_status_table(),
3487 ]
3488 .join("\n");
3489
3490 info!("\n{status_block}");
3491 }
3492}
3493
3494pub fn emit_integration_status_via_tracing(
3495 kind: IntegrationStatusKind,
3496 status_block: impl AsRef<str>,
3497) -> Result<()> {
3498 let status_block = status_block.as_ref();
3499 match kind {
3500 IntegrationStatusKind::Live => {}
3501 IntegrationStatusKind::Iteration => {
3502 info!("\n{status_block}");
3503 info!("");
3504 }
3505 IntegrationStatusKind::Final => {
3506 info!("");
3507 info!("{}", "Final integration results:".bold().green());
3508 info!("");
3509 info!("\n{status_block}");
3510 info!("");
3511 }
3512 }
3513
3514 Ok(())
3515}
3516
3517pub fn render_saved_integration_summary(
3518 integration_state: &IntegrationState,
3519 targets: &[Option<Complex<F<f64>>>],
3520 view_options: &IntegrationStatusViewOptions,
3521 tabled_options: &TabledRenderOptions,
3522) -> String {
3523 render_tabled::render_status_update(
3524 &build_saved_status_update(integration_state, targets, view_options),
3525 tabled_options,
3526 )
3527}
3528
3529pub fn render_status_update_tabled(
3530 update: &StatusUpdate,
3531 tabled_options: &TabledRenderOptions,
3532) -> String {
3533 render_tabled::render_status_update(update, tabled_options)
3534}
3535
3536pub fn build_integration_result(
3537 integration_state: &IntegrationState,
3538 targets: &[Option<Complex<F<f64>>>],
3539) -> IntegrationResult {
3540 let integration_statistics = integration_state.stats.snapshot();
3541 let slots = integration_state
3542 .slot_metas
3543 .iter()
3544 .enumerate()
3545 .map(|(slot_index, slot_meta)| {
3546 let accumulator = &integration_state.all_integrals[slot_index];
3547 let discrete_context =
3548 integration_state.monitored_discrete_context_for_slot(slot_index);
3549 SlotIntegrationResult {
3550 key: slot_meta.key(),
3551 process: slot_meta.process_name.clone(),
3552 integrand: slot_meta.integrand_name.clone(),
3553 target: targets[slot_index],
3554 integral: IntegralEstimate {
3555 neval: accumulator.re.processed_samples,
3556 real_zero: accumulator.re.num_zero_evaluations,
3557 im_zero: accumulator.im.num_zero_evaluations,
3558 result: Complex::new(accumulator.re.avg, accumulator.im.avg),
3559 error: Complex::new(accumulator.re.err, accumulator.im.err),
3560 real_chisq: accumulator.re.chi_sq,
3561 im_chisq: accumulator.im.chi_sq,
3562 },
3563 table_results: build_table_result_summary(
3564 slot_meta,
3565 accumulator,
3566 integration_state.iter,
3567 targets[slot_index],
3568 ),
3569 integration_statistics: integration_statistics.clone(),
3570 max_weight_info: build_max_weight_info_summary(
3571 &integration_state
3572 .sampling_state_for_slot(slot_index)
3573 .discrete_axis_labels,
3574 accumulator,
3575 ),
3576 grid_breakdown: ComponentDiscreteBreakdown {
3577 re: discrete_context.as_ref().and_then(|context| {
3578 integration_state.slot_re_summaries[slot_index]
3579 .as_ref()
3580 .zip(
3581 integration_state
3582 .slot_first_non_trivial_discrete_breakdown_metadata[slot_index]
3583 .as_ref(),
3584 )
3585 .and_then(|(summary, metadata)| {
3586 summary_at_path(summary, &context.path).and_then(|summary| {
3587 summary.first_non_trivial_breakdown(metadata, &context.pdfs)
3588 })
3589 })
3590 }),
3591 im: discrete_context.as_ref().and_then(|context| {
3592 integration_state.slot_im_summaries[slot_index]
3593 .as_ref()
3594 .zip(
3595 integration_state
3596 .slot_first_non_trivial_discrete_breakdown_metadata[slot_index]
3597 .as_ref(),
3598 )
3599 .and_then(|(summary, metadata)| {
3600 summary_at_path(summary, &context.path).and_then(|summary| {
3601 summary.first_non_trivial_breakdown(metadata, &context.pdfs)
3602 })
3603 })
3604 }),
3605 },
3606 }
3607 })
3608 .collect();
3609
3610 IntegrationResult { slots }
3611}
3612
3613pub fn print_integral_result(
3614 itg: &StatisticsAccumulator<F<f64>>,
3615 label: &str,
3616 i_iter: usize,
3617 tag: &str,
3618 trgt: Option<F<f64>>,
3619) {
3620 info!("{}", render_integral_result(itg, label, i_iter, tag, trgt));
3621}
3622
3623#[allow(clippy::format_in_format_args)]
3624fn render_integral_result(
3625 itg: &StatisticsAccumulator<F<f64>>,
3626 label: &str,
3627 i_iter: usize,
3628 tag: &str,
3629 trgt: Option<F<f64>>,
3630) -> String {
3631 let slot_meta = SlotMeta {
3632 process_name: label.to_string(),
3633 integrand_name: String::new(),
3634 };
3635 let mut cells = build_integral_result_cells(itg, &slot_meta, i_iter, tag, trgt);
3636 cells.integrand = format!("{label} {}:", format!("{:-2}", tag).blue().bold());
3637 let delta = match (cells.delta_sigma.as_ref(), cells.delta_percent.as_ref()) {
3638 (Some(delta_sigma), Some(delta_percent)) => format!("{delta_sigma}, {delta_percent}"),
3639 _ => String::new(),
3640 };
3641
3642 format!(
3643 "| {} {} {} {} {} {}",
3644 cells.integrand, cells.value, cells.relative_error, cells.chi_sq, delta, cells.mwi
3645 )
3646}
3647
3648#[cfg(test)]
3649mod tests {
3650 use super::*;
3651 use crate::{UnitVolumeIntegrand, UnitVolumeSettings};
3652 use colored::control;
3653 use ratatui::{Terminal, backend::TestBackend};
3654 use std::fs;
3655 use symbolica::numerical_integration::ContinuousGrid;
3656
3657 fn make_accumulator(
3658 re_avg: f64,
3659 re_err: f64,
3660 re_chi_sq: f64,
3661 im_avg: f64,
3662 im_err: f64,
3663 im_chi_sq: f64,
3664 ) -> ComplexAccumulator {
3665 let mut accumulator = ComplexAccumulator::new();
3666 accumulator.re.avg = F(re_avg);
3667 accumulator.re.err = F(re_err);
3668 accumulator.re.chi_sq = F(re_chi_sq);
3669 accumulator.re.processed_samples = 100_000;
3670 accumulator.re.max_eval_positive = F(1.0);
3671 accumulator.im.avg = F(im_avg);
3672 accumulator.im.err = F(im_err);
3673 accumulator.im.chi_sq = F(im_chi_sq);
3674 accumulator.im.processed_samples = 100_000;
3675 accumulator.im.max_eval_positive = F(1.0);
3676 accumulator
3677 }
3678
3679 struct StatisticsFixture {
3680 precision: crate::settings::runtime::Precision,
3681 total_time: Duration,
3682 integrand_time: Duration,
3683 evaluator_time: Duration,
3684 parameterization_time: Duration,
3685 event_time: Duration,
3686 generated_event_count: usize,
3687 accepted_event_count: usize,
3688 }
3689
3690 fn make_statistics_counter(fixture: StatisticsFixture) -> StatisticsCounter {
3691 let mut evaluation = EvaluationResult::zero();
3692 evaluation.evaluation_metadata.total_timing = fixture.total_time;
3693 evaluation.evaluation_metadata.integrand_evaluation_time = fixture.integrand_time;
3694 evaluation.evaluation_metadata.evaluator_evaluation_time = fixture.evaluator_time;
3695 evaluation.evaluation_metadata.parameterization_time = fixture.parameterization_time;
3696 evaluation.evaluation_metadata.event_processing_time = fixture.event_time;
3697 evaluation.evaluation_metadata.generated_event_count = fixture.generated_event_count;
3698 evaluation.evaluation_metadata.accepted_event_count = fixture.accepted_event_count;
3699 evaluation.evaluation_metadata.stability_results.push(
3700 crate::integrands::evaluation::StabilityResult {
3701 precision: fixture.precision,
3702 estimated_relative_accuracy: None,
3703 status: crate::integrands::evaluation::StabilityStatus::Stable(2),
3704 total_time: fixture.total_time,
3705 },
3706 );
3707 StatisticsCounter::from_evaluation_results(&[evaluation])
3708 }
3709
3710 fn make_integration_state() -> IntegrationState {
3711 let sampling_grid = Grid::Continuous(ContinuousGrid::new(1, 64, 100, None, false));
3712 let mut state = IntegrationState::new_from_settings(
3713 SamplingCorrelationMode::Correlated,
3714 vec![SamplingSlotState::new(sampling_grid, Vec::new())],
3715 vec![
3716 SlotMeta {
3717 process_name: "proc_a".to_string(),
3718 integrand_name: "itg_a".to_string(),
3719 },
3720 SlotMeta {
3721 process_name: "proc_b".to_string(),
3722 integrand_name: "itg_b".to_string(),
3723 },
3724 ],
3725 None,
3726 None,
3727 None,
3728 vec![None, None],
3729 );
3730 state.iter = 1;
3731 state.num_points = 100_000;
3732 state.slot_stats = vec![
3733 make_statistics_counter(StatisticsFixture {
3734 precision: crate::settings::runtime::Precision::Double,
3735 total_time: Duration::from_micros(462),
3736 integrand_time: Duration::from_micros(414),
3737 evaluator_time: Duration::from_micros(279),
3738 parameterization_time: Duration::from_nanos(5_800),
3739 event_time: Duration::from_micros(12),
3740 generated_event_count: 7,
3741 accepted_event_count: 5,
3742 }),
3743 make_statistics_counter(StatisticsFixture {
3744 precision: crate::settings::runtime::Precision::Quad,
3745 total_time: Duration::from_micros(900),
3746 integrand_time: Duration::from_micros(810),
3747 evaluator_time: Duration::from_micros(120),
3748 parameterization_time: Duration::from_micros(34),
3749 event_time: Duration::from_micros(40),
3750 generated_event_count: 9,
3751 accepted_event_count: 6,
3752 }),
3753 ];
3754 state.stats = state.slot_stats[0].merged(&state.slot_stats[1]);
3755 state
3756 .stats
3757 .add_integrator_overhead(Duration::from_micros(110), 2);
3758 state.all_integrals = vec![
3759 make_accumulator(7.5e-5, 9.8e-5, 0.394, 3.2e-5, 1.5e-5, 0.378),
3760 make_accumulator(2.1e-5, 2.0e-6, 0.221, -1.7e-5, 3.0e-6, 0.187),
3761 ];
3762 state
3763 }
3764
3765 fn make_preview_test_slots(slot_metas: &[SlotMeta]) -> Vec<IntegrationSlot> {
3766 slot_metas
3767 .iter()
3768 .cloned()
3769 .map(|meta| {
3770 let settings = RuntimeSettings::default();
3771 IntegrationSlot::new(
3772 meta,
3773 settings.clone(),
3774 Model::default(),
3775 Integrand::UnitVolume(UnitVolumeIntegrand::new(
3776 settings,
3777 UnitVolumeSettings { n_3d_momenta: 1 },
3778 )),
3779 None,
3780 )
3781 })
3782 .collect()
3783 }
3784
3785 fn make_preview_test_core_state(state: &IntegrationState) -> CoreIterationState {
3786 let slot_stats = vec![
3787 make_statistics_counter(StatisticsFixture {
3788 precision: crate::settings::runtime::Precision::Arb,
3789 total_time: Duration::from_millis(8),
3790 integrand_time: Duration::from_millis(7),
3791 evaluator_time: Duration::from_millis(3),
3792 parameterization_time: Duration::from_micros(150),
3793 event_time: Duration::from_micros(500),
3794 generated_event_count: 40,
3795 accepted_event_count: 30,
3796 }),
3797 make_statistics_counter(StatisticsFixture {
3798 precision: crate::settings::runtime::Precision::Arb,
3799 total_time: Duration::from_millis(12),
3800 integrand_time: Duration::from_millis(10),
3801 evaluator_time: Duration::from_millis(2),
3802 parameterization_time: Duration::from_micros(300),
3803 event_time: Duration::from_millis(1),
3804 generated_event_count: 60,
3805 accepted_event_count: 45,
3806 }),
3807 ];
3808 let grid_template = state
3809 .sampling_state_for_slot(0)
3810 .grid
3811 .clone_without_samples();
3812 CoreIterationState {
3813 slot_integrands: Vec::new(),
3814 stats: slot_stats[0].merged(&slot_stats[1]),
3815 slot_stats,
3816 integrals: vec![ComplexAccumulator::new(); state.slot_metas.len()],
3817 sampling_correlation_mode: SamplingCorrelationMode::Correlated,
3818 sampling_states: vec![CoreSamplingSlotState {
3819 sampling_grid: grid_template.clone(),
3820 rng: MonteCarloRng::new(7, 0),
3821 }],
3822 slot_re_grids: (0..state.slot_metas.len())
3823 .map(|_| grid_template.clone())
3824 .collect(),
3825 slot_im_grids: (0..state.slot_metas.len())
3826 .map(|_| grid_template.clone())
3827 .collect(),
3828 remaining_points: 0,
3829 completed_points: 12,
3830 }
3831 }
3832
3833 fn make_discrete_integration_state() -> IntegrationState {
3834 let make_continuous_grid =
3835 || Grid::Continuous(ContinuousGrid::new(1, 64, 100, None, false));
3836 let sampling_grid = Grid::Discrete(DiscreteGrid::new(
3837 vec![Some(make_continuous_grid()), Some(make_continuous_grid())],
3838 F(10.0),
3839 false,
3840 ));
3841 let mut state = IntegrationState::new_from_settings(
3842 SamplingCorrelationMode::Correlated,
3843 vec![SamplingSlotState::new(
3844 sampling_grid,
3845 vec!["graph".to_string()],
3846 )],
3847 vec![
3848 SlotMeta {
3849 process_name: "proc_a".to_string(),
3850 integrand_name: "itg_a".to_string(),
3851 },
3852 SlotMeta {
3853 process_name: "proc_b".to_string(),
3854 integrand_name: "itg_b".to_string(),
3855 },
3856 ],
3857 Some(vec![]),
3858 Some("graph".to_string()),
3859 Some(vec!["GL0".to_string(), "GL1".to_string()]),
3860 vec![
3861 Some(PersistedDiscreteBreakdownMetadata {
3862 axis_label: "graph".to_string(),
3863 fixed_coordinates: Vec::new(),
3864 bin_labels: vec!["GL0".to_string(), "GL1".to_string()],
3865 }),
3866 Some(PersistedDiscreteBreakdownMetadata {
3867 axis_label: "graph".to_string(),
3868 fixed_coordinates: Vec::new(),
3869 bin_labels: vec!["GL0".to_string(), "GL1".to_string()],
3870 }),
3871 ],
3872 );
3873 state.first_non_trivial_discrete_bin_descriptions =
3874 Some(vec!["GL0".to_string(), "GL1".to_string()]);
3875 state.iter = 2;
3876 state.num_points = 210_000;
3877 state.slot_stats = vec![
3878 make_statistics_counter(StatisticsFixture {
3879 precision: crate::settings::runtime::Precision::Double,
3880 total_time: Duration::from_micros(462),
3881 integrand_time: Duration::from_micros(414),
3882 evaluator_time: Duration::from_micros(279),
3883 parameterization_time: Duration::from_nanos(5_800),
3884 event_time: Duration::from_micros(12),
3885 generated_event_count: 7,
3886 accepted_event_count: 5,
3887 }),
3888 make_statistics_counter(StatisticsFixture {
3889 precision: crate::settings::runtime::Precision::Quad,
3890 total_time: Duration::from_micros(900),
3891 integrand_time: Duration::from_micros(810),
3892 evaluator_time: Duration::from_micros(120),
3893 parameterization_time: Duration::from_micros(34),
3894 event_time: Duration::from_micros(40),
3895 generated_event_count: 9,
3896 accepted_event_count: 6,
3897 }),
3898 ];
3899 state.stats = state.slot_stats[0].merged(&state.slot_stats[1]);
3900 state
3901 .stats
3902 .add_integrator_overhead(Duration::from_micros(110), 2);
3903 state.all_integrals = vec![
3904 make_accumulator(7.5e-5, 9.8e-5, 0.394, 3.2e-5, 1.5e-5, 0.378),
3905 make_accumulator(2.1e-5, 2.0e-6, 0.221, -1.7e-5, 3.0e-6, 0.187),
3906 ];
3907 if let Grid::Discrete(discrete_grid) = &mut state.sampling_state_for_slot_mut(0).grid {
3908 discrete_grid.bins[0].pdf = F(0.75);
3909 discrete_grid.bins[1].pdf = F(0.25);
3910 }
3911
3912 for summaries in [&mut state.slot_re_summaries, &mut state.slot_im_summaries] {
3913 for (slot_index, summary) in summaries.iter_mut().enumerate() {
3914 let summary = summary.as_mut().expect("discrete summary expected");
3915 summary.bins[0].accumulator.avg = F(1.0e-5 * (slot_index as f64 + 1.0));
3916 summary.bins[0].accumulator.err = F(2.0e-6);
3917 summary.bins[0].accumulator.chi_sq = F(0.2);
3918 summary.bins[0].accumulator.processed_samples = 150;
3919 summary.bins[0].accumulator.max_eval_positive = F(0.75);
3920 summary.bins[0].accumulator.max_eval_positive_xs =
3921 Some(Sample::Continuous(F(1.0), vec![F(0.25)]));
3922 summary.bins[1].accumulator.avg = F(5.0e-6 * (slot_index as f64 + 1.0));
3923 summary.bins[1].accumulator.err = F(1.0e-6);
3924 summary.bins[1].accumulator.chi_sq = F(0.1);
3925 summary.bins[1].accumulator.processed_samples = 50;
3926 summary.bins[1].accumulator.max_eval_positive = F(0.25);
3927 summary.bins[1].accumulator.max_eval_positive_xs =
3928 Some(Sample::Continuous(F(1.0), vec![F(0.75)]));
3929 }
3930 }
3931
3932 state
3933 }
3934
3935 fn make_single_graph_discrete_integration_state() -> IntegrationState {
3936 let mut state = make_discrete_integration_state();
3937 let Grid::Discrete(grid) = &mut state.sampling_state_for_slot_mut(0).grid else {
3938 unreachable!("discrete fixture must use a discrete grid")
3939 };
3940 grid.bins.truncate(1);
3941 grid.bins[0].pdf = F(1.0);
3942 for summaries in [&mut state.slot_re_summaries, &mut state.slot_im_summaries] {
3943 for summary in summaries.iter_mut().flatten() {
3944 summary.bins.truncate(1);
3945 }
3946 }
3947 state.first_non_trivial_discrete_bin_descriptions = Some(vec!["GL22".to_string()]);
3948 for metadata in &mut state.slot_first_non_trivial_discrete_breakdown_metadata {
3949 if let Some(metadata) = metadata.as_mut() {
3950 metadata.bin_labels = vec!["GL22".to_string()];
3951 }
3952 }
3953 state
3954 }
3955
3956 fn default_view_options() -> IntegrationStatusViewOptions {
3957 IntegrationStatusViewOptions {
3958 phase_display: IntegrationStatusPhaseDisplay::Both,
3959 training_phase_display: IntegrationStatusPhaseDisplay::Real,
3960 training_slot: 0,
3961 slot_training_phase_displays: vec![IntegrationStatusPhaseDisplay::Real],
3962 per_slot_training_phase: false,
3963 target_relative_accuracy: None,
3964 target_absolute_accuracy: None,
3965 show_statistics: true,
3966 show_max_weight_details: true,
3967 show_top_discrete_grid: false,
3968 show_discrete_contributions_sum: false,
3969 contribution_sort: ContributionSortMode::Error,
3970 show_max_weight_info_for_discrete_bins: false,
3971 }
3972 }
3973
3974 fn default_tabled_options() -> TabledRenderOptions {
3975 TabledRenderOptions {
3976 max_table_width: DEFAULT_MAX_SHARED_TABLE_WIDTH,
3977 show_statistics: true,
3978 show_max_weight_details: true,
3979 show_top_discrete_grid: false,
3980 show_discrete_contributions_sum: false,
3981 show_max_weight_info_for_discrete_bins: false,
3982 }
3983 }
3984
3985 fn tabled_options_for_view(view_options: &IntegrationStatusViewOptions) -> TabledRenderOptions {
3986 TabledRenderOptions {
3987 show_statistics: view_options.show_statistics,
3988 show_max_weight_details: view_options.show_max_weight_details,
3989 show_top_discrete_grid: view_options.show_top_discrete_grid,
3990 show_discrete_contributions_sum: view_options.show_discrete_contributions_sum,
3991 show_max_weight_info_for_discrete_bins: view_options
3992 .show_max_weight_info_for_discrete_bins,
3993 ..default_tabled_options()
3994 }
3995 }
3996
3997 #[test]
3998 fn correlated_core_iteration_state_populates_slot_statistics() {
3999 let settings_a = RuntimeSettings::default();
4000 let settings_b = RuntimeSettings::default();
4001 let integrand_a = Integrand::UnitVolume(UnitVolumeIntegrand::new(
4002 settings_a.clone(),
4003 UnitVolumeSettings { n_3d_momenta: 1 },
4004 ));
4005 let integrand_b = Integrand::UnitVolume(UnitVolumeIntegrand::new(
4006 settings_b.clone(),
4007 UnitVolumeSettings { n_3d_momenta: 1 },
4008 ));
4009 let sampling_grid_template = SamplingSlotState::from_integrand(&integrand_a).grid;
4010 let mut core_state = CoreIterationState::new(
4011 vec![integrand_a, integrand_b],
4012 SamplingCorrelationMode::Correlated,
4013 &[sampling_grid_template],
4014 1337,
4015 0,
4016 8,
4017 );
4018 let model = Model::default();
4019 let slot_settings = [&settings_a, &settings_b];
4020 let slot_models = [&model, &model];
4021 let current_max_evals = [Complex::new(F(0.0), F(0.0)), Complex::new(F(0.0), F(0.0))];
4022
4023 let processed = core_state
4024 .evaluate_chunk(&slot_settings, &slot_models, 0, ¤t_max_evals, 8)
4025 .expect("correlated chunk evaluation should succeed");
4026
4027 assert_eq!(processed, 8);
4028 assert!(
4029 core_state
4030 .slot_stats
4031 .iter()
4032 .all(|stats| stats.snapshot().num_evals == processed),
4033 "{:?}",
4034 core_state
4035 .slot_stats
4036 .iter()
4037 .map(|stats| stats.snapshot().num_evals)
4038 .collect_vec()
4039 );
4040 }
4041
4042 fn render_update(request: StatusUpdateBuildRequest<'_>) -> String {
4043 let tabled_options = tabled_options_for_view(request.render_options);
4044 render_status_update_tabled(&build_status_update(request), &tabled_options)
4045 }
4046
4047 fn render_ratatui_update(
4048 request: StatusUpdateBuildRequest<'_>,
4049 configure: impl FnOnce(&mut RatatuiDashboardState),
4050 ) -> String {
4051 let mut dashboard = RatatuiDashboardState::new();
4052 dashboard.update(build_status_update(request));
4053 configure(&mut dashboard);
4054
4055 let backend = TestBackend::new(180, 48);
4056 let mut terminal = Terminal::new(backend).expect("test terminal");
4057 terminal
4058 .draw(|frame| dashboard.draw(frame))
4059 .expect("ratatui draw");
4060
4061 let buffer = terminal.backend().buffer();
4062 (0..buffer.area.height)
4063 .map(|y| {
4064 let mut line = String::new();
4065 for x in 0..buffer.area.width {
4066 line.push_str(buffer[(x, y)].symbol());
4067 }
4068 line.trim_end().to_string()
4069 })
4070 .collect::<Vec<_>>()
4071 .join("\n")
4072 }
4073
4074 #[test]
4075 fn max_weight_details_table_renders_titled_table_without_wrapping_parentheses() {
4076 let mut accumulator = ComplexAccumulator::new();
4077 accumulator.re.max_eval_positive = F(2.3840672847728);
4078 accumulator.re.max_eval_positive_xs = Some(Sample::Discrete(
4079 F(1.0),
4080 0,
4081 Some(Box::new(Sample::Discrete(
4082 F(1.0),
4083 0,
4084 Some(Box::new(Sample::Continuous(
4085 F(1.0),
4086 vec![
4087 F(0.9695746085826609),
4088 F(0.5327714835649985),
4089 F(0.003410284786539597),
4090 ],
4091 ))),
4092 ))),
4093 ));
4094
4095 let mut state = IntegrationState::new_from_settings(
4096 SamplingCorrelationMode::Correlated,
4097 vec![SamplingSlotState::new(
4098 Grid::Continuous(ContinuousGrid::new(1, 64, 100, None, false)),
4099 Vec::new(),
4100 )],
4101 vec![SlotMeta {
4102 process_name: "proc".to_string(),
4103 integrand_name: "itg".to_string(),
4104 }],
4105 None,
4106 None,
4107 None,
4108 vec![None],
4109 );
4110 state.all_integrals = vec![accumulator];
4111 let view_options = IntegrationStatusViewOptions {
4112 show_statistics: false,
4113 show_top_discrete_grid: false,
4114 show_discrete_contributions_sum: false,
4115 ..default_view_options()
4116 };
4117 let rendered = render_status_update_tabled(
4118 &build_status_update(
4119 StatusUpdateBuildRequest::new(
4120 IntegrationStatusKind::Iteration,
4121 &state,
4122 &[None],
4123 &view_options,
4124 )
4125 .with_timing(
4126 1,
4127 Duration::from_secs(0),
4128 Duration::from_secs(0),
4129 0,
4130 0,
4131 0,
4132 ),
4133 ),
4134 &default_tabled_options(),
4135 );
4136
4137 assert!(rendered.contains("Maximum weight details"), "{rendered}");
4138 assert!(rendered.contains("Integrand"), "{rendered}");
4139 assert!(rendered.contains("Max eval"), "{rendered}");
4140 assert!(rendered.contains("Max eval coordinates"), "{rendered}");
4141 assert!(rendered.contains("proc@itg"), "{rendered}");
4142 assert!(rendered.contains("re [+]"), "{rendered}");
4143 assert!(rendered.contains("idx: 0, idx: 0, xs: [ "), "{rendered}");
4144 assert!(rendered.contains("e-01"), "{rendered}");
4145 assert!(rendered.contains("e-03 ]"), "{rendered}");
4146 assert!(!rendered.contains(", 0.532"), "{rendered}");
4147 assert!(!rendered.contains("( graph:"), "{rendered}");
4148 }
4149
4150 #[test]
4151 fn iteration_status_block_uses_compact_header_and_optional_statistics() {
4152 let state = make_integration_state();
4153 let view_options = IntegrationStatusViewOptions {
4154 show_statistics: false,
4155 show_max_weight_details: false,
4156 ..default_view_options()
4157 };
4158 let rendered = render_update(
4159 StatusUpdateBuildRequest::new(
4160 IntegrationStatusKind::Iteration,
4161 &state,
4162 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4163 &view_options,
4164 )
4165 .with_timing(
4166 4,
4167 Duration::from_secs(1),
4168 Duration::from_secs(1),
4169 100_000,
4170 100_000,
4171 100_000,
4172 ),
4173 );
4174
4175 assert!(
4176 rendered.contains("Iteration # 1 ( completed )"),
4177 "{rendered}"
4178 );
4179 assert!(
4180 rendered.contains("# samples per iteration = 100.00K # samples total = 100.00K"),
4181 "{rendered}"
4182 );
4183 assert!(rendered.contains("/sample/core (4 cores)"), "{rendered}");
4184 assert!(rendered.contains("Contribution"), "{rendered}");
4185 assert!(rendered.contains("proc_a@itg_a"), "{rendered}");
4186 assert!(rendered.contains("proc_b@itg_b"), "{rendered}");
4187 assert!(rendered.contains("+7.5(9.8)e-5"), "{rendered}");
4188 assert!(rendered.contains("Δ = 0.255σ"), "{rendered}");
4189 assert!(!rendered.contains("Integration statistics"), "{rendered}");
4190 let lines = rendered.lines().collect::<Vec<_>>();
4191 assert_eq!(lines[1].matches('│').count(), 2, "{rendered}");
4192 assert!(lines[2].contains('┬'), "{rendered}");
4193 assert!(
4194 lines.last().is_some_and(|line| line.contains('┴')),
4195 "{rendered}"
4196 );
4197 }
4198
4199 #[test]
4200 fn iteration_status_block_omits_delta_columns_when_no_target_is_provided() {
4201 let state = make_integration_state();
4202 let view_options = IntegrationStatusViewOptions {
4203 show_statistics: true,
4204 show_max_weight_details: false,
4205 ..default_view_options()
4206 };
4207 let rendered = render_update(
4208 StatusUpdateBuildRequest::new(
4209 IntegrationStatusKind::Iteration,
4210 &state,
4211 &[None, None],
4212 &view_options,
4213 )
4214 .with_timing(
4215 4,
4216 Duration::from_secs(1),
4217 Duration::from_secs(1),
4218 100_000,
4219 100_000,
4220 100_000,
4221 ),
4222 );
4223
4224 assert!(!rendered.contains("Δ [σ]"), "{rendered}");
4225 assert!(!rendered.contains("Δ ="), "{rendered}");
4226 assert!(rendered.contains("Integration statistics"), "{rendered}");
4227 assert!(rendered.contains("mwi"), "{rendered}");
4228 }
4229
4230 #[test]
4231 fn tabled_statistics_panel_uses_global_scope_label() {
4232 let state = make_integration_state();
4233 let view_options = IntegrationStatusViewOptions {
4234 show_statistics: true,
4235 show_max_weight_details: false,
4236 ..default_view_options()
4237 };
4238 let rendered = render_update(
4239 StatusUpdateBuildRequest::new(
4240 IntegrationStatusKind::Iteration,
4241 &state,
4242 &[None, None],
4243 &view_options,
4244 )
4245 .with_timing(
4246 4,
4247 Duration::from_secs(1),
4248 Duration::from_secs(1),
4249 100_000,
4250 100_000,
4251 100_000,
4252 ),
4253 );
4254
4255 assert!(
4256 rendered.contains("Integration statistics [global]"),
4257 "{rendered}"
4258 );
4259 }
4260
4261 #[test]
4262 fn live_iteration_status_block_uses_progress_header() {
4263 let mut state = make_integration_state();
4264 state.iter = 2;
4265 state.num_points = 125_000;
4266 let view_options = IntegrationStatusViewOptions {
4267 show_statistics: false,
4268 show_max_weight_details: false,
4269 ..default_view_options()
4270 };
4271 let rendered = render_update(
4272 StatusUpdateBuildRequest::new(
4273 IntegrationStatusKind::Live,
4274 &state,
4275 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4276 &view_options,
4277 )
4278 .with_timing(
4279 4,
4280 Duration::from_secs(1),
4281 Duration::from_secs(1),
4282 25_000,
4283 125_000,
4284 125_000,
4285 )
4286 .with_live_progress(Some(status_update::LiveIterationProgress {
4287 completed_points: 25_000,
4288 target_points: 100_000,
4289 })),
4290 );
4291
4292 assert!(
4293 rendered.contains("Iteration # 2 ( running )"),
4294 "{rendered}"
4295 );
4296 assert!(
4297 rendered.contains("Iteration progress 25.00K/100.00K"),
4298 "{rendered}"
4299 );
4300 assert!(rendered.contains("25.0%"), "{rendered}");
4301 assert!(rendered.contains("# samples total = 125.00K"), "{rendered}");
4302 }
4303
4304 #[test]
4305 fn iteration_status_block_shows_discrete_contributions_and_discrete_max_weights() {
4306 let state = make_discrete_integration_state();
4307 let view_options = IntegrationStatusViewOptions {
4308 show_statistics: false,
4309 show_max_weight_details: true,
4310 show_top_discrete_grid: true,
4311 show_discrete_contributions_sum: true,
4312 contribution_sort: ContributionSortMode::Index,
4313 show_max_weight_info_for_discrete_bins: true,
4314 ..default_view_options()
4315 };
4316 let rendered = render_update(
4317 StatusUpdateBuildRequest::new(
4318 IntegrationStatusKind::Iteration,
4319 &state,
4320 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4321 &view_options,
4322 )
4323 .with_timing(
4324 4,
4325 Duration::from_secs(2),
4326 Duration::from_secs(2),
4327 110_000,
4328 210_000,
4329 210_000,
4330 ),
4331 );
4332
4333 assert!(rendered.contains("Sum"), "{rendered}");
4334 assert!(rendered.contains("Contribution (idx=graph)"), "{rendered}");
4335 assert!(
4336 !rendered.contains("│ (idx=graph) │"),
4337 "{rendered}"
4338 );
4339 assert!(rendered.contains("#0: GL0"), "{rendered}");
4340 assert!(rendered.contains("75.0%"), "{rendered}");
4341 assert!(rendered.contains("25.0%"), "{rendered}");
4342 assert!(
4343 rendered.contains("Maximum weight details by discrete bin"),
4344 "{rendered}"
4345 );
4346 assert!(
4347 rendered.contains("xs: [ 2.5000000000000000e-01 ]"),
4348 "{rendered}"
4349 );
4350 }
4351
4352 #[test]
4353 fn grouped_graph_descriptions_include_all_group_members() {
4354 let description =
4355 graph_group_description(["GL0".to_string(), "GL1".to_string(), "GL2".to_string()]);
4356
4357 assert_eq!(description, "[GL0,GL1,GL2]");
4358 }
4359
4360 #[test]
4361 fn explicit_single_graph_subset_monitors_the_root_graph_axis() {
4362 let grid = Grid::Discrete(DiscreteGrid::new(
4363 vec![Some(Grid::Continuous(ContinuousGrid::new(
4364 1, 64, 100, None, false,
4365 )))],
4366 F(10.0),
4367 false,
4368 ));
4369 let labels = vec!["graph".to_string()];
4370
4371 assert!(monitored_discrete_layout(&grid, &labels, false).is_none());
4372 assert_eq!(
4373 monitored_discrete_layout(&grid, &labels, true),
4374 Some((Vec::new(), "graph".to_string(), 1))
4375 );
4376 }
4377
4378 #[test]
4379 fn tabled_single_graph_subset_keeps_the_graph_row() {
4380 let state = make_single_graph_discrete_integration_state();
4381 let view_options = IntegrationStatusViewOptions {
4382 show_statistics: false,
4383 show_max_weight_details: false,
4384 show_top_discrete_grid: true,
4385 contribution_sort: ContributionSortMode::Index,
4386 ..default_view_options()
4387 };
4388 let rendered = render_update(StatusUpdateBuildRequest::new(
4389 IntegrationStatusKind::Iteration,
4390 &state,
4391 &[None, None],
4392 &view_options,
4393 ));
4394
4395 assert!(rendered.contains("Contribution (idx=graph)"), "{rendered}");
4396 assert!(rendered.contains("#0: GL22"), "{rendered}");
4397 assert!(!rendered.contains("GL1"), "{rendered}");
4398 }
4399
4400 #[test]
4401 fn iteration_status_block_hides_spanned_metadata_header_separators() {
4402 let state = make_discrete_integration_state();
4403 let view_options = IntegrationStatusViewOptions {
4404 show_statistics: false,
4405 show_max_weight_details: false,
4406 show_top_discrete_grid: true,
4407 show_discrete_contributions_sum: true,
4408 contribution_sort: ContributionSortMode::Index,
4409 show_max_weight_info_for_discrete_bins: false,
4410 ..default_view_options()
4411 };
4412 let rendered = render_update(
4413 StatusUpdateBuildRequest::new(
4414 IntegrationStatusKind::Iteration,
4415 &state,
4416 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4417 &view_options,
4418 )
4419 .with_timing(
4420 4,
4421 Duration::from_secs(2),
4422 Duration::from_secs(2),
4423 110_000,
4424 210_000,
4425 210_000,
4426 ),
4427 );
4428
4429 let header_line = rendered
4430 .lines()
4431 .find(|line| line.contains("χ²/dof") && line.contains("mwi"))
4432 .expect("expected spanned metadata header line");
4433
4434 let chi_to_mwi =
4435 &header_line[header_line.find("χ²/dof").unwrap()..header_line.find("mwi").unwrap()];
4436 assert!(!chi_to_mwi.contains('│'), "{rendered}");
4437
4438 let delta_sigma_to_percent =
4439 &header_line[header_line.find("Δ [σ]").unwrap()..header_line.find("Δ [%]").unwrap()];
4440 assert!(!delta_sigma_to_percent.contains('│'), "{rendered}");
4441 }
4442
4443 #[test]
4444 fn integration_result_always_contains_first_non_trivial_discrete_breakdown() {
4445 let state = make_discrete_integration_state();
4446 let result =
4447 build_integration_result(&state, &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None]);
4448
4449 let slot = result
4450 .slot("proc_a@itg_a")
4451 .expect("discrete slot must be present");
4452 let breakdown = slot
4453 .grid_breakdown
4454 .re
4455 .as_ref()
4456 .expect("discrete breakdown must be persisted");
4457
4458 assert_eq!(breakdown.axis_label, "graph");
4459 assert!(breakdown.fixed_coordinates.is_empty());
4460 assert_eq!(breakdown.entries.len(), 2);
4461 assert_eq!(breakdown.entries[0].bin_index, 0);
4462 assert_eq!(breakdown.entries[0].bin_label.as_deref(), Some("GL0"));
4463 assert_eq!(breakdown.entries[0].pdf, F(0.75));
4464 assert_eq!(breakdown.entries[0].processed_samples, 150);
4465 assert_eq!(breakdown.entries[1].bin_index, 1);
4466 assert_eq!(breakdown.entries[1].bin_label.as_deref(), Some("GL1"));
4467 assert_eq!(breakdown.entries[1].pdf, F(0.25));
4468 assert_eq!(breakdown.entries[1].processed_samples, 50);
4469 }
4470
4471 #[test]
4472 fn orientation_descriptions_use_colored_signs() {
4473 let mut state = make_discrete_integration_state();
4474 state.first_non_trivial_discrete_label = Some("orientation".to_string());
4475 state.first_non_trivial_discrete_bin_descriptions =
4476 Some(vec!["+-0".to_string(), "0++".to_string()]);
4477 for metadata in &mut state.slot_first_non_trivial_discrete_breakdown_metadata {
4478 if let Some(metadata) = metadata.as_mut() {
4479 metadata.axis_label = "orientation".to_string();
4480 metadata.bin_labels = vec!["+-0".to_string(), "0++".to_string()];
4481 }
4482 }
4483
4484 control::set_override(true);
4485 let expected_plus = "+".green().bold().to_string();
4486 let expected_minus = "-".red().bold().to_string();
4487
4488 let view_options = IntegrationStatusViewOptions {
4489 show_statistics: false,
4490 show_max_weight_details: false,
4491 show_top_discrete_grid: true,
4492 show_discrete_contributions_sum: false,
4493 contribution_sort: ContributionSortMode::Index,
4494 show_max_weight_info_for_discrete_bins: false,
4495 ..default_view_options()
4496 };
4497 let ansi = render_tabled::render_status_update(
4498 &build_status_update(
4499 StatusUpdateBuildRequest::new(
4500 IntegrationStatusKind::Iteration,
4501 &state,
4502 &[None, None],
4503 &view_options,
4504 )
4505 .with_timing(
4506 1,
4507 Duration::from_secs(0),
4508 Duration::from_secs(0),
4509 0,
4510 0,
4511 0,
4512 ),
4513 ),
4514 &tabled_options_for_view(&view_options),
4515 );
4516 control::set_override(false);
4517
4518 assert!(ansi.contains(&expected_plus), "{ansi}");
4519 assert!(ansi.contains(&expected_minus), "{ansi}");
4520 assert!(ansi.contains("0"), "{ansi}");
4521 }
4522
4523 #[test]
4524 fn mismatched_bin_descriptions_fall_back_to_indices_with_warning() {
4525 let (descriptions, warning) = coalesce_first_non_trivial_discrete_bin_descriptions(
4526 "graph",
4527 &[
4528 (
4529 "proc_a@itg_a".to_string(),
4530 vec!["GL0".to_string(), "GL1".to_string()],
4531 ),
4532 (
4533 "proc_b@itg_b".to_string(),
4534 vec!["GX0".to_string(), "GX1".to_string()],
4535 ),
4536 ],
4537 );
4538
4539 assert!(descriptions.is_none());
4540 let warning = warning.expect("mismatch should produce a warning");
4541 assert!(warning.contains("graph"), "{warning}");
4542 assert!(warning.contains("proc_a@itg_a"), "{warning}");
4543 assert!(warning.contains("proc_b@itg_b"), "{warning}");
4544 }
4545
4546 #[test]
4547 fn format_max_eval_sample_keeps_full_discrete_coordinates() {
4548 let axis_labels = vec!["graph".to_string(), "LMB channel".to_string()];
4549 let full_sample = Sample::Discrete(
4550 F(1.0),
4551 0,
4552 Some(Box::new(Sample::Discrete(
4553 F(1.0),
4554 1,
4555 Some(Box::new(Sample::Continuous(F(1.0), vec![F(0.25)]))),
4556 ))),
4557 );
4558 let nested_sample = Sample::Discrete(
4559 F(1.0),
4560 1,
4561 Some(Box::new(Sample::Continuous(F(1.0), vec![F(0.75)]))),
4562 );
4563
4564 assert_eq!(
4565 display::format_max_eval_sample(&full_sample, &axis_labels, &[]),
4566 "graph: 0, LMB channel: 1, xs: [ 2.5000000000000000e-01 ]"
4567 );
4568 assert_eq!(
4569 display::format_max_eval_sample(&nested_sample, &axis_labels, &[0]),
4570 "graph: 0, LMB channel: 1, xs: [ 7.5000000000000000e-01 ]"
4571 );
4572 }
4573
4574 #[test]
4575 fn format_max_eval_sample_wraps_long_coordinate_lists() {
4576 let axis_labels = vec!["graph".to_string(), "orientation".to_string()];
4577 let sample = Sample::Discrete(
4578 F(1.0),
4579 0,
4580 Some(Box::new(Sample::Discrete(
4581 F(1.0),
4582 9,
4583 Some(Box::new(Sample::Continuous(
4584 F(1.0),
4585 vec![
4586 F(0.125),
4587 F(0.25),
4588 F(0.375),
4589 F(0.5),
4590 F(0.625),
4591 F(0.75),
4592 F(0.875),
4593 ],
4594 ))),
4595 ))),
4596 );
4597
4598 assert_eq!(
4599 display::format_max_eval_sample(&sample, &axis_labels, &[]),
4600 "graph: 0, orientation: 9, xs: [\n1.2500000000000000e-01 2.5000000000000000e-01 3.7500000000000000e-01\n5.0000000000000000e-01 6.2500000000000000e-01 7.5000000000000000e-01\n8.7500000000000000e-01 ]"
4601 );
4602 }
4603
4604 #[test]
4605 fn discrete_bin_prefixes_use_fixed_width_within_each_range() {
4606 assert_eq!(status_update::format_discrete_bin_prefix(2, 99), "#2: ");
4607 assert_eq!(status_update::format_discrete_bin_prefix(12, 99), "#12:");
4608 assert_eq!(status_update::format_discrete_bin_prefix(2, 999), "#2: ");
4609 assert_eq!(status_update::format_discrete_bin_prefix(12, 999), "#12: ");
4610 assert_eq!(status_update::format_discrete_bin_prefix(123, 999), "#123:");
4611 assert_eq!(
4612 status_update::format_discrete_bin_prefix(2, 9_999),
4613 "#2: "
4614 );
4615 assert_eq!(
4616 status_update::format_discrete_bin_prefix(1234, 9_999),
4617 "#1234:"
4618 );
4619 }
4620
4621 #[test]
4622 fn final_summary_omits_discrete_max_weight_block_when_disabled() {
4623 let state = make_discrete_integration_state();
4624 let rendered = render_saved_integration_summary(
4625 &state,
4626 &[None, None],
4627 &IntegrationStatusViewOptions {
4628 show_statistics: true,
4629 show_max_weight_details: true,
4630 show_top_discrete_grid: false,
4631 show_discrete_contributions_sum: false,
4632 contribution_sort: ContributionSortMode::Index,
4633 show_max_weight_info_for_discrete_bins: false,
4634 ..default_view_options()
4635 },
4636 &default_tabled_options(),
4637 );
4638
4639 assert!(rendered.contains("Maximum weight details"), "{rendered}");
4640 assert!(
4641 !rendered.contains("Maximum weight details by discrete bin"),
4642 "{rendered}"
4643 );
4644 assert!(
4645 !rendered.contains("# samples per iteration = 0"),
4646 "{rendered}"
4647 );
4648 }
4649
4650 #[test]
4651 fn results_output_summary_renders_tabled_workspace_rows() {
4652 let workspace = Path::new("/tmp/gl_workspace");
4653 let rendered = render_results_output_summary_table(Some(workspace), &[], &[])
4654 .expect("workspace summary should render");
4655
4656 assert!(
4657 rendered.contains("Integration results emitted"),
4658 "{rendered}"
4659 );
4660 assert!(rendered.contains("type"), "{rendered}");
4661 assert!(rendered.contains("path"), "{rendered}");
4662 assert!(rendered.contains("workspace path"), "{rendered}");
4663 assert!(rendered.contains("/tmp/gl_workspace"), "{rendered}");
4664 assert!(rendered.contains("results"), "{rendered}");
4665 assert!(rendered.contains("integration_result.json"), "{rendered}");
4666 assert!(rendered.contains("├"), "{rendered}");
4667 assert!(!rendered.contains("iteration snapshots"), "{rendered}");
4668 assert!(!rendered.contains("iter_*"), "{rendered}");
4669 }
4670
4671 #[test]
4672 fn results_output_summary_skips_missing_observable_outputs() {
4673 let settings = RuntimeSettings::default();
4674 let slot = IntegrationSlot::new(
4675 SlotMeta {
4676 process_name: "proc".to_string(),
4677 integrand_name: "default".to_string(),
4678 },
4679 settings.clone(),
4680 Model::default(),
4681 Integrand::UnitVolume(UnitVolumeIntegrand::new(
4682 settings,
4683 UnitVolumeSettings { n_3d_momenta: 1 },
4684 )),
4685 None,
4686 );
4687 let rendered = render_results_output_summary_table(
4688 None,
4689 &[slot],
4690 &[vec![PathBuf::from(
4691 "/tmp/definitely_missing_observables_final.json",
4692 )]],
4693 );
4694
4695 assert!(rendered.is_none(), "{rendered:?}");
4696 }
4697
4698 #[test]
4699 fn results_output_summary_lists_all_emitted_observable_files() {
4700 let base = std::env::temp_dir().join(format!(
4701 "gammaloop_results_output_summary_{}",
4702 std::process::id()
4703 ));
4704 if base.exists() {
4705 let _ = fs::remove_dir_all(&base);
4706 }
4707 let workspace = base.join("workspace");
4708 let slot_dir = workspace.join("integrands").join("proc@default");
4709 fs::create_dir_all(&slot_dir).unwrap();
4710 fs::write(workspace_result_snapshot_path(&workspace), b"{}").unwrap();
4711 let json_path = slot_dir.join("observables_final.json");
4712 let hwu_path = slot_dir.join("observables_final.hwu");
4713 fs::write(&json_path, b"{}").unwrap();
4714 fs::write(&hwu_path, b"# hwu\n").unwrap();
4715
4716 let settings = RuntimeSettings::default();
4717 let slot = IntegrationSlot::new(
4718 SlotMeta {
4719 process_name: "proc".to_string(),
4720 integrand_name: "default".to_string(),
4721 },
4722 settings.clone(),
4723 Model::default(),
4724 Integrand::UnitVolume(UnitVolumeIntegrand::new(
4725 settings,
4726 UnitVolumeSettings { n_3d_momenta: 1 },
4727 )),
4728 None,
4729 );
4730 let rendered = render_results_output_summary_table(
4731 Some(&workspace),
4732 &[slot],
4733 &[vec![hwu_path.clone(), json_path.clone()]],
4734 )
4735 .expect("summary should render");
4736
4737 assert!(rendered.contains("proc@default (hwu)"), "{rendered}");
4738 assert!(rendered.contains("proc@default (json)"), "{rendered}");
4739 assert!(rendered.contains("observables_final.hwu"), "{rendered}");
4740 assert!(rendered.contains("observables_final.json"), "{rendered}");
4741
4742 fs::remove_dir_all(&base).unwrap();
4743 }
4744
4745 #[test]
4746 fn ratatui_overview_shows_eta_and_all_slot_metrics() {
4747 let state = make_integration_state();
4748 let view_options = IntegrationStatusViewOptions {
4749 show_statistics: true,
4750 show_max_weight_details: false,
4751 ..default_view_options()
4752 };
4753 let rendered = render_ratatui_update(
4754 StatusUpdateBuildRequest::new(
4755 IntegrationStatusKind::Live,
4756 &state,
4757 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4758 &view_options,
4759 )
4760 .with_timing(
4761 4,
4762 Duration::from_secs(15),
4763 Duration::from_secs(10),
4764 25_000,
4765 125_000,
4766 125_000,
4767 )
4768 .with_live_progress(Some(status_update::LiveIterationProgress {
4769 completed_points: 25_000,
4770 target_points: 100_000,
4771 })),
4772 |_| {},
4773 );
4774
4775 assert!(rendered.contains("Iteration progress"), "{rendered}");
4776 assert!(rendered.contains("ETA 30s"), "{rendered}");
4777 assert!(rendered.contains("25.00K / 100.00K (25.0%)"), "{rendered}");
4778 assert!(rendered.contains("# samples total 125.00K"), "{rendered}");
4779 assert!(rendered.contains("#samples/s"), "{rendered}");
4780 assert!(
4781 rendered.contains("480µs /sample/core (4 cores)"),
4782 "{rendered}"
4783 );
4784 assert!(rendered.contains("/sample/core (4 cores)"), "{rendered}");
4785 assert!(rendered.contains("Integrands"), "{rendered}");
4786 assert!(rendered.contains("Focused integrand"), "{rendered}");
4787 assert!(rendered.contains("Results summary"), "{rendered}");
4788 assert!(
4789 rendered.contains("Integration statistics [global]"),
4790 "{rendered}"
4791 );
4792 assert!(rendered.contains("% err"), "{rendered}");
4793 assert!(rendered.contains("chi^2"), "{rendered}");
4794 assert!(rendered.contains("m.w.i"), "{rendered}");
4795 assert!(
4796 rendered.contains("Timing composition [global]"),
4797 "{rendered}"
4798 );
4799 assert!(rendered.contains("Precision mix [global]"), "{rendered}");
4800 }
4801
4802 #[test]
4803 fn live_status_updates_use_current_batch_statistics_in_bottom_panels() {
4804 let state = make_integration_state();
4805 let slots = make_preview_test_slots(&state.slot_metas);
4806 let core_state = make_preview_test_core_state(&state);
4807 let preview_state = build_preview_integration_state(
4808 &state,
4809 &slots,
4810 4,
4811 core_state.completed_points,
4812 1.5,
4813 &[core_state],
4814 );
4815 let view_options = IntegrationStatusViewOptions {
4816 show_statistics: true,
4817 show_max_weight_details: false,
4818 ..default_view_options()
4819 };
4820
4821 let initial_update = build_status_update(
4822 StatusUpdateBuildRequest::new(
4823 IntegrationStatusKind::Live,
4824 &state,
4825 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4826 &view_options,
4827 )
4828 .with_timing(
4829 4,
4830 Duration::from_secs(1),
4831 Duration::from_millis(400),
4832 0,
4833 state.num_points,
4834 state.num_points,
4835 )
4836 .with_live_progress(Some(status_update::LiveIterationProgress {
4837 completed_points: 0,
4838 target_points: 100,
4839 })),
4840 );
4841 let preview_update = build_status_update(
4842 StatusUpdateBuildRequest::new(
4843 IntegrationStatusKind::Live,
4844 &preview_state,
4845 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4846 &view_options,
4847 )
4848 .with_timing(
4849 4,
4850 Duration::from_millis(1500),
4851 Duration::from_millis(900),
4852 12,
4853 preview_state.num_points,
4854 preview_state.num_points,
4855 )
4856 .with_live_progress(Some(status_update::LiveIterationProgress {
4857 completed_points: 12,
4858 target_points: 100,
4859 })),
4860 );
4861
4862 let initial_statistics = initial_update
4863 .statistics
4864 .as_ref()
4865 .expect("statistics should be present");
4866 let preview_statistics = preview_update
4867 .statistics
4868 .as_ref()
4869 .expect("statistics should be present");
4870 let initial_snapshot = initial_statistics.global_snapshot();
4871 let preview_snapshot = preview_statistics.global_snapshot();
4872
4873 assert!(preview_snapshot.num_evals > initial_snapshot.num_evals);
4874 assert!(preview_snapshot.generated_event_count > initial_snapshot.generated_event_count);
4875 assert!(preview_snapshot.accepted_event_count > initial_snapshot.accepted_event_count);
4876
4877 let initial_timing_mix = initial_statistics
4878 .timing_mix_segments(status_update::StatisticsScope::Global)
4879 .iter()
4880 .map(|segment| segment.percentage)
4881 .collect_vec();
4882 let preview_timing_mix = preview_statistics
4883 .timing_mix_segments(status_update::StatisticsScope::Global)
4884 .iter()
4885 .map(|segment| segment.percentage)
4886 .collect_vec();
4887 assert_ne!(preview_timing_mix, initial_timing_mix);
4888
4889 let initial_precision_mix = initial_statistics
4890 .precision_mix_segments(status_update::StatisticsScope::Global)
4891 .iter()
4892 .map(|segment| segment.percentage)
4893 .collect_vec();
4894 let preview_precision_mix = preview_statistics
4895 .precision_mix_segments(status_update::StatisticsScope::Global)
4896 .iter()
4897 .map(|segment| segment.percentage)
4898 .collect_vec();
4899 assert_ne!(preview_precision_mix, initial_precision_mix);
4900 assert!(
4901 preview_precision_mix
4902 .iter()
4903 .any(|percentage| (*percentage - 50.0).abs() < f64::EPSILON),
4904 "{preview_precision_mix:?}"
4905 );
4906
4907 let preview_render = render_ratatui_update(
4908 StatusUpdateBuildRequest::new(
4909 IntegrationStatusKind::Live,
4910 &preview_state,
4911 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4912 &view_options,
4913 )
4914 .with_timing(
4915 4,
4916 Duration::from_millis(1500),
4917 Duration::from_millis(900),
4918 12,
4919 preview_state.num_points,
4920 preview_state.num_points,
4921 )
4922 .with_live_progress(Some(status_update::LiveIterationProgress {
4923 completed_points: 12,
4924 target_points: 100,
4925 })),
4926 |_| {},
4927 );
4928 let preview_total_time = crate::utils::format_evaluation_time_from_f64(
4929 preview_snapshot.average_total_time_seconds,
4930 );
4931 assert!(
4932 preview_render.contains(&preview_total_time),
4933 "{preview_render}"
4934 );
4935 assert!(
4936 preview_render.contains("Precision mix [global]"),
4937 "{preview_render}"
4938 );
4939 assert!(
4940 preview_render.contains("Timing composition [global]"),
4941 "{preview_render}"
4942 );
4943 }
4944
4945 #[test]
4946 fn ratatui_statistics_panels_can_toggle_to_focused_slot_scope() {
4947 let state = make_integration_state();
4948 let view_options = IntegrationStatusViewOptions {
4949 show_statistics: true,
4950 show_max_weight_details: false,
4951 ..default_view_options()
4952 };
4953 let rendered = render_ratatui_update(
4954 StatusUpdateBuildRequest::new(
4955 IntegrationStatusKind::Live,
4956 &state,
4957 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
4958 &view_options,
4959 )
4960 .with_timing(
4961 4,
4962 Duration::from_secs(15),
4963 Duration::from_secs(10),
4964 25_000,
4965 125_000,
4966 125_000,
4967 ),
4968 |dashboard| {
4969 dashboard.focus_next_slot();
4970 dashboard.toggle_statistics_scope();
4971 },
4972 );
4973
4974 assert!(
4975 rendered.contains("Integration statistics [proc_b@itg_b]"),
4976 "{rendered}"
4977 );
4978 assert!(
4979 rendered.contains("Timing composition [proc_b@itg_b]"),
4980 "{rendered}"
4981 );
4982 assert!(
4983 rendered.contains("Precision mix [proc_b@itg_b]"),
4984 "{rendered}"
4985 );
4986 }
4987
4988 #[test]
4989 fn slot_scoped_statistics_rows_hide_shared_integrator_overhead() {
4990 let state = make_integration_state();
4991 let update = build_status_update(
4992 StatusUpdateBuildRequest::new(
4993 IntegrationStatusKind::Iteration,
4994 &state,
4995 &[None, None],
4996 &default_view_options(),
4997 )
4998 .with_timing(
4999 4,
5000 Duration::from_secs(1),
5001 Duration::from_secs(1),
5002 100_000,
5003 100_000,
5004 100_000,
5005 ),
5006 );
5007 let statistics = update.statistics.expect("statistics section should exist");
5008 let rows = statistics.table_rows(status_update::StatisticsScope::Slot(1));
5009 let integrator_entry = rows[2]
5010 .entries
5011 .last()
5012 .expect("integrator entry should be present");
5013
5014 assert_eq!(integrator_entry.label.to_plain_string(), "integrator");
5015 assert_eq!(integrator_entry.value.to_plain_string(), "N/A");
5016 }
5017
5018 #[test]
5019 fn ratatui_overview_chart_phase_toggle_updates_convergence_label() {
5020 let state = make_integration_state();
5021 let view_options = IntegrationStatusViewOptions {
5022 show_statistics: false,
5023 show_max_weight_details: false,
5024 ..default_view_options()
5025 };
5026 let rendered = render_ratatui_update(
5027 StatusUpdateBuildRequest::new(
5028 IntegrationStatusKind::Live,
5029 &state,
5030 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
5031 &view_options,
5032 )
5033 .with_timing(
5034 4,
5035 Duration::from_secs(15),
5036 Duration::from_secs(10),
5037 25_000,
5038 125_000,
5039 125_000,
5040 )
5041 .with_live_progress(Some(status_update::LiveIterationProgress {
5042 completed_points: 25_000,
5043 target_points: 100_000,
5044 })),
5045 |dashboard| dashboard.toggle_chart_component(),
5046 );
5047
5048 assert!(
5049 rendered.contains("Convergence : imag (not selected for training)"),
5050 "{rendered}"
5051 );
5052 }
5053
5054 #[test]
5055 fn ratatui_integrand_targets_are_trimmed_for_display() {
5056 let state = make_integration_state();
5057 let view_options = IntegrationStatusViewOptions {
5058 show_statistics: false,
5059 show_max_weight_details: false,
5060 ..default_view_options()
5061 };
5062 let rendered = render_ratatui_update(
5063 StatusUpdateBuildRequest::new(
5064 IntegrationStatusKind::Live,
5065 &state,
5066 &[
5067 Some(Complex::new(
5068 F(7.600000000000001e-6),
5069 F(7.430000000000004e-5),
5070 )),
5071 Some(Complex::new(F(6.629999999999999e-5), F(0.0))),
5072 ],
5073 &view_options,
5074 )
5075 .with_timing(
5076 4,
5077 Duration::from_secs(15),
5078 Duration::from_secs(10),
5079 25_000,
5080 125_000,
5081 125_000,
5082 )
5083 .with_live_progress(Some(status_update::LiveIterationProgress {
5084 completed_points: 25_000,
5085 target_points: 100_000,
5086 })),
5087 |_| {},
5088 );
5089
5090 assert!(rendered.contains("trgt"), "{rendered}");
5091 assert!(rendered.contains("+7.6e-6"), "{rendered}");
5092 assert!(rendered.contains("+7.43e-5"), "{rendered}");
5093 assert!(rendered.contains("+6.63e-5"), "{rendered}");
5094 assert!(rendered.contains("+0e0"), "{rendered}");
5095 }
5096
5097 #[test]
5098 fn ratatui_overview_shows_eta_to_target_when_configured() {
5099 let state = make_integration_state();
5100 let view_options = IntegrationStatusViewOptions {
5101 target_relative_accuracy: Some(0.05),
5102 show_statistics: false,
5103 show_max_weight_details: false,
5104 ..default_view_options()
5105 };
5106 let rendered = render_ratatui_update(
5107 StatusUpdateBuildRequest::new(
5108 IntegrationStatusKind::Live,
5109 &state,
5110 &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
5111 &view_options,
5112 )
5113 .with_timing(
5114 4,
5115 Duration::from_secs(25),
5116 Duration::from_secs(10),
5117 25_000,
5118 125_000,
5119 125_000,
5120 )
5121 .with_live_progress(Some(status_update::LiveIterationProgress {
5122 completed_points: 25_000,
5123 target_points: 100_000,
5124 })),
5125 |_| {},
5126 );
5127
5128 assert!(rendered.contains("ETA to target"), "{rendered}");
5129 assert!(rendered.contains("(% err <= 5%)"), "{rendered}");
5130 }
5131
5132 #[test]
5133 fn eta_to_target_specification_formats_relative_and_absolute_targets() {
5134 let state = make_integration_state();
5135 let relative_view_options = IntegrationStatusViewOptions {
5136 target_relative_accuracy: Some(0.05),
5137 ..default_view_options()
5138 };
5139 let update = build_status_update(
5140 StatusUpdateBuildRequest::new(
5141 IntegrationStatusKind::Live,
5142 &state,
5143 &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
5144 &relative_view_options,
5145 )
5146 .with_timing(
5147 4,
5148 Duration::from_secs(25),
5149 Duration::from_secs(10),
5150 25_000,
5151 125_000,
5152 125_000,
5153 )
5154 .with_live_progress(Some(status_update::LiveIterationProgress {
5155 completed_points: 25_000,
5156 target_points: 100_000,
5157 })),
5158 );
5159 assert_eq!(
5160 update.meta.eta_to_target_specification(),
5161 Some("% err <= 5%")
5162 );
5163
5164 let relative_scientific_view_options = IntegrationStatusViewOptions {
5165 target_relative_accuracy: Some(1.0e-6),
5166 ..default_view_options()
5167 };
5168 let update = build_status_update(
5169 StatusUpdateBuildRequest::new(
5170 IntegrationStatusKind::Live,
5171 &state,
5172 &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
5173 &relative_scientific_view_options,
5174 )
5175 .with_timing(
5176 4,
5177 Duration::from_secs(25),
5178 Duration::from_secs(10),
5179 25_000,
5180 125_000,
5181 125_000,
5182 )
5183 .with_live_progress(Some(status_update::LiveIterationProgress {
5184 completed_points: 25_000,
5185 target_points: 100_000,
5186 })),
5187 );
5188 assert_eq!(
5189 update.meta.eta_to_target_specification(),
5190 Some("% err <= 1e-4%")
5191 );
5192
5193 let absolute_view_options = IntegrationStatusViewOptions {
5194 target_absolute_accuracy: Some(1.0e-6),
5195 ..default_view_options()
5196 };
5197 let update = build_status_update(
5198 StatusUpdateBuildRequest::new(
5199 IntegrationStatusKind::Live,
5200 &state,
5201 &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
5202 &absolute_view_options,
5203 )
5204 .with_timing(
5205 4,
5206 Duration::from_secs(25),
5207 Duration::from_secs(10),
5208 25_000,
5209 125_000,
5210 125_000,
5211 )
5212 .with_live_progress(Some(status_update::LiveIterationProgress {
5213 completed_points: 25_000,
5214 target_points: 100_000,
5215 })),
5216 );
5217 assert_eq!(
5218 update.meta.eta_to_target_specification(),
5219 Some("err <= 1e-6")
5220 );
5221
5222 let combined_view_options = IntegrationStatusViewOptions {
5223 target_relative_accuracy: Some(0.05),
5224 target_absolute_accuracy: Some(1.0e-6),
5225 ..default_view_options()
5226 };
5227 let update = build_status_update(
5228 StatusUpdateBuildRequest::new(
5229 IntegrationStatusKind::Live,
5230 &state,
5231 &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
5232 &combined_view_options,
5233 )
5234 .with_timing(
5235 4,
5236 Duration::from_secs(25),
5237 Duration::from_secs(10),
5238 25_000,
5239 125_000,
5240 125_000,
5241 )
5242 .with_live_progress(Some(status_update::LiveIterationProgress {
5243 completed_points: 25_000,
5244 target_points: 100_000,
5245 })),
5246 );
5247 assert_eq!(
5248 update.meta.eta_to_target_specification(),
5249 Some("% err <= 5% or err <= 1e-6")
5250 );
5251 }
5252
5253 #[test]
5254 fn target_accuracy_status_uses_either_absolute_or_relative_condition() {
5255 let state = make_integration_state();
5256
5257 let reached_by_absolute = status_update::evaluate_target_accuracy(
5258 &state,
5259 100_000,
5260 Duration::from_secs(10),
5261 &[None, None],
5262 IntegrationStatusPhaseDisplay::Real,
5263 None,
5264 Some(1.0e-4),
5265 );
5266 assert!(reached_by_absolute.is_reached());
5267
5268 let reached_by_relative = status_update::evaluate_target_accuracy(
5269 &state,
5270 100_000,
5271 Duration::from_secs(10),
5272 &[Some(Complex::new(F(1.0e-3), F(0.0))), None],
5273 IntegrationStatusPhaseDisplay::Real,
5274 Some(0.1),
5275 None,
5276 );
5277 assert!(reached_by_relative.is_reached());
5278 }
5279
5280 #[test]
5281 fn target_accuracy_status_requires_all_slots_to_reach_relative_target() {
5282 let mut state = make_integration_state();
5283 state.all_integrals[0].re.avg = F(1.0);
5284 state.all_integrals[0].re.err = F(1.0e-5);
5285 state.all_integrals[1].re.avg = F(1.0);
5286 state.all_integrals[1].re.err = F(2.0e-3);
5287
5288 let status = status_update::evaluate_target_accuracy(
5289 &state,
5290 100_000,
5291 Duration::from_secs(10),
5292 &[None, None],
5293 IntegrationStatusPhaseDisplay::Real,
5294 Some(1.0e-3),
5295 None,
5296 );
5297
5298 assert!(!status.is_reached());
5299 assert!(!status.relative_reached);
5300 assert!(status.eta_to_target.is_some());
5301 }
5302
5303 #[test]
5304 fn target_accuracy_status_requires_all_displayed_components_to_reach_target() {
5305 let mut state = make_integration_state();
5306 state.all_integrals[0].re.avg = F(1.0);
5307 state.all_integrals[0].re.err = F(1.0e-5);
5308 state.all_integrals[0].im.avg = F(1.0);
5309 state.all_integrals[0].im.err = F(2.0e-3);
5310
5311 let status = status_update::evaluate_target_accuracy(
5312 &state,
5313 100_000,
5314 Duration::from_secs(10),
5315 &[Some(Complex::new(F(1.0), F(1.0))), None],
5316 IntegrationStatusPhaseDisplay::Both,
5317 Some(1.0e-3),
5318 None,
5319 );
5320
5321 assert!(!status.is_reached());
5322 assert!(!status.relative_reached);
5323 assert!(status.eta_to_target.is_some());
5324 }
5325
5326 #[test]
5327 fn target_accuracy_status_reports_infinite_eta_for_unreachable_absolute_target() {
5328 let state = make_integration_state();
5329 let status = status_update::evaluate_target_accuracy(
5330 &state,
5331 100_000,
5332 Duration::from_secs(10),
5333 &[None, None],
5334 IntegrationStatusPhaseDisplay::Real,
5335 None,
5336 Some(1.0e-99),
5337 );
5338
5339 assert!(!status.is_reached());
5340 assert_eq!(status.eta_to_target, Some(Duration::MAX));
5341 }
5342
5343 #[test]
5344 fn target_accuracy_status_prefers_finite_relative_eta_over_infinite_absolute_eta() {
5345 let state = make_integration_state();
5346 let status = status_update::evaluate_target_accuracy(
5347 &state,
5348 100_000,
5349 Duration::from_secs(10),
5350 &[None, None],
5351 IntegrationStatusPhaseDisplay::Real,
5352 Some(1.0e-3),
5353 Some(1.0e-99),
5354 );
5355
5356 assert!(!status.is_reached());
5357 assert!(status.eta_to_target.is_some());
5358 assert_ne!(status.eta_to_target, Some(Duration::MAX));
5359 }
5360
5361 #[test]
5362 fn ratatui_overview_displays_infinite_eta_to_target() {
5363 let state = make_integration_state();
5364 let view_options = IntegrationStatusViewOptions {
5365 target_absolute_accuracy: Some(1.0e-99),
5366 show_statistics: false,
5367 show_max_weight_details: false,
5368 ..default_view_options()
5369 };
5370 let rendered = render_ratatui_update(
5371 StatusUpdateBuildRequest::new(
5372 IntegrationStatusKind::Live,
5373 &state,
5374 &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
5375 &view_options,
5376 )
5377 .with_timing(
5378 4,
5379 Duration::from_secs(25),
5380 Duration::from_secs(10),
5381 25_000,
5382 125_000,
5383 125_000,
5384 )
5385 .with_live_progress(Some(status_update::LiveIterationProgress {
5386 completed_points: 25_000,
5387 target_points: 100_000,
5388 })),
5389 |_| {},
5390 );
5391
5392 assert!(rendered.contains("ETA to target"), "{rendered}");
5393 assert!(rendered.contains("∞"), "{rendered}");
5394 }
5395
5396 #[test]
5397 fn ratatui_overview_prefers_finite_relative_eta_over_infinite_absolute_eta() {
5398 let state = make_integration_state();
5399 let view_options = IntegrationStatusViewOptions {
5400 target_relative_accuracy: Some(1.0e-3),
5401 target_absolute_accuracy: Some(1.0e-99),
5402 show_statistics: false,
5403 show_max_weight_details: false,
5404 ..default_view_options()
5405 };
5406 let rendered = render_ratatui_update(
5407 StatusUpdateBuildRequest::new(
5408 IntegrationStatusKind::Live,
5409 &state,
5410 &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
5411 &view_options,
5412 )
5413 .with_timing(
5414 4,
5415 Duration::from_secs(25),
5416 Duration::from_secs(10),
5417 25_000,
5418 125_000,
5419 125_000,
5420 )
5421 .with_live_progress(Some(status_update::LiveIterationProgress {
5422 completed_points: 25_000,
5423 target_points: 100_000,
5424 })),
5425 |_| {},
5426 );
5427
5428 assert!(rendered.contains("ETA to target"), "{rendered}");
5429 assert!(!rendered.contains("∞"), "{rendered}");
5430 }
5431
5432 #[test]
5433 fn ratatui_recent_history_window_tracks_current_iteration_points() {
5434 let mut dashboard = RatatuiDashboardState::new();
5435 let mut state = make_integration_state();
5436 let view_options = IntegrationStatusViewOptions {
5437 show_statistics: false,
5438 show_max_weight_details: false,
5439 ..default_view_options()
5440 };
5441
5442 state.iter = 1;
5443 dashboard.update(build_status_update(
5444 StatusUpdateBuildRequest::new(
5445 IntegrationStatusKind::Live,
5446 &state,
5447 &[None, None],
5448 &view_options,
5449 )
5450 .with_timing(
5451 4,
5452 Duration::from_secs(10),
5453 Duration::from_secs(10),
5454 500_000,
5455 1_000_000,
5456 1_000_000,
5457 )
5458 .with_live_progress(Some(status_update::LiveIterationProgress {
5459 completed_points: 500_000,
5460 target_points: 1_000_000,
5461 })),
5462 ));
5463
5464 state.iter = 2;
5465 dashboard.update(build_status_update(
5466 StatusUpdateBuildRequest::new(
5467 IntegrationStatusKind::Live,
5468 &state,
5469 &[None, None],
5470 &view_options,
5471 )
5472 .with_timing(
5473 4,
5474 Duration::from_secs(20),
5475 Duration::from_secs(5),
5476 0,
5477 2_000_000,
5478 2_000_000,
5479 )
5480 .with_live_progress(Some(status_update::LiveIterationProgress {
5481 completed_points: 0,
5482 target_points: 1_000_000,
5483 })),
5484 ));
5485 dashboard.update(build_status_update(
5486 StatusUpdateBuildRequest::new(
5487 IntegrationStatusKind::Live,
5488 &state,
5489 &[None, None],
5490 &view_options,
5491 )
5492 .with_timing(
5493 4,
5494 Duration::from_secs(25),
5495 Duration::from_secs(10),
5496 200_000,
5497 2_200_000,
5498 2_200_000,
5499 )
5500 .with_live_progress(Some(status_update::LiveIterationProgress {
5501 completed_points: 200_000,
5502 target_points: 1_000_000,
5503 })),
5504 ));
5505
5506 dashboard.toggle_chart_history_window();
5507 for _ in 0..5 {
5508 dashboard.narrow_chart_history_window();
5509 }
5510
5511 assert_eq!(
5512 dashboard.visible_history_sample_bounds(),
5513 Some((2_000_000, 2_200_000))
5514 );
5515 }
5516
5517 #[test]
5518 fn ratatui_full_history_preserves_origin_after_compaction() {
5519 let mut dashboard = RatatuiDashboardState::new();
5520 let mut state = make_integration_state();
5521 let view_options = IntegrationStatusViewOptions {
5522 show_statistics: false,
5523 show_max_weight_details: false,
5524 ..default_view_options()
5525 };
5526
5527 for index in 0..4_300 {
5528 state.iter = 1 + index / 40;
5529 let total_points = index * 1_000;
5530 dashboard.update(build_status_update(
5531 StatusUpdateBuildRequest::new(
5532 IntegrationStatusKind::Live,
5533 &state,
5534 &[None, None],
5535 &view_options,
5536 )
5537 .with_timing(
5538 4,
5539 Duration::from_secs(index as u64),
5540 Duration::from_secs(10),
5541 total_points % 1_000_000,
5542 total_points,
5543 total_points,
5544 )
5545 .with_live_progress(Some(status_update::LiveIterationProgress {
5546 completed_points: total_points % 1_000_000,
5547 target_points: 1_000_000,
5548 })),
5549 ));
5550 }
5551
5552 dashboard.toggle_chart_history_window();
5553 dashboard.toggle_chart_history_window();
5554
5555 assert_eq!(
5556 dashboard.visible_history_sample_bounds(),
5557 Some((0, 4_299_000))
5558 );
5559 }
5560
5561 #[test]
5562 fn ratatui_convergence_title_shows_active_y_span() {
5563 let state = make_integration_state();
5564 let view_options = IntegrationStatusViewOptions {
5565 show_statistics: false,
5566 show_max_weight_details: false,
5567 ..default_view_options()
5568 };
5569 let rendered = render_ratatui_update(
5570 StatusUpdateBuildRequest::new(
5571 IntegrationStatusKind::Live,
5572 &state,
5573 &[None, None],
5574 &view_options,
5575 )
5576 .with_timing(
5577 4,
5578 Duration::from_secs(15),
5579 Duration::from_secs(10),
5580 25_000,
5581 125_000,
5582 125_000,
5583 )
5584 .with_live_progress(Some(status_update::LiveIterationProgress {
5585 completed_points: 25_000,
5586 target_points: 100_000,
5587 })),
5588 |dashboard| dashboard.widen_chart_y_sigma_span(),
5589 );
5590
5591 assert!(rendered.contains("y ±5σ"), "{rendered}");
5592 }
5593
5594 #[test]
5595 fn ratatui_discrete_tab_renders_selected_bin_detail() {
5596 let state = make_discrete_integration_state();
5597 let view_options = IntegrationStatusViewOptions {
5598 show_statistics: false,
5599 show_max_weight_details: false,
5600 show_top_discrete_grid: false,
5601 show_discrete_contributions_sum: false,
5602 show_max_weight_info_for_discrete_bins: false,
5603 ..default_view_options()
5604 };
5605 let rendered = render_ratatui_update(
5606 StatusUpdateBuildRequest::new(
5607 IntegrationStatusKind::Iteration,
5608 &state,
5609 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
5610 &view_options,
5611 )
5612 .with_timing(
5613 4,
5614 Duration::from_secs(12),
5615 Duration::from_secs(12),
5616 110_000,
5617 210_000,
5618 210_000,
5619 ),
5620 |dashboard| dashboard.select_tab(1),
5621 );
5622
5623 assert!(rendered.contains("Selected bin"), "{rendered}");
5624 assert!(
5625 rendered.contains("Discrete bins for focused integrand"),
5626 "{rendered}"
5627 );
5628 assert!(rendered.contains("sample %"), "{rendered}");
5629 assert!(rendered.contains("GL0"), "{rendered}");
5630 assert!(rendered.contains("GL1"), "{rendered}");
5631 assert!(rendered.contains("pdf"), "{rendered}");
5632 assert!(rendered.contains("Per integrand details"), "{rendered}");
5633 assert!(rendered.contains("# samples"), "{rendered}");
5634 }
5635
5636 #[test]
5637 fn ratatui_single_graph_subset_keeps_the_graph_row() {
5638 let state = make_single_graph_discrete_integration_state();
5639 let view_options = IntegrationStatusViewOptions {
5640 show_statistics: false,
5641 show_max_weight_details: false,
5642 ..default_view_options()
5643 };
5644 let rendered = render_ratatui_update(
5645 StatusUpdateBuildRequest::new(
5646 IntegrationStatusKind::Iteration,
5647 &state,
5648 &[None, None],
5649 &view_options,
5650 ),
5651 |dashboard| dashboard.select_tab(1),
5652 );
5653
5654 assert!(
5655 rendered.contains("Discrete bins for focused integrand"),
5656 "{rendered}"
5657 );
5658 assert!(rendered.contains("GL22"), "{rendered}");
5659 assert!(!rendered.contains("GL1"), "{rendered}");
5660 }
5661
5662 #[test]
5663 fn ratatui_max_weight_tab_preserves_full_scientific_exponent() {
5664 let mut state = make_discrete_integration_state();
5665 let max_eval = 3.8477312059601813_f64;
5666 let expected = format!("{:+.16e}", max_eval);
5667 state.all_integrals[0].re.max_eval_positive = F(max_eval);
5668 state.all_integrals[0].re.max_eval_positive_xs = Some(Sample::Discrete(
5669 F(1.0),
5670 0,
5671 Some(Box::new(Sample::Continuous(
5672 F(1.0),
5673 vec![F(0.4502578156709256)],
5674 ))),
5675 ));
5676
5677 let view_options = IntegrationStatusViewOptions {
5678 show_statistics: false,
5679 ..default_view_options()
5680 };
5681 let rendered = render_ratatui_update(
5682 StatusUpdateBuildRequest::new(
5683 IntegrationStatusKind::Iteration,
5684 &state,
5685 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
5686 &view_options,
5687 )
5688 .with_timing(
5689 4,
5690 Duration::from_secs(12),
5691 Duration::from_secs(12),
5692 110_000,
5693 210_000,
5694 210_000,
5695 ),
5696 |dashboard| dashboard.select_tab(2),
5697 );
5698
5699 assert!(rendered.contains(&expected), "{rendered}");
5700 assert!(rendered.contains("graph: 0, xs: ["), "{rendered}");
5701 }
5702
5703 mod failing {
5704 use super::*;
5705
5706 #[test]
5707 fn target_accuracy_status_treats_zero_relative_reference_as_inactive() {
5708 let state = make_integration_state();
5709 let status = status_update::evaluate_target_accuracy(
5710 &state,
5711 100_000,
5712 Duration::from_secs(10),
5713 &[Some(Complex::new(F(0.0), F(0.0))), None],
5714 IntegrationStatusPhaseDisplay::Real,
5715 Some(0.05),
5716 None,
5717 );
5718
5719 assert!(!status.relative_reached);
5720 assert!(!status.absolute_reached);
5721 assert_eq!(status.eta_to_target, None);
5722 }
5723 }
5724}
5725
5726#[test]
5727fn test_threading() {
5728 use symbolica::numerical_integration::ContinuousGrid;
5729
5730 fn test_fn(x: f64) -> f64 {
5731 x * x * (x * 6.).sin()
5732 }
5733
5734 let mut acc_1 = StatisticsAccumulator::<f64>::new();
5735 let mut acc_2 = StatisticsAccumulator::<f64>::new();
5736
5737 let samples_per_sample = 4;
5738 let samples_per_iter = 100000;
5739 let n_iter = 10;
5740
5741 let mut rng = MonteCarloRng::new(42, 0);
5742
5743 let mut grid = Grid::<f64>::Continuous(ContinuousGrid::new(1, 64, 100, None, false));
5744
5745 for _i_iter in 0..n_iter {
5746 let mut multiplice_accs = vec![StatisticsAccumulator::<f64>::new(); samples_per_sample];
5747 for _i_sample in 0..samples_per_iter {
5748 let n_samples = (0..samples_per_sample)
5749 .map(|_| {
5750 let mut sample = Sample::new();
5751 grid.sample(&mut rng, &mut sample);
5752 sample
5753 })
5754 .collect_vec();
5755
5756 let n_evals = n_samples
5757 .iter()
5758 .map(|s| match s {
5759 Sample::Continuous(_, xs) => test_fn(xs[0]),
5760 _ => unreachable!(),
5761 })
5762 .collect_vec();
5763
5764 for (i_eval, (sample, eval)) in n_samples.iter().zip(&n_evals).enumerate() {
5765 acc_1.add_sample(eval * sample.get_weight(), Some(sample));
5766 multiplice_accs[i_eval].add_sample(eval * sample.get_weight(), Some(sample));
5767 }
5768 }
5769
5770 for acc in multiplice_accs {
5771 acc_2.merge_samples(&mut acc.clone());
5772 }
5773 acc_1.update_iter(false);
5774 acc_2.update_iter(false);
5775 }
5776
5777 println!("acc1: {:?}", acc_1);
5778 println!("acc2: {:?}", acc_2);
5779}