1#![allow(dead_code)]
2
3mod display;
9mod render_ratatui;
10mod render_tabled;
11mod status_update;
12
13use crate::integrands::process::EvaluationTarget;
14use bincode::Decode;
15use bincode::Encode;
16use color_eyre::{Report, Result};
17use colored::Colorize;
18use itertools::Itertools;
19use itertools::izip;
20use linnet::half_edge::involution::{EdgeVec, Orientation};
21use rayon::ThreadPoolBuilder;
22use serde::Deserialize;
23use serde::Serialize;
24use spenso::algebra::algebraic_traits::IsZero;
25use symbolica::domains::float::Constructible;
26use symbolica::numerical_integration::{
27 DiscreteGrid, Grid, MonteCarloRng, Sample, StatisticsAccumulator,
28};
29
30use crate::Integrand;
31use crate::graph::GroupId;
32use crate::integrands::HasIntegrand;
33use crate::integrands::evaluation::StatisticsCounter;
34use crate::integrands::evaluation::{EvaluationMetaData, EvaluationResult};
35use crate::integrands::process::GaussianReferenceFunction;
36use crate::integrands::process::{GraphTerm, ProcessIntegrand};
37use crate::model::{Model, SerializableInputParamCard};
38use crate::observables::{
39 EventGroupList, ObservableAccumulatorBundle, ObservableFileFormat, ObservableSnapshotBundle,
40};
41use crate::settings::IntegratorSettings;
42use crate::settings::RuntimeSettings;
43use crate::settings::runtime::{
44 AbsoluteIntegrationResult, ComponentDiscreteBreakdown, DiscreteBreakdown,
45 DiscreteBreakdownEntry, DiscreteCoordinate, DiscreteGraphSamplingType, IntegralEstimate,
46 IntegratedPhase, IntegrationResult, IntegrationTableComponentResult, MaxWeightInfoEntry,
47 SamplingSettings, SlotIntegrationResult,
48};
49use crate::utils;
50use crate::utils::F;
51use crate::{
52 clear_iteration_abort_request, is_interrupted, is_iteration_abort_requested, set_interrupted,
53};
54use rayon::prelude::*;
55pub use render_ratatui::RatatuiDashboardState;
56pub use render_tabled::TabledRenderOptions;
57use spenso::algebra::complex::Complex;
58pub use status_update::{
59 ContributionSortMode, IntegrationStatusKind, IntegrationStatusPhaseDisplay,
60 IntegrationStatusViewOptions, StatusUpdate,
61};
62use status_update::{
63 StatusUpdateBuildRequest, build_saved_status_update, build_status_update,
64 evaluate_target_accuracy,
65};
66use std::fs;
67use std::io::Write;
68use std::path::Path;
69use std::path::PathBuf;
70use std::time::Duration;
71use std::time::Instant;
72use tabled::{
73 Tabled,
74 builder::Builder,
75 settings::{
76 Alignment, Modify, Panel, Style,
77 object::{Columns, Rows},
78 style::HorizontalLine,
79 },
80};
81#[allow(unused_imports)]
82use tracing::{debug, error, info, trace, warn};
83
84#[derive(Clone, Copy, Debug, Default)]
85pub struct WorkspaceSnapshotControl {
86 pub write_iteration_archives: bool,
87}
88
89#[derive(Clone, Copy, Debug)]
90pub struct IterationBatchingSettings {
91 pub batch_size: Option<usize>,
92 pub batch_timing_seconds: f64,
93 pub min_time_between_status_updates_seconds: f64,
94 pub emit_live_status_updates: bool,
95 pub emit_initial_status_update: bool,
96}
97
98impl Default for IterationBatchingSettings {
99 fn default() -> Self {
100 Self {
101 batch_size: None,
102 batch_timing_seconds: 5.0,
103 min_time_between_status_updates_seconds: 0.0,
104 emit_live_status_updates: true,
105 emit_initial_status_update: true,
106 }
107 }
108}
109
110pub struct IntegrationSlot {
111 pub meta: SlotMeta,
112 pub settings: RuntimeSettings,
113 pub model: Model,
114 pub integrand: Integrand,
115 pub target: Option<Complex<F<f64>>>,
116 pub reference_gaussian: Option<GaussianReferenceFunction>,
118}
119
120impl IntegrationSlot {
121 pub fn new(
122 meta: SlotMeta,
123 settings: RuntimeSettings,
124 model: Model,
125 integrand: Integrand,
126 target: Option<Complex<F<f64>>>,
127 ) -> Self {
128 Self {
129 meta,
130 settings,
131 model,
132 integrand,
133 target,
134 reference_gaussian: None,
135 }
136 }
137}
138
139pub struct HavanaIntegrateRequest {
140 pub slots: Vec<IntegrationSlot>,
141 pub sampling_correlation_mode: SamplingCorrelationMode,
142 pub n_cores: usize,
143 pub state: Option<IntegrationState>,
144 pub workspace: Option<PathBuf>,
145 pub output_control: WorkspaceSnapshotControl,
146 pub batching: IterationBatchingSettings,
147 pub view_options: IntegrationStatusViewOptions,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
153pub struct SlotMeta {
154 pub process_name: String,
155 pub integrand_name: String,
156}
157
158impl SlotMeta {
159 pub fn key(&self) -> String {
160 format!("{}@{}", self.process_name, self.integrand_name)
161 }
162}
163
164#[derive(Serialize, Deserialize, Clone)]
165pub struct IntegrationWorkspaceManifest {
166 #[serde(default)]
167 pub version: u32,
168 pub slots: Vec<SlotMeta>,
169 pub targets: Vec<Option<Complex<F<f64>>>>,
170 pub effective_model_parameters: Vec<SerializableInputParamCard<F<f64>>>,
171 pub integrand_fingerprints: Vec<String>,
172 #[serde(default, skip_serializing_if = "Vec::is_empty")]
174 pub reference_gaussians: Vec<Option<GaussianReferenceFunction>>,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub reference_observable_convention: Option<String>,
177 pub training_slot: usize,
178 pub integrator_settings_slot: usize,
179 pub sampling_correlation_mode: SamplingCorrelationMode,
180}
181
182impl IntegrationWorkspaceManifest {
183 pub const CURRENT_VERSION: u32 = 3;
186
187 pub fn validate_reference_config(
190 &self,
191 requested: &[Option<GaussianReferenceFunction>],
192 ) -> Result<()> {
193 if !self.reference_gaussians.is_empty()
194 && self.reference_gaussians.len() != self.slots.len()
195 {
196 return Err(Report::msg(
197 "Workspace reference descriptors must match its slots",
198 ));
199 }
200 for reference in self.reference_gaussians.iter().flatten() {
201 let scale = reference.expected_second_moment::<f64>().0;
202 if reference.center().is_empty() || !scale.is_finite() || scale <= 0.0 {
203 return Err(Report::msg(
204 "Workspace reference descriptor must contain a resolved center and finite positive moment scale",
205 ));
206 }
207 }
208 let has_reference = requested.iter().any(Option::is_some);
209 let expected_convention =
210 has_reference.then_some(GaussianReferenceFunction::INTEGRATION_CONVENTION);
211 let same_config = if self.reference_gaussians.is_empty() {
212 !has_reference
213 } else {
214 self.reference_gaussians == requested
215 };
216 if !same_config || self.reference_observable_convention.as_deref() != expected_convention {
217 return Err(Report::msg(format!(
218 "Workspace physical/reference configuration does not match the request. Repeat the same --reference-gaussian descriptor to resume acceptance, or use --restart for a different target. Saved descriptors: {}",
219 serde_json::to_string(&self.reference_gaussians)?
220 )));
221 }
222 Ok(())
223 }
224
225 pub fn validate_version(&self) -> Result<()> {
226 if self.version == Self::CURRENT_VERSION {
227 return Ok(());
228 }
229
230 let reason = if self.version == 0 {
231 "is unversioned".to_string()
232 } else if self.version < Self::CURRENT_VERSION {
233 format!("uses the older format version {}", self.version)
234 } else {
235 format!("uses the newer format version {}", self.version)
236 };
237 Err(Report::msg(format!(
238 "Integration workspace {reason} and cannot be resumed by this GammaLoop build (supported version {}); use --restart to create a new workspace",
239 Self::CURRENT_VERSION
240 )))
241 }
242}
243
244impl crate::utils::serde_utils::SmartSerde for IntegrationWorkspaceManifest {}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)]
247#[serde(rename_all = "snake_case")]
248pub enum SamplingCorrelationMode {
249 Correlated,
250 Uncorrelated,
251}
252
253impl SamplingCorrelationMode {
254 fn state_index(self, slot_index: usize) -> usize {
255 match self {
256 Self::Correlated => 0,
257 Self::Uncorrelated => slot_index,
258 }
259 }
260
261 fn state_count(self, n_slots: usize) -> usize {
262 match self {
263 Self::Correlated => 1,
264 Self::Uncorrelated => n_slots,
265 }
266 }
267}
268
269#[derive(Serialize, Deserialize, Encode, Decode, Clone)]
270pub(crate) struct DiscreteGridAccumulatorSummary {
271 #[bincode(with_serde)]
272 pub(crate) bins: Vec<DiscreteGridBinAccumulatorSummary>,
273}
274
275#[derive(Serialize, Deserialize, Encode, Decode, Clone)]
276pub(crate) struct DiscreteGridBinAccumulatorSummary {
277 #[bincode(with_serde)]
278 pub(crate) accumulator: StatisticsAccumulator<F<f64>>,
279 pub(crate) sub_summary: Option<Box<DiscreteGridAccumulatorSummary>>,
280}
281
282#[derive(Serialize, Deserialize, Encode, Decode, Clone, Default)]
283struct PersistedDiscreteBreakdownMetadata {
284 axis_label: String,
285 #[bincode(with_serde)]
286 fixed_coordinates: Vec<DiscreteCoordinate>,
287 bin_labels: Vec<String>,
288}
289
290#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
291struct SamplingSlotState {
292 #[bincode(with_serde)]
293 grid: Grid<F<f64>>,
294 discrete_axis_labels: Vec<String>,
295}
296
297impl SamplingSlotState {
298 fn new(grid: Grid<F<f64>>, discrete_axis_labels: Vec<String>) -> Self {
299 Self {
300 grid,
301 discrete_axis_labels,
302 }
303 }
304
305 fn from_integrand(integrand: &Integrand) -> Self {
306 let grid = integrand.create_grid();
307 let discrete_axis_labels = match integrand {
308 Integrand::ProcessIntegrand(process_integrand) => {
309 discrete_axis_labels(&process_integrand.get_settings().sampling)
310 .into_iter()
311 .map(str::to_string)
312 .collect_vec()
313 }
314 #[cfg(test)]
315 Integrand::TestProbe(_) => Vec::new(),
316 };
317 Self::new(grid, discrete_axis_labels)
318 }
319}
320
321#[derive(Default)]
322struct MonitoredDiscreteSetup {
323 path: Option<Vec<usize>>,
324 axis_label: Option<String>,
325 bin_descriptions: Option<Vec<String>>,
326 warning: Option<String>,
327}
328
329#[derive(Tabled)]
330pub struct IntegralResult {
331 id: String,
332 n_samples: String,
333 #[tabled(rename = "n_samples[%]")]
334 n_samples_perc: String,
335 #[tabled(rename = "<I>")]
336 integral: String,
337 #[tabled(rename = "sqrt(σ)")]
338 variance: String,
339 err: String,
340 #[tabled(rename = "err[%]")]
341 err_perc: String,
342 #[tabled(rename = "PDF")]
343 pdf: String,
344}
345
346#[derive(Serialize, Deserialize, Encode, Decode, Clone)]
347pub struct ComplexAccumulator {
348 #[bincode(with_serde)]
349 pub re: StatisticsAccumulator<F<f64>>,
350 #[bincode(with_serde)]
351 pub im: StatisticsAccumulator<F<f64>>,
352 #[bincode(with_serde)]
353 pub absolute_re: StatisticsAccumulator<F<f64>>,
354 #[bincode(with_serde)]
355 pub absolute_im: StatisticsAccumulator<F<f64>>,
356}
357
358impl ComplexAccumulator {
359 pub(crate) fn new() -> Self {
360 Self {
361 re: StatisticsAccumulator::new(),
362 im: StatisticsAccumulator::new(),
363 absolute_re: StatisticsAccumulator::new(),
364 absolute_im: StatisticsAccumulator::new(),
365 }
366 }
367
368 pub(crate) fn get_worst_case(&self) -> Complex<F<f64>> {
370 Complex::new(
371 self.re
372 .max_eval_positive
373 .abs()
374 .max(self.re.max_eval_negative.abs()),
375 self.im
376 .max_eval_positive
377 .abs()
378 .max(self.im.max_eval_negative.abs()),
379 )
380 }
381
382 pub(crate) fn add_sample(
384 &mut self,
385 result: Complex<F<f64>>,
386 absolute_result: Option<Complex<F<f64>>>,
387 sample_weight: F<f64>,
388 sample: Option<&Sample<F<f64>>>,
389 ) {
390 let weighted_re = result.re * sample_weight;
391 let weighted_im = result.im * sample_weight;
392 self.re.add_sample(weighted_re, sample);
393 self.im.add_sample(weighted_im, sample);
394 let absolute = absolute_result
398 .unwrap_or_else(|| Complex::new(result.re.abs(), result.im.abs()))
399 * sample_weight.abs();
400 self.absolute_re.add_sample(absolute.re, sample);
401 self.absolute_im.add_sample(absolute.im, sample);
402 }
403
404 pub(crate) fn merge(&mut self, other: &Self) {
405 self.re.merge_samples_no_reset(&other.re);
406 self.im.merge_samples_no_reset(&other.im);
407 self.absolute_re.merge_samples_no_reset(&other.absolute_re);
408 self.absolute_im.merge_samples_no_reset(&other.absolute_im);
409 }
410
411 pub(crate) fn update_iter(&mut self, use_weighted_average: bool) {
412 self.re.update_iter(use_weighted_average);
413 self.im.update_iter(use_weighted_average);
414 self.absolute_re.update_iter(use_weighted_average);
415 self.absolute_im.update_iter(use_weighted_average);
416 }
417
418 pub(crate) fn max_weight_rows(
419 &self,
420 slot_meta: &SlotMeta,
421 discrete_axis_labels: &[String],
422 ) -> Vec<[String; 3]> {
423 let max_evals = [
424 &self.re.max_eval_positive,
425 &self.re.max_eval_negative,
426 &self.im.max_eval_positive,
427 &self.im.max_eval_negative,
428 ];
429
430 let max_eval_samples = [
431 &self.re.max_eval_positive_xs,
432 &self.re.max_eval_negative_xs,
433 &self.im.max_eval_positive_xs,
434 &self.im.max_eval_negative_xs,
435 ];
436
437 let sign_strs = ["+", "-", "+", "-"];
438 let phase_strs = ["re", "re", "im", "im"];
439
440 izip!(
441 max_evals.iter(),
442 max_eval_samples.iter(),
443 sign_strs.iter(),
444 phase_strs.iter()
445 )
446 .filter_map(|(max_eval, max_eval_sample, sign_str, phase_str)| {
447 if max_eval.is_non_zero() {
448 Some([
449 format!(
450 "{} {} [{}] ",
451 slot_label(slot_meta),
452 format!("{:<2}", phase_str).blue(),
453 format!("{:<1}", sign_str).blue()
454 ),
455 format!("{:+.16e}", max_eval),
456 if let Some(sample) = max_eval_sample {
457 format_max_eval_sample(sample, discrete_axis_labels, &[])
458 } else {
459 "N/A".to_string()
460 },
461 ])
462 } else {
463 None
464 }
465 })
466 .collect()
467 }
468}
469
470impl DiscreteGridAccumulatorSummary {
471 fn from_grid(grid: &Grid<F<f64>>) -> Option<Self> {
472 match grid {
473 Grid::Discrete(discrete_grid) => Some(Self {
474 bins: discrete_grid
475 .bins
476 .iter()
477 .map(|bin| DiscreteGridBinAccumulatorSummary {
478 accumulator: StatisticsAccumulator::new(),
479 sub_summary: bin
480 .sub_grid
481 .as_ref()
482 .and_then(Self::from_grid)
483 .map(Box::new),
484 })
485 .collect(),
486 }),
487 Grid::Continuous(_) | Grid::Uniform(_, _) => None,
488 }
489 }
490
491 fn merge_iteration_grid(&mut self, grid: &Grid<F<f64>>) {
492 let Grid::Discrete(discrete_grid) = grid else {
493 return;
494 };
495
496 for (summary_bin, grid_bin) in self.bins.iter_mut().zip(discrete_grid.bins.iter()) {
497 summary_bin
498 .accumulator
499 .merge_samples_no_reset(&grid_bin.accumulator);
500 if let (Some(sub_summary), Some(sub_grid)) =
501 (summary_bin.sub_summary.as_mut(), grid_bin.sub_grid.as_ref())
502 {
503 sub_summary.merge_iteration_grid(sub_grid);
504 }
505 }
506 }
507
508 fn from_shallow_grid(grid: &DiscreteGrid<F<f64>>) -> Self {
509 Self {
510 bins: grid
511 .bins
512 .iter()
513 .map(|_| DiscreteGridBinAccumulatorSummary {
514 accumulator: StatisticsAccumulator::new(),
515 sub_summary: None,
516 })
517 .collect(),
518 }
519 }
520
521 fn merge_shallow_iteration_grid(&mut self, grid: &DiscreteGrid<F<f64>>) {
522 assert_eq!(
523 self.bins.len(),
524 grid.bins.len(),
525 "absolute monitor grid shape changed unexpectedly"
526 );
527 for (summary_bin, grid_bin) in self.bins.iter_mut().zip(&grid.bins) {
528 assert!(
529 summary_bin.sub_summary.is_none() && grid_bin.sub_grid.is_none(),
530 "absolute monitor grids must not contain descendants"
531 );
532 summary_bin
533 .accumulator
534 .merge_samples_no_reset(&grid_bin.accumulator);
535 }
536 }
537
538 fn update_iter(&mut self) {
539 for bin in &mut self.bins {
540 bin.accumulator.update_iter(false);
541 if let Some(sub_summary) = bin.sub_summary.as_mut() {
542 sub_summary.update_iter();
543 }
544 }
545 }
546
547 fn first_non_trivial_breakdown(
548 &self,
549 metadata: &PersistedDiscreteBreakdownMetadata,
550 pdfs: &[F<f64>],
551 ) -> Option<DiscreteBreakdown> {
552 (!self.bins.is_empty()).then(|| DiscreteBreakdown {
553 axis_label: metadata.axis_label.clone(),
554 fixed_coordinates: metadata.fixed_coordinates.clone(),
555 entries: self
556 .bins
557 .iter()
558 .enumerate()
559 .map(|(bin_index, bin)| DiscreteBreakdownEntry {
560 bin_index,
561 bin_label: metadata.bin_labels.get(bin_index).cloned(),
562 pdf: pdfs.get(bin_index).copied().unwrap_or(F(0.0)),
563 value: bin.accumulator.avg,
564 error: bin.accumulator.err,
565 chi_sq: bin.accumulator.chi_sq,
566 processed_samples: bin.accumulator.processed_samples,
567 })
568 .collect(),
569 })
570 }
571}
572
573struct IntegralResultCells {
574 integrand: String,
575 value: String,
576 relative_error: String,
577 chi_sq: String,
578 delta_sigma: Option<String>,
579 delta_percent: Option<String>,
580 mwi: String,
581}
582
583const DEFAULT_MAX_SHARED_TABLE_WIDTH: usize = 250;
584
585fn slot_label(slot_meta: &SlotMeta) -> String {
586 format!("itg {}", slot_meta.key())
587}
588
589fn slot_key_label(slot_meta: &SlotMeta) -> String {
590 slot_meta.key()
591}
592
593fn format_max_eval_coordinate(value: F<f64>) -> String {
594 let formatted = format!("{:.16e}", value.0);
595 let Some((mantissa, exponent)) = formatted.rsplit_once('e') else {
596 return formatted;
597 };
598 let exponent = exponent.parse::<i32>().unwrap_or_default();
599 format!("{mantissa}e{exponent:+03}")
600}
601
602fn format_max_eval_coordinates(xs: &[F<f64>]) -> String {
603 if xs.len() <= 3 {
604 return format!(
605 "[ {} ]",
606 xs.iter()
607 .map(|value| format_max_eval_coordinate(*value))
608 .join(" ")
609 );
610 }
611
612 let rows = xs
613 .chunks(3)
614 .map(|chunk| {
615 chunk
616 .iter()
617 .map(|value| format_max_eval_coordinate(*value))
618 .join(" ")
619 })
620 .join("\n");
621 format!("[\n{rows} ]")
622}
623
624pub(crate) fn discrete_axis_label(axis_labels: &[String], depth: usize) -> &str {
625 axis_labels.get(depth).map(String::as_str).unwrap_or("idx")
626}
627
628fn append_max_eval_sample_parts(
629 sample: &Sample<F<f64>>,
630 axis_labels: &[String],
631 depth: usize,
632 parts: &mut Vec<String>,
633) {
634 match sample {
635 Sample::Continuous(_, xs) => {
636 parts.push(format!("xs: {}", format_max_eval_coordinates(xs)));
637 }
638 Sample::Discrete(_, index, Some(nested_sample)) => {
639 parts.push(format!(
640 "{}: {}",
641 discrete_axis_label(axis_labels, depth),
642 index
643 ));
644 append_max_eval_sample_parts(nested_sample, axis_labels, depth + 1, parts);
645 }
646 Sample::Discrete(_, index, None) => {
647 parts.push(format!(
648 "{}: {}",
649 discrete_axis_label(axis_labels, depth),
650 index
651 ));
652 }
653 Sample::Uniform(_, indices, xs) => {
654 for (offset, index) in indices.iter().enumerate() {
655 parts.push(format!(
656 "{}: {}",
657 discrete_axis_label(axis_labels, depth + offset),
658 index
659 ));
660 }
661 parts.push(format!("xs: {}", format_max_eval_coordinates(xs)));
662 }
663 }
664}
665
666fn format_max_eval_sample(
667 sample: &Sample<F<f64>>,
668 axis_labels: &[String],
669 prefix_path: &[usize],
670) -> String {
671 let mut parts = prefix_path
672 .iter()
673 .enumerate()
674 .map(|(depth, index)| format!("{}: {}", discrete_axis_label(axis_labels, depth), index))
675 .collect_vec();
676 append_max_eval_sample_parts(sample, axis_labels, prefix_path.len(), &mut parts);
677 if parts.is_empty() {
678 String::from("N/A")
679 } else {
680 parts.join(", ")
681 }
682}
683
684fn format_iteration_points(points: usize) -> String {
685 format_abbreviated_count(points)
686}
687
688fn format_total_points(points: usize) -> String {
689 format_abbreviated_count(points)
690}
691
692#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
693enum ComponentKind {
694 Real,
695 Imag,
696}
697
698impl ComponentKind {
699 fn all_for_display(display: IntegrationStatusPhaseDisplay) -> Vec<Self> {
700 let mut components = Vec::new();
701 if display.shows_real() {
702 components.push(Self::Real);
703 }
704 if display.shows_imag() {
705 components.push(Self::Imag);
706 }
707 components
708 }
709
710 fn tag(self) -> &'static str {
711 match self {
712 Self::Real => "re",
713 Self::Imag => "im",
714 }
715 }
716
717 fn colorized_tag(self) -> String {
718 self.tag().blue().bold().to_string()
719 }
720}
721
722#[derive(Clone, Copy, Debug, Eq, PartialEq)]
723enum ContributionKind {
724 All,
725 Sum,
726 Bin(usize),
727}
728
729impl ContributionKind {
730 fn label(self, integration_state: &IntegrationState) -> String {
731 match self {
732 Self::All => "All".bold().green().to_string(),
733 Self::Sum => "Sum".bold().green().to_string(),
734 Self::Bin(bin_index) => contribution_bin_label(integration_state, bin_index),
735 }
736 }
737}
738
739#[derive(Clone, Debug)]
740struct DiscreteLevelContext {
741 path: Vec<usize>,
742 pdfs: Vec<F<f64>>,
743}
744
745fn first_non_trivial_discrete_path(grid: &Grid<F<f64>>) -> Option<Vec<usize>> {
746 match grid {
747 Grid::Discrete(discrete_grid) => {
748 if discrete_grid.bins.len() > 1 {
749 Some(Vec::new())
750 } else {
751 discrete_grid
752 .bins
753 .first()
754 .and_then(|bin| bin.sub_grid.as_ref())
755 .and_then(first_non_trivial_discrete_path)
756 .map(|mut path| {
757 path.insert(0, 0);
758 path
759 })
760 }
761 }
762 Grid::Continuous(_) | Grid::Uniform(_, _) => None,
763 }
764}
765
766fn discrete_grid_at_path<'a>(
767 grid: &'a Grid<F<f64>>,
768 path: &[usize],
769) -> Option<&'a DiscreteGrid<F<f64>>> {
770 match (grid, path.split_first()) {
771 (Grid::Discrete(discrete_grid), None) => Some(discrete_grid),
772 (Grid::Discrete(discrete_grid), Some((bin_index, rest))) => discrete_grid
773 .bins
774 .get(*bin_index)?
775 .sub_grid
776 .as_ref()
777 .and_then(|sub_grid| discrete_grid_at_path(sub_grid, rest)),
778 (Grid::Continuous(_) | Grid::Uniform(_, _), _) => None,
779 }
780}
781
782fn shallow_discrete_monitor_grid(
783 grid: &Grid<F<f64>>,
784 path: &[usize],
785) -> Result<DiscreteGrid<F<f64>>> {
786 let source = discrete_grid_at_path(grid, path).ok_or_else(|| {
787 Report::msg(format!(
788 "monitored discrete path {path:?} does not match the sampling grid"
789 ))
790 })?;
791 if source.bins.is_empty() {
792 return Err(Report::msg(format!(
793 "monitored discrete path {path:?} resolves to an empty grid"
794 )));
795 }
796
797 let mut monitor =
798 DiscreteGrid::new(vec![None; source.bins.len()], F(1.0), false).map_err(Report::msg)?;
799 for (monitor_bin, source_bin) in monitor.bins.iter_mut().zip(&source.bins) {
800 monitor_bin.pdf = source_bin.pdf;
801 }
802 Ok(monitor)
803}
804
805fn discrete_sample_at_path<'a>(
806 sample: &'a Sample<F<f64>>,
807 path: &[usize],
808) -> Result<&'a Sample<F<f64>>> {
809 let mut local_sample = sample;
810 for (depth, expected_index) in path.iter().copied().enumerate() {
811 let Sample::Discrete(_, sampled_index, sub_sample) = local_sample else {
812 return Err(Report::msg(format!(
813 "sample does not contain the discrete monitor path {path:?} at depth {depth}"
814 )));
815 };
816 if *sampled_index != expected_index {
817 return Err(Report::msg(format!(
818 "sample index {sampled_index} does not match monitored path index {expected_index} at depth {depth}"
819 )));
820 }
821 local_sample = sub_sample.as_deref().ok_or_else(|| {
822 Report::msg(format!(
823 "sample ends before the discrete monitor path {path:?} at depth {depth}"
824 ))
825 })?;
826 }
827 Ok(local_sample)
828}
829
830fn add_shallow_monitor_sample(
831 monitor: Option<&mut DiscreteGrid<F<f64>>>,
832 monitored_path: Option<&[usize]>,
833 sample: &Sample<F<f64>>,
834 evaluation: F<f64>,
835) -> Result<()> {
836 let (monitor, path) = match (monitor, monitored_path) {
837 (Some(monitor), Some(path)) => (monitor, path),
838 (None, None) => return Ok(()),
839 _ => {
840 return Err(Report::msg(
841 "absolute monitor grid and monitored path are inconsistent",
842 ));
843 }
844 };
845 let local_sample = discrete_sample_at_path(sample, path)?;
846 let Sample::Discrete(_, bin_index, _) = local_sample else {
847 return Err(Report::msg(format!(
848 "sample at monitored discrete path {path:?} is not discrete"
849 )));
850 };
851 if *bin_index >= monitor.bins.len() {
852 return Err(Report::msg(format!(
853 "sample bin {bin_index} is out of bounds for absolute monitor with {} bins at path {path:?}",
854 monitor.bins.len()
855 )));
856 }
857 monitor
858 .add_training_sample(local_sample, evaluation.abs())
859 .map_err(Report::msg)
860}
861
862fn summary_at_path<'a>(
863 summary: &'a DiscreteGridAccumulatorSummary,
864 path: &[usize],
865) -> Option<&'a DiscreteGridAccumulatorSummary> {
866 match path.split_first() {
867 None => Some(summary),
868 Some((bin_index, rest)) => summary
869 .bins
870 .get(*bin_index)?
871 .sub_summary
872 .as_deref()
873 .and_then(|sub_summary| summary_at_path(sub_summary, rest)),
874 }
875}
876
877fn first_non_trivial_discrete_context(
878 sampling_grid: &Grid<F<f64>>,
879) -> Option<DiscreteLevelContext> {
880 let path = first_non_trivial_discrete_path(sampling_grid)?;
881 let discrete_grid = discrete_grid_at_path(sampling_grid, &path)?;
882 Some(DiscreteLevelContext {
883 path,
884 pdfs: discrete_grid.bins.iter().map(|bin| bin.pdf).collect(),
885 })
886}
887
888fn discrete_axis_labels(sampling: &SamplingSettings) -> Vec<&'static str> {
889 match sampling {
890 SamplingSettings::Default(_) | SamplingSettings::MultiChanneling(_) => Vec::new(),
891 SamplingSettings::DiscreteGraphs(settings) => {
892 let mut labels = vec!["graph"];
893 match &settings.sampling_type {
894 DiscreteGraphSamplingType::Default(_)
895 | DiscreteGraphSamplingType::MultiChanneling(_)
896 | DiscreteGraphSamplingType::TropicalSampling(_) => {
897 if settings.sample_orientations {
898 labels.push("orientation");
899 }
900 }
901 DiscreteGraphSamplingType::SamplingMultiChanneling(_) => {
902 if settings.sample_orientations {
903 labels.push("orientation");
904 }
905 labels.push("sampling channel");
906 }
907 }
908 labels
909 }
910 }
911}
912
913fn orientation_description(orientation: &EdgeVec<Orientation>) -> String {
914 orientation
915 .iter()
916 .map(|(_, orientation)| match *orientation {
917 Orientation::Default => '+',
918 Orientation::Reversed => '-',
919 Orientation::Undirected => '0',
920 })
921 .collect()
922}
923
924fn graph_group_description<I>(graph_names: I) -> String
925where
926 I: IntoIterator<Item = String>,
927{
928 let graph_names = graph_names.into_iter().collect_vec();
929 if graph_names.len() <= 1 {
930 return graph_names.into_iter().next().unwrap_or_default();
931 }
932
933 format!("[{}]", graph_names.join(","))
934}
935
936fn first_non_trivial_discrete_bin_descriptions_for_process_integrand(
937 integrand: &ProcessIntegrand,
938 path: &[usize],
939 axis_label: &str,
940) -> Option<Vec<String>> {
941 match (integrand, axis_label) {
942 (ProcessIntegrand::Amplitude(integrand), "graph") => {
943 Some(
944 integrand
945 .data
946 .graph_group_structure
947 .iter()
948 .map(|group| {
949 graph_group_description(group.into_iter().map(|graph_id| {
950 integrand.data.graph_terms[graph_id].graph.name.clone()
951 }))
952 })
953 .collect(),
954 )
955 }
956 (ProcessIntegrand::CrossSection(integrand), "graph") => {
957 Some(
958 integrand
959 .data
960 .graph_group_structure
961 .iter()
962 .map(|group| {
963 graph_group_description(group.into_iter().map(|graph_id| {
964 integrand.data.graph_terms[graph_id].graph.name.clone()
965 }))
966 })
967 .collect(),
968 )
969 }
970 (ProcessIntegrand::Amplitude(integrand), "orientation") => {
971 let group_id = GroupId(*path.first()?);
972 let group = integrand.data.graph_group_structure.get(group_id)?;
973 let master = group.master();
974 Some(
975 integrand.data.graph_terms[master]
976 .orientations
977 .iter()
978 .map(orientation_description)
979 .collect(),
980 )
981 }
982 (ProcessIntegrand::CrossSection(integrand), "orientation") => {
983 let group_id = GroupId(*path.first()?);
984 let group = integrand.data.graph_group_structure.get(group_id)?;
985 let master = group.master();
986 Some(
987 integrand.data.graph_terms[master]
988 .orientations
989 .iter()
990 .map(orientation_description)
991 .collect(),
992 )
993 }
994 (ProcessIntegrand::Amplitude(integrand), "sampling channel") => {
995 let group_id = GroupId(*path.first()?);
996 let group = integrand.data.graph_group_structure.get(group_id)?;
997 let master = group.master();
998 let graph_term = &integrand.data.graph_terms[master];
999 let parameterization_settings = integrand
1000 .settings
1001 .sampling
1002 .get_parameterization_settings()
1003 .unwrap_or_default();
1004 let channel_ids = graph_term
1005 .sampling_channel_ids(¶meterization_settings)
1006 .ok()?;
1007 Some(
1008 channel_ids
1009 .iter()
1010 .map(|&channel_id| {
1011 graph_term
1012 .sampling_channel_label(channel_id, ¶meterization_settings)
1013 .ok()
1014 .flatten()
1015 .unwrap_or_else(|| format!("#{}", channel_id.index()))
1016 })
1017 .collect(),
1018 )
1019 }
1020 (ProcessIntegrand::CrossSection(integrand), "sampling channel") => {
1021 let group_id = GroupId(*path.first()?);
1022 let group = integrand.data.graph_group_structure.get(group_id)?;
1023 let master = group.master();
1024 let graph_term = &integrand.data.graph_terms[master];
1025 let parameterization_settings = integrand
1026 .settings
1027 .sampling
1028 .get_parameterization_settings()
1029 .unwrap_or_default();
1030 let channel_ids = graph_term
1031 .sampling_channel_ids(¶meterization_settings)
1032 .ok()?;
1033 Some(
1034 channel_ids
1035 .iter()
1036 .map(|&channel_id| {
1037 graph_term
1038 .sampling_channel_label(channel_id, ¶meterization_settings)
1039 .ok()
1040 .flatten()
1041 .unwrap_or_else(|| format!("#{}", channel_id.index()))
1042 })
1043 .collect(),
1044 )
1045 }
1046 _ => None,
1047 }
1048}
1049
1050fn first_non_trivial_discrete_bin_descriptions_for_integrand(
1051 integrand: &Integrand,
1052 path: &[usize],
1053 axis_label: &str,
1054) -> Option<Vec<String>> {
1055 match integrand {
1056 Integrand::ProcessIntegrand(process_integrand) => {
1057 first_non_trivial_discrete_bin_descriptions_for_process_integrand(
1058 process_integrand,
1059 path,
1060 axis_label,
1061 )
1062 }
1063 #[cfg(test)]
1064 Integrand::TestProbe(_) => None,
1065 }
1066}
1067
1068fn discrete_coordinate_label(
1069 integrand: &Integrand,
1070 path_prefix: &[usize],
1071 axis_label: &str,
1072 bin_index: usize,
1073) -> Option<String> {
1074 first_non_trivial_discrete_bin_descriptions_for_integrand(integrand, path_prefix, axis_label)?
1075 .get(bin_index)
1076 .cloned()
1077}
1078
1079fn build_persisted_discrete_breakdown_metadata(
1080 integrand: &Integrand,
1081 path: &[usize],
1082 discrete_axis_labels: &[String],
1083) -> Option<PersistedDiscreteBreakdownMetadata> {
1084 let axis_label = discrete_axis_labels.get(path.len())?.clone();
1085 let bin_labels =
1086 first_non_trivial_discrete_bin_descriptions_for_integrand(integrand, path, &axis_label)?;
1087
1088 let fixed_coordinates = path
1089 .iter()
1090 .enumerate()
1091 .map(|(axis_index, &bin_index)| {
1092 let fixed_axis_label = discrete_axis_labels.get(axis_index)?.clone();
1093 Some(DiscreteCoordinate {
1094 axis_label: fixed_axis_label.clone(),
1095 bin_index,
1096 bin_label: discrete_coordinate_label(
1097 integrand,
1098 &path[..axis_index],
1099 &fixed_axis_label,
1100 bin_index,
1101 ),
1102 })
1103 })
1104 .collect::<Option<Vec<_>>>()?;
1105
1106 Some(PersistedDiscreteBreakdownMetadata {
1107 axis_label,
1108 fixed_coordinates,
1109 bin_labels,
1110 })
1111}
1112
1113fn monitored_discrete_layout(
1114 grid: &Grid<F<f64>>,
1115 discrete_axis_labels: &[String],
1116 monitor_explicit_graph_subset: bool,
1117) -> Option<(Vec<usize>, String, usize)> {
1118 let path = if monitor_explicit_graph_subset
1119 && discrete_axis_labels
1120 .first()
1121 .is_some_and(|label| label == "graph")
1122 && matches!(grid, Grid::Discrete(_))
1123 {
1124 Vec::new()
1125 } else {
1126 first_non_trivial_discrete_path(grid)?
1127 };
1128 let axis_label = discrete_axis_labels.get(path.len())?.clone();
1129 let discrete_grid = discrete_grid_at_path(grid, &path)?;
1130 Some((path, axis_label, discrete_grid.bins.len()))
1131}
1132
1133fn coalesce_first_non_trivial_discrete_bin_descriptions(
1134 axis_label: &str,
1135 slot_descriptions: &[(String, Vec<String>)],
1136) -> (Option<Vec<String>>, Option<String>) {
1137 let Some((reference_slot, reference_descriptions)) = slot_descriptions.first() else {
1138 return (None, None);
1139 };
1140
1141 if let Some((slot_key, _)) = slot_descriptions
1142 .iter()
1143 .skip(1)
1144 .find(|(_, descriptions)| descriptions != reference_descriptions)
1145 {
1146 return (
1147 None,
1148 Some(format!(
1149 "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.",
1150 reference_slot.blue(),
1151 slot_key.blue()
1152 )),
1153 );
1154 }
1155
1156 (Some(reference_descriptions.clone()), None)
1157}
1158
1159fn resolve_first_non_trivial_discrete_bin_descriptions(
1160 slots: &[IntegrationSlot],
1161 monitored_path: &[usize],
1162 axis_label: &str,
1163) -> (Option<Vec<String>>, Option<String>) {
1164 let Some(slot_descriptions) = slots
1165 .iter()
1166 .map(|slot| {
1167 first_non_trivial_discrete_bin_descriptions_for_integrand(
1168 &slot.integrand,
1169 monitored_path,
1170 axis_label,
1171 )
1172 .map(|descriptions| (slot.meta.key(), descriptions))
1173 })
1174 .collect::<Option<Vec<_>>>()
1175 else {
1176 return (None, None);
1177 };
1178
1179 coalesce_first_non_trivial_discrete_bin_descriptions(axis_label, &slot_descriptions)
1180}
1181
1182fn resolve_monitored_discrete_setup(
1183 sampling_correlation_mode: SamplingCorrelationMode,
1184 slots: &[IntegrationSlot],
1185 sampling_states: &[SamplingSlotState],
1186) -> MonitoredDiscreteSetup {
1187 let Some(reference_state) = sampling_states.first() else {
1188 return MonitoredDiscreteSetup::default();
1189 };
1190 let Some(reference_slot) = slots.first() else {
1191 return MonitoredDiscreteSetup::default();
1192 };
1193 let monitor_explicit_graph_subset = !reference_slot
1194 .settings
1195 .sampling
1196 .selected_graph_names()
1197 .is_empty();
1198
1199 let Some((reference_path, reference_axis_label, reference_bin_count)) =
1200 monitored_discrete_layout(
1201 &reference_state.grid,
1202 &reference_state.discrete_axis_labels,
1203 monitor_explicit_graph_subset,
1204 )
1205 else {
1206 return MonitoredDiscreteSetup::default();
1207 };
1208
1209 if sampling_correlation_mode == SamplingCorrelationMode::Uncorrelated {
1210 for (slot_index, sampling_state) in sampling_states.iter().enumerate().skip(1) {
1211 let Some((path, axis_label, bin_count)) = monitored_discrete_layout(
1212 &sampling_state.grid,
1213 &sampling_state.discrete_axis_labels,
1214 !slots[slot_index]
1215 .settings
1216 .sampling
1217 .selected_graph_names()
1218 .is_empty(),
1219 ) else {
1220 return MonitoredDiscreteSetup {
1221 warning: Some(
1222 "Selected integrands do not all expose a compatible first non-trivial monitored discrete dimension. Shared discrete-bin monitoring tables will be disabled."
1223 .to_string(),
1224 ),
1225 ..MonitoredDiscreteSetup::default()
1226 };
1227 };
1228
1229 if path != reference_path
1230 || axis_label != reference_axis_label
1231 || bin_count != reference_bin_count
1232 {
1233 return MonitoredDiscreteSetup {
1234 warning: Some(format!(
1235 "Selected integrands do not share a compatible first non-trivial monitored discrete layout (mismatch at {}). Shared discrete-bin monitoring tables will be disabled.",
1236 slots[slot_index].meta.key().blue()
1237 )),
1238 ..MonitoredDiscreteSetup::default()
1239 };
1240 }
1241 }
1242 }
1243
1244 let (descriptions, label_warning) = resolve_first_non_trivial_discrete_bin_descriptions(
1245 slots,
1246 &reference_path,
1247 &reference_axis_label,
1248 );
1249
1250 MonitoredDiscreteSetup {
1251 path: Some(reference_path),
1252 axis_label: Some(reference_axis_label),
1253 bin_descriptions: descriptions,
1254 warning: label_warning,
1255 }
1256}
1257
1258fn render_orientation_description(description: &str) -> String {
1259 description
1260 .chars()
1261 .map(|sign| match sign {
1262 '+' => "+".green().bold().to_string(),
1263 '-' => "-".red().bold().to_string(),
1264 '0' => "0".dimmed().to_string(),
1265 other => other.to_string(),
1266 })
1267 .join("")
1268}
1269
1270fn render_graph_description(description: &str) -> String {
1271 if let Some(inner) = description
1272 .strip_prefix('[')
1273 .and_then(|trimmed| trimmed.strip_suffix(']'))
1274 {
1275 let names = inner
1276 .split(',')
1277 .filter(|name| !name.is_empty())
1278 .collect_vec();
1279 if names.is_empty() {
1280 return description.bold().green().to_string();
1281 }
1282
1283 let rendered = names
1284 .into_iter()
1285 .enumerate()
1286 .map(|(index, name)| {
1287 if index == 0 {
1288 name.bold().green().to_string()
1289 } else {
1290 name.bold().blue().to_string()
1291 }
1292 })
1293 .join(",");
1294 return format!("[{rendered}]");
1295 }
1296
1297 description.bold().green().to_string()
1298}
1299
1300fn render_bin_description(axis_label: &str, description: &str) -> String {
1301 match axis_label {
1302 "orientation" => render_orientation_description(description),
1303 "graph" => render_graph_description(description),
1304 _ => description.bold().green().to_string(),
1305 }
1306}
1307
1308fn contribution_bin_label(integration_state: &IntegrationState, bin_index: usize) -> String {
1309 if let (Some(axis_label), Some(descriptions)) = (
1310 integration_state
1311 .first_non_trivial_discrete_label
1312 .as_deref(),
1313 integration_state
1314 .first_non_trivial_discrete_bin_descriptions
1315 .as_ref(),
1316 ) && let Some(description) = descriptions.get(bin_index)
1317 {
1318 return format!(
1319 "#{bin_index}: {}",
1320 render_bin_description(axis_label, description)
1321 );
1322 }
1323
1324 format!("#{bin_index}")
1325}
1326
1327fn contribution_header_label(
1328 integration_state: &IntegrationState,
1329 discrete_monitoring_enabled: bool,
1330) -> String {
1331 if discrete_monitoring_enabled
1332 && let Some(label) = integration_state
1333 .first_non_trivial_discrete_label
1334 .as_deref()
1335 {
1336 return format!("Contribution (idx={label})")
1337 .bold()
1338 .blue()
1339 .to_string();
1340 }
1341
1342 "Contribution".bold().blue().to_string()
1343}
1344
1345fn format_percentage_sig(value: f64, significant_digits: usize) -> String {
1346 if !value.is_finite() {
1347 return "None".red().to_string();
1348 }
1349
1350 if value == 0.0 {
1351 let decimals = significant_digits.saturating_sub(1);
1352 return format!("{:.*}%", decimals, 0.0);
1353 }
1354
1355 let abs_value = value.abs();
1356 let exponent = abs_value.log10().floor() as i32;
1357 if exponent < -2 || exponent >= significant_digits as i32 {
1358 return format!("{:.*e}%", significant_digits.saturating_sub(1), value);
1359 }
1360
1361 let decimals = (significant_digits as i32 - exponent - 1).max(0) as usize;
1362 format!("{value:.decimals$}%")
1363}
1364
1365#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1366enum UncertaintyNotation {
1367 Dynamic,
1368 Scientific,
1369}
1370
1371fn format_significant_percentage(
1372 value: f64,
1373 significant_digits: usize,
1374 scientific_threshold: Option<(f64, f64)>,
1375) -> String {
1376 if !value.is_finite() {
1377 return "None".red().to_string();
1378 }
1379
1380 if value == 0.0 {
1381 return format!("{:.*}%", significant_digits.saturating_sub(1), 0.0);
1382 }
1383
1384 let abs_value = value.abs();
1385 if scientific_threshold.is_some_and(|(upper, lower)| abs_value >= upper || abs_value < lower) {
1386 return format!("{:.*e}%", significant_digits.saturating_sub(1), value);
1387 }
1388
1389 let exponent = abs_value.log10().floor() as i32;
1390 let decimals = (significant_digits as i32 - exponent - 1).max(0) as usize;
1391 format!("{value:.decimals$}%")
1392}
1393
1394fn should_use_scientific_uncertainty_notation(avg: F<f64>, err: F<f64>) -> bool {
1395 let avg_abs = avg.abs().0;
1396 let err_abs = err.abs().0;
1397 avg_abs >= 1e6
1398 || (avg_abs != 0.0 && avg_abs < 1e-5)
1399 || (avg.is_zero() && !(1e-4..1e5).contains(&err_abs))
1400}
1401
1402fn format_uncertainty_with_notation(
1403 avg: F<f64>,
1404 err: F<f64>,
1405 notation: UncertaintyNotation,
1406) -> String {
1407 if !matches!(notation, UncertaintyNotation::Scientific)
1408 && !should_use_scientific_uncertainty_notation(avg, err)
1409 {
1410 return utils::format_uncertainty(avg, err);
1411 }
1412
1413 if !avg.0.is_finite() || !err.0.is_finite() {
1414 return utils::format_uncertainty(avg, err);
1415 }
1416
1417 let exponent = if avg.is_non_zero() {
1418 avg.abs().0.log10().floor() as i32
1419 } else if err.is_non_zero() {
1420 err.abs().0.log10().floor() as i32
1421 } else {
1422 0
1423 };
1424 let scale = 10_f64.powi(exponent);
1425 let mantissa = utils::format_uncertainty(F(avg.0 / scale), F(err.0 / scale));
1426 format!("{mantissa}e{exponent}")
1427}
1428
1429fn format_signed_uncertainty(avg: F<f64>, err: F<f64>, notation: UncertaintyNotation) -> String {
1430 let formatted = format_uncertainty_with_notation(avg, err, notation);
1431 if avg.0.is_sign_negative() {
1432 formatted
1433 } else {
1434 format!("+{formatted}")
1435 }
1436}
1437
1438fn format_abbreviated_count(value: usize) -> String {
1439 if value < 1_000 {
1440 return value.to_string();
1441 }
1442
1443 let value = value as f64;
1444 if value < 1_000_000.0 {
1445 return format!("{:.2}K", value / 1_000.0);
1446 }
1447 if value < 1_000_000_000.0 {
1448 return format!("{:.2}M", value / 1_000_000.0);
1449 }
1450 if value < 1_000_000_000_000.0 {
1451 return format!("{:.2}B", value / 1_000_000_000.0);
1452 }
1453
1454 format!("{:.3}T", value / 1_000_000_000_000.0)
1455}
1456
1457fn build_integral_result_cells(
1458 itg: &StatisticsAccumulator<F<f64>>,
1459 slot_meta: &SlotMeta,
1460 i_iter: usize,
1461 tag: &str,
1462 trgt: Option<F<f64>>,
1463) -> IntegralResultCells {
1464 let relative_error = format_relative_error_cell(itg);
1465 let chi_sq = format_chi_sq_cell(itg, i_iter);
1466 let (delta_sigma, delta_percent) = format_delta_cells(itg, trgt);
1467 let mwi = format_mwi_cell(itg);
1468
1469 IntegralResultCells {
1470 integrand: format!(
1471 "{} {}:",
1472 slot_label(slot_meta),
1473 format!("{:-2}", tag).blue().bold(),
1474 ),
1475 value: format_signed_uncertainty(itg.avg, itg.err, UncertaintyNotation::Scientific)
1476 .blue()
1477 .bold()
1478 .to_string(),
1479 relative_error,
1480 chi_sq,
1481 delta_sigma,
1482 delta_percent,
1483 mwi,
1484 }
1485}
1486
1487fn format_relative_error_cell(itg: &StatisticsAccumulator<F<f64>>) -> String {
1488 format_relative_error_from_estimate(itg.avg, itg.err)
1489}
1490
1491fn format_relative_error_from_estimate(avg: F<f64>, err: F<f64>) -> String {
1492 if avg.is_zero() {
1493 return String::new();
1494 }
1495
1496 let formatted =
1497 format_significant_percentage((err / avg).abs().0 * 100.0, 3, Some((1.0e4, 1.0e-4)));
1498 if (err / avg).abs().0 > 0.01 {
1499 formatted.red().to_string()
1500 } else {
1501 formatted.green().to_string()
1502 }
1503}
1504
1505fn format_chi_sq_cell(itg: &StatisticsAccumulator<F<f64>>, i_iter: usize) -> String {
1506 let chi_sq = format!("{:.3}", itg.chi_sq.0 / (i_iter as f64));
1507 if itg.chi_sq / F::<f64>::new_from_usize(i_iter) > F(5.) {
1508 chi_sq.red().to_string()
1509 } else {
1510 chi_sq
1511 }
1512}
1513
1514fn format_delta_cells(
1515 itg: &StatisticsAccumulator<F<f64>>,
1516 trgt: Option<F<f64>>,
1517) -> (Option<String>, Option<String>) {
1518 format_delta_cells_from_estimate(itg.avg, itg.err, trgt)
1519}
1520
1521fn format_delta_cells_from_estimate(
1522 avg: F<f64>,
1523 err: F<f64>,
1524 trgt: Option<F<f64>>,
1525) -> (Option<String>, Option<String>) {
1526 let Some(t) = trgt else {
1527 return (None, None);
1528 };
1529
1530 let delta_in_sigmas = if err.is_zero() {
1531 0.0
1532 } else {
1533 (t - avg).abs().0 / err.0
1534 };
1535 let delta_in_percent = if t.abs().is_non_zero() {
1536 (t - avg).abs().0 / t.abs().0 * 100.
1537 } else {
1538 0.
1539 };
1540 let is_outside_target =
1541 delta_in_sigmas > 5. || (t.abs().is_non_zero() && ((t - avg).abs() / t.abs()).0 > 0.01);
1542 let sigma_text = format!("Δ = {:.3}σ", delta_in_sigmas);
1543 let percent_text = format!("Δ = {:.3}%", delta_in_percent);
1544 if is_outside_target {
1545 (
1546 Some(sigma_text.red().to_string()),
1547 Some(percent_text.red().to_string()),
1548 )
1549 } else {
1550 (
1551 Some(sigma_text.green().to_string()),
1552 Some(percent_text.green().to_string()),
1553 )
1554 }
1555}
1556
1557fn format_mwi_cell(itg: &StatisticsAccumulator<F<f64>>) -> String {
1558 let mwi_value = max_weight_impact(itg);
1559 let formatted = format!("{:.4e}", mwi_value.0);
1560 if mwi_value > F(1.) {
1561 formatted.red().to_string()
1562 } else {
1563 formatted
1564 }
1565}
1566
1567fn max_weight_impact(itg: &StatisticsAccumulator<F<f64>>) -> F<f64> {
1568 if itg.avg.abs().0 == 0. || itg.processed_samples == 0 {
1569 return F(0.0);
1570 }
1571
1572 itg.max_eval_negative.abs().max(itg.max_eval_positive.abs())
1573 / (itg.avg.abs() * F::<f64>::new_from_usize(itg.processed_samples))
1574}
1575
1576fn build_table_component_result(
1577 slot_meta: &SlotMeta,
1578 iter: usize,
1579 component: &str,
1580 accumulator: &StatisticsAccumulator<F<f64>>,
1581 target_component: Option<F<f64>>,
1582) -> IntegrationTableComponentResult {
1583 let cells =
1584 build_integral_result_cells(accumulator, slot_meta, iter, component, target_component);
1585 IntegrationTableComponentResult {
1586 component: component.to_string(),
1587 value: accumulator.avg,
1588 error: accumulator.err,
1589 relative_error_percent: accumulator
1590 .avg
1591 .is_non_zero()
1592 .then(|| (accumulator.err / accumulator.avg).abs().0 * 100.0),
1593 chi_sq_per_dof: if iter > 0 {
1594 accumulator.chi_sq.0 / (iter as f64)
1595 } else {
1596 0.0
1597 },
1598 target_delta_sigma: cells.delta_sigma.as_ref().map(|_| {
1599 if accumulator.err.is_zero() {
1600 0.0
1601 } else if let Some(target_value) = target_component {
1602 (target_value - accumulator.avg).abs().0 / accumulator.err.0
1603 } else {
1604 0.0
1605 }
1606 }),
1607 target_delta_percent: target_component.map(|target_value| {
1608 if target_value.is_zero() {
1609 0.0
1610 } else {
1611 (target_value - accumulator.avg).abs().0 / target_value.abs().0 * 100.0
1612 }
1613 }),
1614 max_weight_impact: max_weight_impact(accumulator).0,
1615 }
1616}
1617
1618fn build_table_result_summary(
1619 slot_meta: &SlotMeta,
1620 accumulator: &ComplexAccumulator,
1621 iter: usize,
1622 target: Option<Complex<F<f64>>>,
1623) -> Vec<IntegrationTableComponentResult> {
1624 [
1625 ("re", &accumulator.re, target.as_ref().map(|value| value.re)),
1626 ("im", &accumulator.im, target.as_ref().map(|value| value.im)),
1627 ]
1628 .into_iter()
1629 .map(|(component, accumulator, target_component)| {
1630 build_table_component_result(slot_meta, iter, component, accumulator, target_component)
1631 })
1632 .collect()
1633}
1634
1635fn build_absolute_table_result_summary(
1636 slot_meta: &SlotMeta,
1637 accumulator: &ComplexAccumulator,
1638 iter: usize,
1639) -> Vec<IntegrationTableComponentResult> {
1640 [
1641 ("|re|", &accumulator.absolute_re),
1642 ("|im|", &accumulator.absolute_im),
1643 ]
1644 .into_iter()
1645 .map(|(component, accumulator)| {
1646 build_table_component_result(slot_meta, iter, component, accumulator, None)
1647 })
1648 .collect()
1649}
1650
1651fn build_max_weight_info_summary(
1652 discrete_axis_labels: &[String],
1653 accumulator: &ComplexAccumulator,
1654) -> Vec<MaxWeightInfoEntry> {
1655 [
1656 (
1657 "re",
1658 "+",
1659 accumulator.re.max_eval_positive,
1660 accumulator.re.max_eval_positive_xs.as_ref(),
1661 ),
1662 (
1663 "re",
1664 "-",
1665 accumulator.re.max_eval_negative,
1666 accumulator.re.max_eval_negative_xs.as_ref(),
1667 ),
1668 (
1669 "im",
1670 "+",
1671 accumulator.im.max_eval_positive,
1672 accumulator.im.max_eval_positive_xs.as_ref(),
1673 ),
1674 (
1675 "im",
1676 "-",
1677 accumulator.im.max_eval_negative,
1678 accumulator.im.max_eval_negative_xs.as_ref(),
1679 ),
1680 ]
1681 .into_iter()
1682 .filter_map(|(component, sign, max_eval, sample)| {
1683 if max_eval.is_zero() {
1684 return None;
1685 }
1686
1687 Some(MaxWeightInfoEntry {
1688 component: component.to_string(),
1689 sign: sign.to_string(),
1690 max_eval,
1691 coordinates: sample
1692 .map(|sample| format_max_eval_sample(sample, discrete_axis_labels, &[])),
1693 })
1694 })
1695 .collect()
1696}
1697
1698fn build_absolute_max_weight_info_summary(
1699 discrete_axis_labels: &[String],
1700 accumulator: &ComplexAccumulator,
1701) -> Vec<MaxWeightInfoEntry> {
1702 [
1703 ("re", &accumulator.absolute_re),
1704 ("im", &accumulator.absolute_im),
1705 ]
1706 .into_iter()
1707 .filter_map(|(component, accumulator)| {
1708 if accumulator.max_eval_positive.is_zero() {
1709 return None;
1710 }
1711 Some(MaxWeightInfoEntry {
1712 component: component.to_string(),
1713 sign: "+".to_string(),
1714 max_eval: accumulator.max_eval_positive,
1715 coordinates: accumulator
1716 .max_eval_positive_xs
1717 .as_ref()
1718 .map(|sample| format_max_eval_sample(sample, discrete_axis_labels, &[])),
1719 })
1720 })
1721 .collect()
1722}
1723
1724#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1725struct LiveIterationProgress {
1726 completed_points: usize,
1727 target_points: usize,
1728}
1729
1730fn build_iteration_status_header_left(
1731 elapsed_time: Duration,
1732 iter: usize,
1733 live_progress: Option<LiveIterationProgress>,
1734) -> String {
1735 let iteration_label = if live_progress.is_some() {
1736 format!("Iteration #{:-4} ( running )", iter)
1737 .bold()
1738 .green()
1739 } else {
1740 format!("Iteration #{:-4} ( completed )", iter)
1741 .bold()
1742 .green()
1743 };
1744
1745 format!(
1746 "[ {} ] {}",
1747 format!(
1748 "{:^7}",
1749 utils::format_wdhms(elapsed_time.as_secs() as usize)
1750 )
1751 .bold(),
1752 iteration_label,
1753 )
1754}
1755
1756fn build_iteration_status_header_middle(
1757 cur_points: usize,
1758 total_points: usize,
1759 live_progress: Option<LiveIterationProgress>,
1760) -> String {
1761 let per_iteration = if let Some(progress) = live_progress {
1762 let percentage = if progress.target_points == 0 {
1763 String::from("0.0%")
1764 } else {
1765 format!(
1766 "{:.1}%",
1767 (progress.completed_points as f64) / (progress.target_points as f64) * 100.0
1768 )
1769 .green()
1770 .to_string()
1771 };
1772 format!(
1773 "Iteration progress {}/{} {}",
1774 format_iteration_points(progress.completed_points),
1775 format_iteration_points(progress.target_points),
1776 percentage
1777 )
1778 } else if cur_points > 0 {
1779 format!(
1780 "# samples per iteration = {}",
1781 format_iteration_points(cur_points)
1782 )
1783 .blue()
1784 .bold()
1785 .to_string()
1786 } else {
1787 String::new()
1788 };
1789
1790 let total = format!("# samples total = {}", format_total_points(total_points))
1791 .bold()
1792 .green()
1793 .to_string();
1794 if per_iteration.is_empty() {
1795 total
1796 } else {
1797 format!("{per_iteration} {total}")
1798 }
1799}
1800
1801fn build_iteration_status_header_tail(
1802 cores: usize,
1803 elapsed_time: Duration,
1804 n_samples_evaluated: usize,
1805) -> String {
1806 let sample_core_time = if n_samples_evaluated == 0 {
1807 "N/A /sample/core".red().to_string()
1808 } else {
1809 format!(
1810 "{} /sample/core",
1811 utils::format_evaluation_time_from_f64(
1812 elapsed_time.as_secs_f64() / (n_samples_evaluated as f64) * (cores as f64),
1813 )
1814 .bold()
1815 .green()
1816 )
1817 };
1818
1819 format!(
1820 "{sample_core_time} {}",
1821 format!("({cores} cores)").bold().blue()
1822 )
1823}
1824
1825fn max_weight_row_descriptors(
1826 phase_display: IntegrationStatusPhaseDisplay,
1827) -> Vec<(ComponentKind, &'static str, bool)> {
1828 let mut rows = Vec::new();
1829 if phase_display.shows_real() {
1830 rows.push((ComponentKind::Real, "+", true));
1831 rows.push((ComponentKind::Real, "-", false));
1832 }
1833 if phase_display.shows_imag() {
1834 rows.push((ComponentKind::Imag, "+", true));
1835 rows.push((ComponentKind::Imag, "-", false));
1836 }
1837 rows
1838}
1839
1840type MaxEvalEntry<'a> = Option<(F<f64>, Option<&'a Sample<F<f64>>>)>;
1841
1842fn max_eval_entry(accumulator: &StatisticsAccumulator<F<f64>>, positive: bool) -> MaxEvalEntry<'_> {
1843 let (value, sample) = if positive {
1844 (
1845 accumulator.max_eval_positive,
1846 accumulator.max_eval_positive_xs.as_ref(),
1847 )
1848 } else {
1849 (
1850 accumulator.max_eval_negative,
1851 accumulator.max_eval_negative_xs.as_ref(),
1852 )
1853 };
1854
1855 if value.is_zero() {
1856 None
1857 } else {
1858 Some((value, sample))
1859 }
1860}
1861
1862#[derive(Clone, Serialize, Deserialize, Encode, Decode)]
1865pub struct IntegrationState {
1866 pub num_points: usize,
1867 #[bincode(with_serde)]
1868 pub all_integrals: Vec<ComplexAccumulator>,
1869 #[bincode(with_serde)]
1870 slot_re_summaries: Vec<Option<DiscreteGridAccumulatorSummary>>,
1871 #[bincode(with_serde)]
1872 slot_im_summaries: Vec<Option<DiscreteGridAccumulatorSummary>>,
1873 #[bincode(with_serde)]
1874 pub(crate) slot_absolute_re_summaries: Vec<Option<DiscreteGridAccumulatorSummary>>,
1875 #[bincode(with_serde)]
1876 pub(crate) slot_absolute_im_summaries: Vec<Option<DiscreteGridAccumulatorSummary>>,
1877 pub stats: StatisticsCounter,
1878 pub slot_stats: Vec<StatisticsCounter>,
1879 pub slot_metas: Vec<SlotMeta>,
1880 pub sampling_correlation_mode: SamplingCorrelationMode,
1881 sampling_states: Vec<SamplingSlotState>,
1882 monitored_discrete_path: Option<Vec<usize>>,
1883 pub first_non_trivial_discrete_label: Option<String>,
1884 pub first_non_trivial_discrete_bin_descriptions: Option<Vec<String>>,
1885 slot_first_non_trivial_discrete_breakdown_metadata:
1886 Vec<Option<PersistedDiscreteBreakdownMetadata>>,
1887 pub iter: usize,
1888 pub elapsed_seconds: f64,
1889 pub n_cores: usize,
1890}
1891
1892impl IntegrationState {
1893 fn new_from_settings(
1894 sampling_correlation_mode: SamplingCorrelationMode,
1895 sampling_states: Vec<SamplingSlotState>,
1896 slot_metas: Vec<SlotMeta>,
1897 monitored_discrete_path: Option<Vec<usize>>,
1898 first_non_trivial_discrete_label: Option<String>,
1899 first_non_trivial_discrete_bin_descriptions: Option<Vec<String>>,
1900 slot_first_non_trivial_discrete_breakdown_metadata: Vec<
1901 Option<PersistedDiscreteBreakdownMetadata>,
1902 >,
1903 ) -> Self {
1904 let num_points = 0;
1905 let iter = 0;
1906 let all_integrals = vec![ComplexAccumulator::new(); slot_metas.len()];
1907 let slot_re_summaries = (0..slot_metas.len())
1908 .map(|slot_index| {
1909 DiscreteGridAccumulatorSummary::from_grid(
1910 &sampling_states[sampling_correlation_mode.state_index(slot_index)].grid,
1911 )
1912 })
1913 .collect();
1914 let slot_im_summaries = (0..slot_metas.len())
1915 .map(|slot_index| {
1916 DiscreteGridAccumulatorSummary::from_grid(
1917 &sampling_states[sampling_correlation_mode.state_index(slot_index)].grid,
1918 )
1919 })
1920 .collect();
1921 let absolute_summary_for_slot = |slot_index| {
1922 monitored_discrete_path.as_deref().map(|path| {
1923 let grid = &sampling_states[sampling_correlation_mode.state_index(slot_index)].grid;
1924 let discrete_grid = discrete_grid_at_path(grid, path)
1925 .expect("validated monitored discrete path must match every sampling grid");
1926 DiscreteGridAccumulatorSummary::from_shallow_grid(discrete_grid)
1927 })
1928 };
1929 let slot_absolute_re_summaries = (0..slot_metas.len())
1930 .map(&absolute_summary_for_slot)
1931 .collect();
1932 let slot_absolute_im_summaries = (0..slot_metas.len())
1933 .map(absolute_summary_for_slot)
1934 .collect();
1935 let stats = StatisticsCounter::new_empty();
1936 let slot_stats = vec![StatisticsCounter::new_empty(); slot_metas.len()];
1937
1938 Self {
1939 num_points,
1940 all_integrals,
1941 slot_re_summaries,
1942 slot_im_summaries,
1943 slot_absolute_re_summaries,
1944 slot_absolute_im_summaries,
1945 stats,
1946 slot_stats,
1947 slot_metas,
1948 sampling_correlation_mode,
1949 sampling_states,
1950 monitored_discrete_path,
1951 first_non_trivial_discrete_label,
1952 first_non_trivial_discrete_bin_descriptions,
1953 slot_first_non_trivial_discrete_breakdown_metadata,
1954 iter,
1955 elapsed_seconds: 0.0,
1956 n_cores: 1,
1957 }
1958 }
1959
1960 fn update_iter(&mut self, use_weighted_average: bool) {
1961 self.all_integrals
1962 .iter_mut()
1963 .for_each(|acc| acc.update_iter(use_weighted_average));
1964 }
1965
1966 fn sampling_state_for_slot(&self, slot_index: usize) -> &SamplingSlotState {
1967 &self.sampling_states[self.sampling_correlation_mode.state_index(slot_index)]
1968 }
1969
1970 fn sampling_state_for_slot_mut(&mut self, slot_index: usize) -> &mut SamplingSlotState {
1971 let sampling_state_index = self.sampling_correlation_mode.state_index(slot_index);
1972 &mut self.sampling_states[sampling_state_index]
1973 }
1974
1975 fn monitored_discrete_context_for_slot(
1976 &self,
1977 slot_index: usize,
1978 ) -> Option<DiscreteLevelContext> {
1979 let path = self.monitored_discrete_path.clone()?;
1980 let discrete_grid =
1981 discrete_grid_at_path(&self.sampling_state_for_slot(slot_index).grid, &path)?;
1982 Some(DiscreteLevelContext {
1983 path,
1984 pdfs: discrete_grid.bins.iter().map(|bin| bin.pdf).collect(),
1985 })
1986 }
1987
1988 fn monitored_discrete_context(&self) -> Option<DiscreteLevelContext> {
1989 self.monitored_discrete_context_for_slot(0)
1990 }
1991}
1992
1993struct CoreIterationState {
1994 slot_integrands: Vec<Integrand>,
1995 stats: StatisticsCounter,
1996 slot_stats: Vec<StatisticsCounter>,
1997 integrals: Vec<ComplexAccumulator>,
1998 sampling_correlation_mode: SamplingCorrelationMode,
1999 sampling_states: Vec<CoreSamplingSlotState>,
2000 slot_re_grids: Vec<Grid<F<f64>>>,
2001 slot_im_grids: Vec<Grid<F<f64>>>,
2002 monitored_discrete_path: Option<Vec<usize>>,
2003 slot_absolute_re_grids: Vec<Option<DiscreteGrid<F<f64>>>>,
2004 slot_absolute_im_grids: Vec<Option<DiscreteGrid<F<f64>>>>,
2005 remaining_points: usize,
2006 completed_points: usize,
2007 failed_samples: Vec<(usize, Sample<F<f64>>, EvaluationMetaData)>,
2008}
2009
2010struct CoreSamplingSlotState {
2011 sampling_grid: Grid<F<f64>>,
2012 rng: MonteCarloRng,
2013}
2014
2015impl CoreIterationState {
2016 fn new(
2017 slot_integrands: Vec<Integrand>,
2018 sampling_correlation_mode: SamplingCorrelationMode,
2019 sampling_grid_templates: &[Grid<F<f64>>],
2020 monitored_discrete_path: Option<&[usize]>,
2021 seed: u64,
2022 sample_skip: usize,
2023 remaining_points: usize,
2024 ) -> Self {
2025 let n_slots = slot_integrands.len();
2026 let sampling_states = (0..sampling_correlation_mode.state_count(n_slots))
2027 .map(|sampling_state_index| {
2028 let slot_index = match sampling_correlation_mode {
2029 SamplingCorrelationMode::Correlated => 0,
2030 SamplingCorrelationMode::Uncorrelated => sampling_state_index,
2031 };
2032 let mut rng = MonteCarloRng::new(slot_seed(seed, slot_index), 0);
2033 let mut sampling_grid =
2034 sampling_grid_templates[sampling_state_index].clone_without_samples();
2035
2036 for _ in 0..sample_skip {
2037 let mut sample = Sample::new();
2038 sampling_grid.sample(&mut rng, &mut sample);
2039 }
2040
2041 CoreSamplingSlotState { sampling_grid, rng }
2042 })
2043 .collect_vec();
2044 let absolute_monitor_for_slot = |slot_index| {
2045 monitored_discrete_path.map(|path| {
2046 shallow_discrete_monitor_grid(
2047 &sampling_grid_templates[sampling_correlation_mode.state_index(slot_index)],
2048 path,
2049 )
2050 .expect("validated monitored discrete path must match every sampling grid")
2051 })
2052 };
2053
2054 Self {
2055 slot_integrands,
2056 stats: StatisticsCounter::new_empty(),
2057 slot_stats: vec![StatisticsCounter::new_empty(); n_slots],
2058 integrals: vec![ComplexAccumulator::new(); n_slots],
2059 sampling_correlation_mode,
2060 sampling_states,
2061 slot_re_grids: (0..n_slots)
2062 .map(|slot_index| {
2063 sampling_grid_templates[sampling_correlation_mode.state_index(slot_index)]
2064 .clone_without_samples()
2065 })
2066 .collect(),
2067 slot_im_grids: (0..n_slots)
2068 .map(|slot_index| {
2069 sampling_grid_templates[sampling_correlation_mode.state_index(slot_index)]
2070 .clone_without_samples()
2071 })
2072 .collect(),
2073 monitored_discrete_path: monitored_discrete_path.map(|path| path.to_vec()),
2074 slot_absolute_re_grids: (0..n_slots).map(&absolute_monitor_for_slot).collect(),
2075 slot_absolute_im_grids: (0..n_slots).map(absolute_monitor_for_slot).collect(),
2076 remaining_points,
2077 completed_points: 0,
2078 failed_samples: Vec::new(),
2079 }
2080 }
2081
2082 fn evaluate_chunk(
2083 &mut self,
2084 slot_settings: &[&RuntimeSettings],
2085 slot_targets: &[EvaluationTarget<'_>],
2086 iter: usize,
2087 current_max_evals: &[Complex<F<f64>>],
2088 current_integral_estimates: &[Option<(f64, f64)>],
2089 chunk_size: usize,
2090 ) -> Result<usize> {
2091 let n_points = chunk_size.min(self.remaining_points);
2092 if n_points == 0 {
2093 return Ok(0);
2094 }
2095
2096 let chunk_start = Instant::now();
2097 let mut batch_evaluation_time = Duration::ZERO;
2098 let mut batch_stats = StatisticsCounter::new_empty();
2099 let mut processed_points = 0;
2100 let mut total_sample_evaluations = 0;
2101 let monitored_discrete_path = self.monitored_discrete_path.clone();
2102
2103 match self.sampling_correlation_mode {
2104 SamplingCorrelationMode::Correlated => {
2105 let mut samples = Vec::with_capacity(n_points);
2106 for _ in 0..n_points {
2107 if is_interrupted() {
2108 break;
2109 }
2110
2111 let mut sample = Sample::new();
2112 let sampling_state = &mut self.sampling_states[0];
2113 sampling_state
2114 .sampling_grid
2115 .sample(&mut sampling_state.rng, &mut sample);
2116 processed_points += 1;
2117 samples.push(sample);
2118 }
2119
2120 if processed_points == 0 {
2121 return Ok(0);
2122 }
2123
2124 let mut slot_results = Vec::with_capacity(self.slot_integrands.len());
2125
2126 for (slot_index, integrand) in self.slot_integrands.iter_mut().enumerate() {
2127 let evaluation_start = Instant::now();
2128 let raw_batch = integrand.evaluate_samples_raw_with_estimate(
2129 &samples,
2130 slot_targets[slot_index],
2131 iter,
2132 false,
2133 true,
2134 current_max_evals[slot_index],
2135 current_integral_estimates[slot_index],
2136 )?;
2137 if raw_batch.samples.len() < samples.len() {
2138 return Ok(0);
2139 }
2140 batch_evaluation_time += evaluation_start.elapsed();
2141 total_sample_evaluations += raw_batch.samples.len();
2142 batch_stats = batch_stats.merged(&raw_batch.statistics);
2143 self.slot_stats[slot_index] =
2144 self.slot_stats[slot_index].merged(&raw_batch.statistics);
2145 slot_results.push(raw_batch.samples);
2146 }
2147
2148 for (sample_index, sample) in samples.iter().enumerate() {
2149 for (
2150 slot_index,
2151 (
2152 core_accumulator,
2153 re_grid,
2154 im_grid,
2155 absolute_re_grid,
2156 absolute_im_grid,
2157 results,
2158 ),
2159 ) in izip!(
2160 self.integrals.iter_mut(),
2161 self.slot_re_grids.iter_mut(),
2162 self.slot_im_grids.iter_mut(),
2163 self.slot_absolute_re_grids.iter_mut(),
2164 self.slot_absolute_im_grids.iter_mut(),
2165 slot_results.iter(),
2166 )
2167 .enumerate()
2168 {
2169 let result = &results[sample_index];
2170 if result.evaluation_metadata.is_nan {
2171 self.failed_samples.push((
2172 slot_index,
2173 sample.clone(),
2174 result.evaluation_metadata.clone(),
2175 ));
2176 }
2177 let jacobian = result.parameterization_jacobian.unwrap_or(F(1.0));
2178 let effective_integrand_result =
2179 result.integrand_result * Complex::new_re(jacobian);
2180 let effective_absolute_result = result
2181 .absolute_integrand_result
2182 .map(|value| value * jacobian.abs());
2183
2184 core_accumulator.add_sample(
2185 effective_integrand_result,
2186 effective_absolute_result,
2187 sample.get_weight(),
2188 Some(sample),
2189 );
2190 re_grid
2191 .add_training_sample(sample, effective_integrand_result.re)
2192 .map_err(Report::msg)?;
2193 im_grid
2194 .add_training_sample(sample, effective_integrand_result.im)
2195 .map_err(Report::msg)?;
2196 add_shallow_monitor_sample(
2197 absolute_re_grid.as_mut(),
2198 monitored_discrete_path.as_deref(),
2199 sample,
2200 effective_absolute_result
2201 .map_or(effective_integrand_result.re.abs(), |value| value.re),
2202 )?;
2203 add_shallow_monitor_sample(
2204 absolute_im_grid.as_mut(),
2205 monitored_discrete_path.as_deref(),
2206 sample,
2207 effective_absolute_result
2208 .map_or(effective_integrand_result.im.abs(), |value| value.im),
2209 )?;
2210
2211 if slot_index == 0 {
2212 let training_eval = match slot_settings[0].integrator.integrated_phase {
2213 IntegratedPhase::Real => effective_integrand_result.re,
2214 IntegratedPhase::Imag => effective_integrand_result.im,
2215 IntegratedPhase::Both => unimplemented!(),
2216 };
2217
2218 self.sampling_states[0]
2219 .sampling_grid
2220 .add_training_sample(sample, training_eval)
2221 .map_err(Report::msg)?;
2222 }
2223 }
2224 }
2225 }
2226 SamplingCorrelationMode::Uncorrelated => {
2227 for slot_index in 0..self.slot_integrands.len() {
2228 let mut samples = Vec::with_capacity(n_points);
2229 for _ in 0..n_points {
2230 if is_interrupted() {
2231 break;
2232 }
2233
2234 let mut sample = Sample::new();
2235 let sampling_state = &mut self.sampling_states[slot_index];
2236 sampling_state
2237 .sampling_grid
2238 .sample(&mut sampling_state.rng, &mut sample);
2239 samples.push(sample);
2240 }
2241
2242 if slot_index == 0 {
2243 processed_points = samples.len();
2244 } else if samples.len() != processed_points {
2245 return Err(Report::msg(
2246 "uncorrelated slot batch sizes diverged unexpectedly",
2247 ));
2248 }
2249
2250 if samples.is_empty() {
2251 continue;
2252 }
2253
2254 let evaluation_start = Instant::now();
2255 let raw_batch = self.slot_integrands[slot_index]
2256 .evaluate_samples_raw_with_estimate(
2257 &samples,
2258 slot_targets[slot_index],
2259 iter,
2260 false,
2261 true,
2262 current_max_evals[slot_index],
2263 current_integral_estimates[slot_index],
2264 )?;
2265 if raw_batch.samples.len() < samples.len() {
2266 return Ok(0);
2267 }
2268 batch_evaluation_time += evaluation_start.elapsed();
2269 total_sample_evaluations += raw_batch.samples.len();
2270 batch_stats = batch_stats.merged(&raw_batch.statistics);
2271 self.slot_stats[slot_index] =
2272 self.slot_stats[slot_index].merged(&raw_batch.statistics);
2273
2274 for (sample, result) in samples.iter().zip(raw_batch.samples.iter()) {
2275 if result.evaluation_metadata.is_nan {
2276 self.failed_samples.push((
2277 slot_index,
2278 sample.clone(),
2279 result.evaluation_metadata.clone(),
2280 ));
2281 }
2282 let jacobian = result.parameterization_jacobian.unwrap_or(F(1.0));
2283 let effective_integrand_result =
2284 result.integrand_result * Complex::new_re(jacobian);
2285 let effective_absolute_result = result
2286 .absolute_integrand_result
2287 .map(|value| value * jacobian.abs());
2288
2289 self.integrals[slot_index].add_sample(
2290 effective_integrand_result,
2291 effective_absolute_result,
2292 sample.get_weight(),
2293 Some(sample),
2294 );
2295 self.slot_re_grids[slot_index]
2296 .add_training_sample(sample, effective_integrand_result.re)
2297 .map_err(Report::msg)?;
2298 self.slot_im_grids[slot_index]
2299 .add_training_sample(sample, effective_integrand_result.im)
2300 .map_err(Report::msg)?;
2301 add_shallow_monitor_sample(
2302 self.slot_absolute_re_grids[slot_index].as_mut(),
2303 monitored_discrete_path.as_deref(),
2304 sample,
2305 effective_absolute_result
2306 .map_or(effective_integrand_result.re.abs(), |value| value.re),
2307 )?;
2308 add_shallow_monitor_sample(
2309 self.slot_absolute_im_grids[slot_index].as_mut(),
2310 monitored_discrete_path.as_deref(),
2311 sample,
2312 effective_absolute_result
2313 .map_or(effective_integrand_result.im.abs(), |value| value.im),
2314 )?;
2315
2316 let training_eval =
2317 match slot_settings[slot_index].integrator.integrated_phase {
2318 IntegratedPhase::Real => effective_integrand_result.re,
2319 IntegratedPhase::Imag => effective_integrand_result.im,
2320 IntegratedPhase::Both => unimplemented!(),
2321 };
2322 self.sampling_states[slot_index]
2323 .sampling_grid
2324 .add_training_sample(sample, training_eval)
2325 .map_err(Report::msg)?;
2326 }
2327 }
2328
2329 if processed_points == 0 {
2330 return Ok(0);
2331 }
2332 }
2333 }
2334
2335 self.remaining_points -= processed_points;
2336 self.completed_points += processed_points;
2337 self.stats = self.stats.merged(&batch_stats);
2338 self.stats.add_integrator_overhead(
2339 chunk_start.elapsed().saturating_sub(batch_evaluation_time),
2340 total_sample_evaluations,
2341 );
2342
2343 Ok(processed_points)
2344 }
2345
2346 fn write_failed_samples(
2347 &mut self,
2348 workspace: Option<&Path>,
2349 slots: &[SlotMeta],
2350 iteration: usize,
2351 ) -> Result<()> {
2352 if self.failed_samples.is_empty() {
2353 return Ok(());
2354 }
2355 if let Some(workspace) = workspace {
2356 let directory = workspace.join("numerical_stability");
2357 fs::create_dir_all(&directory)?;
2358 let mut file = fs::OpenOptions::new()
2359 .create(true)
2360 .append(true)
2361 .open(directory.join("failed_samples.jsonl"))?;
2362 for (slot_index, sample, metadata) in &self.failed_samples {
2363 serde_json::to_writer(
2364 &mut file,
2365 &serde_json::json!({
2366 "schema_version": 1,
2367 "iteration": iteration,
2368 "slot": slots[*slot_index],
2369 "sample": sample,
2370 "evaluation_metadata": metadata,
2371 }),
2372 )?;
2373 file.write_all(b"\n")?;
2374 }
2375 file.flush()?;
2376 }
2377 self.failed_samples.clear();
2378 Ok(())
2379 }
2380}
2381
2382fn initial_batch_size(batching: IterationBatchingSettings) -> usize {
2383 batching.batch_size.unwrap_or(100).max(1)
2384}
2385
2386fn next_batch_size(
2387 batching: IterationBatchingSettings,
2388 current_batch_size: usize,
2389 round_elapsed: Duration,
2390) -> usize {
2391 if let Some(batch_size) = batching.batch_size {
2392 return batch_size.max(1);
2393 }
2394
2395 let round_seconds = round_elapsed.as_secs_f64();
2396 if round_seconds <= 0.0 {
2397 return current_batch_size.max(1);
2398 }
2399
2400 let estimated = ((current_batch_size as f64) * batching.batch_timing_seconds / round_seconds)
2401 .round() as usize;
2402 estimated.max(1)
2403}
2404
2405fn total_completed_points(core_states: &[CoreIterationState]) -> usize {
2406 core_states.iter().map(|state| state.completed_points).sum()
2407}
2408
2409fn total_remaining_points(core_states: &[CoreIterationState]) -> usize {
2410 core_states.iter().map(|state| state.remaining_points).sum()
2411}
2412
2413fn slot_seed(global_seed: u64, slot_index: usize) -> u64 {
2414 let slot = (slot_index as u64).wrapping_add(1);
2415 let mut mixed = global_seed.wrapping_add(0x9e37_79b9_7f4a_7c15u64.wrapping_mul(slot));
2416 mixed ^= mixed >> 30;
2417 mixed = mixed.wrapping_mul(0xbf58_476d_1ce4_e5b9);
2418 mixed ^= mixed >> 27;
2419 mixed = mixed.wrapping_mul(0x94d0_49bb_1331_11eb);
2420 mixed ^ (mixed >> 31)
2421}
2422
2423fn apply_iteration_core_states(
2424 integration_state: &mut IntegrationState,
2425 slots: &[IntegrationSlot],
2426 cores: usize,
2427 cur_points: usize,
2428 elapsed_seconds: f64,
2429 core_states: &[CoreIterationState],
2430) {
2431 let n_slots = integration_state.slot_metas.len();
2432
2433 for core_state in core_states {
2434 integration_state.stats = integration_state.stats.merged(&core_state.stats);
2435 for (integral, core_integral) in integration_state
2436 .all_integrals
2437 .iter_mut()
2438 .zip(core_state.integrals.iter())
2439 {
2440 integral.merge(core_integral);
2441 }
2442 for (slot_stats, core_slot_stats) in integration_state
2443 .slot_stats
2444 .iter_mut()
2445 .zip(core_state.slot_stats.iter())
2446 {
2447 *slot_stats = slot_stats.merged(core_slot_stats);
2448 }
2449 }
2450
2451 let mut merged_sampling_grids = integration_state
2452 .sampling_states
2453 .iter()
2454 .map(|sampling_state| sampling_state.grid.clone_without_samples())
2455 .collect_vec();
2456 let mut merged_re_grids = (0..n_slots)
2457 .map(|slot_index| {
2458 integration_state
2459 .sampling_state_for_slot(slot_index)
2460 .grid
2461 .clone_without_samples()
2462 })
2463 .collect_vec();
2464 let mut merged_im_grids = (0..n_slots)
2465 .map(|slot_index| {
2466 integration_state
2467 .sampling_state_for_slot(slot_index)
2468 .grid
2469 .clone_without_samples()
2470 })
2471 .collect_vec();
2472 let mut merged_absolute_re_grids = (0..n_slots)
2473 .map(|slot_index| {
2474 integration_state
2475 .monitored_discrete_path
2476 .as_deref()
2477 .map(|path| {
2478 shallow_discrete_monitor_grid(
2479 &integration_state.sampling_state_for_slot(slot_index).grid,
2480 path,
2481 )
2482 .expect("validated monitored discrete path must match every sampling grid")
2483 })
2484 })
2485 .collect_vec();
2486 let mut merged_absolute_im_grids = merged_absolute_re_grids.clone();
2487
2488 for core_state in core_states {
2489 for (sampling_state, merged_grid) in core_state
2490 .sampling_states
2491 .iter()
2492 .zip(merged_sampling_grids.iter_mut())
2493 {
2494 merged_grid
2495 .merge(&sampling_state.sampling_grid)
2496 .expect("could not merge grids");
2497 }
2498 for slot_index in 0..n_slots {
2499 merged_re_grids[slot_index]
2500 .merge(&core_state.slot_re_grids[slot_index])
2501 .expect("could not merge real accumulation grids");
2502 merged_im_grids[slot_index]
2503 .merge(&core_state.slot_im_grids[slot_index])
2504 .expect("could not merge imaginary accumulation grids");
2505 match (
2506 merged_absolute_re_grids[slot_index].as_mut(),
2507 core_state.slot_absolute_re_grids[slot_index].as_ref(),
2508 ) {
2509 (Some(merged), Some(core)) => merged
2510 .merge(core)
2511 .expect("could not merge absolute real monitor grids"),
2512 (None, None) => {}
2513 _ => panic!("absolute real monitor grid presence is inconsistent"),
2514 }
2515 match (
2516 merged_absolute_im_grids[slot_index].as_mut(),
2517 core_state.slot_absolute_im_grids[slot_index].as_ref(),
2518 ) {
2519 (Some(merged), Some(core)) => merged
2520 .merge(core)
2521 .expect("could not merge absolute imaginary monitor grids"),
2522 (None, None) => {}
2523 _ => panic!("absolute imaginary monitor grid presence is inconsistent"),
2524 }
2525 }
2526 }
2527
2528 for (summary, grid) in integration_state
2529 .slot_re_summaries
2530 .iter_mut()
2531 .zip(merged_re_grids.iter())
2532 {
2533 if let Some(summary) = summary.as_mut() {
2534 summary.merge_iteration_grid(grid);
2535 summary.update_iter();
2536 }
2537 }
2538 for (summary, grid) in integration_state
2539 .slot_im_summaries
2540 .iter_mut()
2541 .zip(merged_im_grids.iter())
2542 {
2543 if let Some(summary) = summary.as_mut() {
2544 summary.merge_iteration_grid(grid);
2545 summary.update_iter();
2546 }
2547 }
2548 for (summary, grid) in integration_state
2549 .slot_absolute_re_summaries
2550 .iter_mut()
2551 .zip(&merged_absolute_re_grids)
2552 {
2553 match (summary.as_mut(), grid.as_ref()) {
2554 (Some(summary), Some(grid)) => {
2555 summary.merge_shallow_iteration_grid(grid);
2556 summary.update_iter();
2557 }
2558 (None, None) => {}
2559 _ => panic!("absolute real summary and monitor grid presence is inconsistent"),
2560 }
2561 }
2562 for (summary, grid) in integration_state
2563 .slot_absolute_im_summaries
2564 .iter_mut()
2565 .zip(&merged_absolute_im_grids)
2566 {
2567 match (summary.as_mut(), grid.as_ref()) {
2568 (Some(summary), Some(grid)) => {
2569 summary.merge_shallow_iteration_grid(grid);
2570 summary.update_iter();
2571 }
2572 (None, None) => {}
2573 _ => panic!("absolute imaginary summary and monitor grid presence is inconsistent"),
2574 }
2575 }
2576
2577 for (slot_index, merged_grid) in merged_sampling_grids.into_iter().enumerate() {
2578 let actual_slot_index = match integration_state.sampling_correlation_mode {
2579 SamplingCorrelationMode::Correlated => 0,
2580 SamplingCorrelationMode::Uncorrelated => slot_index,
2581 };
2582 let discrete_axis_labels = integration_state
2583 .sampling_state_for_slot(actual_slot_index)
2584 .discrete_axis_labels
2585 .clone();
2586 let sampling_state = integration_state.sampling_state_for_slot_mut(actual_slot_index);
2587 *sampling_state = SamplingSlotState {
2588 grid: merged_grid,
2589 discrete_axis_labels,
2590 };
2591 sampling_state.grid.update(
2592 F(slots[actual_slot_index]
2593 .settings
2594 .integrator
2595 .discrete_dim_learning_rate),
2596 F(slots[actual_slot_index]
2597 .settings
2598 .integrator
2599 .continuous_dim_learning_rate),
2600 );
2601 }
2602
2603 integration_state.update_iter(false);
2604 integration_state.iter += 1;
2605 integration_state.elapsed_seconds = elapsed_seconds;
2606 integration_state.n_cores = cores;
2607 integration_state.num_points += cur_points;
2608}
2609
2610fn build_preview_integration_state(
2611 integration_state: &IntegrationState,
2612 slots: &[IntegrationSlot],
2613 cores: usize,
2614 completed_points: usize,
2615 elapsed_seconds: f64,
2616 core_states: &[CoreIterationState],
2617) -> IntegrationState {
2618 let mut preview = integration_state.clone();
2619 apply_iteration_core_states(
2620 &mut preview,
2621 slots,
2622 cores,
2623 completed_points,
2624 elapsed_seconds,
2625 core_states,
2626 );
2627 preview
2628}
2629
2630fn write_interrupted_numerical_stability_outputs(
2631 workspace: Option<&Path>,
2632 integration_state: &IntegrationState,
2633 slots: &[IntegrationSlot],
2634 cores: usize,
2635 completed_points: usize,
2636 elapsed_seconds: f64,
2637 core_states: &[CoreIterationState],
2638) -> Result<Vec<PathBuf>> {
2639 if workspace.is_none() || completed_points == 0 {
2640 return Ok(Vec::new());
2641 }
2642
2643 let preview_state = build_preview_integration_state(
2644 integration_state,
2645 slots,
2646 cores,
2647 completed_points,
2648 elapsed_seconds,
2649 core_states,
2650 );
2651 write_numerical_stability_outputs(
2652 workspace,
2653 &preview_state,
2654 WorkspaceSnapshotControl {
2655 write_iteration_archives: false,
2656 },
2657 )
2658}
2659
2660pub fn havana_integrate<S>(
2662 request: HavanaIntegrateRequest,
2663 mut status_emitter: S,
2664) -> Result<IntegrationResult>
2665where
2666 S: FnMut(StatusUpdate) -> Result<()>,
2667{
2668 let HavanaIntegrateRequest {
2669 mut slots,
2670 sampling_correlation_mode,
2671 n_cores,
2672 state,
2673 workspace,
2674 output_control,
2675 batching,
2676 view_options,
2677 } = request;
2678
2679 if slots.is_empty() {
2680 return Err(Report::msg(
2681 "At least one integrand must be selected for integration",
2682 ));
2683 }
2684
2685 let slot_metas = slots.iter().map(|slot| slot.meta.clone()).collect_vec();
2686 let targets = slots.iter().map(|slot| slot.target).collect_vec();
2687 let sampling_states = match sampling_correlation_mode {
2688 SamplingCorrelationMode::Correlated => {
2689 vec![SamplingSlotState::from_integrand(&slots[0].integrand)]
2690 }
2691 SamplingCorrelationMode::Uncorrelated => slots
2692 .iter()
2693 .map(|slot| SamplingSlotState::from_integrand(&slot.integrand))
2694 .collect_vec(),
2695 };
2696 let monitored_discrete_setup =
2697 resolve_monitored_discrete_setup(sampling_correlation_mode, &slots, &sampling_states);
2698 let slot_first_non_trivial_discrete_breakdown_metadata =
2699 if let Some(path) = monitored_discrete_setup.path.as_deref() {
2700 slots
2701 .iter()
2702 .enumerate()
2703 .map(|(slot_index, slot)| {
2704 build_persisted_discrete_breakdown_metadata(
2705 &slot.integrand,
2706 path,
2707 &sampling_states[sampling_correlation_mode.state_index(slot_index)]
2708 .discrete_axis_labels,
2709 )
2710 })
2711 .collect_vec()
2712 } else {
2713 vec![None; slot_metas.len()]
2714 };
2715 if let Some(label_warning) = monitored_discrete_setup.warning.as_ref() {
2716 warn!("{label_warning}");
2717 }
2718
2719 let mut integration_state = if let Some(integration_state) = state {
2720 integration_state
2721 } else {
2722 IntegrationState::new_from_settings(
2723 sampling_correlation_mode,
2724 sampling_states.clone(),
2725 slot_metas.clone(),
2726 monitored_discrete_setup.path.clone(),
2727 monitored_discrete_setup.axis_label.clone(),
2728 monitored_discrete_setup.bin_descriptions.clone(),
2729 slot_first_non_trivial_discrete_breakdown_metadata,
2730 )
2731 };
2732 integration_state.monitored_discrete_path = monitored_discrete_setup.path;
2733 integration_state.first_non_trivial_discrete_label = monitored_discrete_setup.axis_label;
2734 integration_state.first_non_trivial_discrete_bin_descriptions =
2735 monitored_discrete_setup.bin_descriptions;
2736 if integration_state.slot_metas != slot_metas {
2737 return Err(Report::msg(
2738 "Saved integration state slots do not match the currently selected integrands",
2739 ));
2740 }
2741 if integration_state.sampling_correlation_mode != sampling_correlation_mode {
2742 return Err(Report::msg(
2743 "Saved integration state sampling mode does not match the current integration mode",
2744 ));
2745 }
2746 if integration_state.sampling_states.len()
2747 != sampling_correlation_mode.state_count(slot_metas.len())
2748 {
2749 return Err(Report::msg(
2750 "Saved integration state sampling metadata is inconsistent with the selected slots",
2751 ));
2752 }
2753 if integration_state
2754 .slot_first_non_trivial_discrete_breakdown_metadata
2755 .len()
2756 != slot_metas.len()
2757 {
2758 return Err(Report::msg(
2759 "Saved integration state discrete breakdown metadata is inconsistent with the selected slots",
2760 ));
2761 }
2762 if integration_state.slot_absolute_re_summaries.len() != slot_metas.len()
2763 || integration_state.slot_absolute_im_summaries.len() != slot_metas.len()
2764 {
2765 return Err(Report::msg(
2766 "Saved integration state absolute summaries are inconsistent with the selected slots",
2767 ));
2768 }
2769 for slot_index in 0..slot_metas.len() {
2770 let expected_bin_count = integration_state
2771 .monitored_discrete_path
2772 .as_deref()
2773 .map(|path| {
2774 discrete_grid_at_path(
2775 &integration_state.sampling_state_for_slot(slot_index).grid,
2776 path,
2777 )
2778 .map(|grid| grid.bins.len())
2779 .ok_or_else(|| {
2780 Report::msg(format!(
2781 "Saved integration state monitor path does not match slot {} sampling grid",
2782 integration_state.slot_metas[slot_index].key()
2783 ))
2784 })
2785 })
2786 .transpose()?;
2787 for summary in [
2788 &integration_state.slot_absolute_re_summaries[slot_index],
2789 &integration_state.slot_absolute_im_summaries[slot_index],
2790 ] {
2791 let valid = match (summary, expected_bin_count) {
2792 (None, None) => true,
2793 (Some(summary), Some(bin_count)) => {
2794 summary.bins.len() == bin_count
2795 && summary.bins.iter().all(|bin| bin.sub_summary.is_none())
2796 }
2797 _ => false,
2798 };
2799 if !valid {
2800 return Err(Report::msg(format!(
2801 "Saved integration state absolute summary does not match slot {} monitor grid",
2802 integration_state.slot_metas[slot_index].key()
2803 )));
2804 }
2805 }
2806 }
2807
2808 let primary = &slots[0];
2809 let sampling_str = primary.settings.sampling.describe_settings();
2810 let dimension = primary.integrand.get_n_dim();
2811 let discrete_depth = primary.settings.sampling.discrete_depth();
2812 let is_tropical_sampling = primary
2813 .settings
2814 .sampling
2815 .get_parameterization_settings()
2816 .is_none();
2817 let use_ltd = primary.settings.general.use_ltd;
2818 let integration_seed = primary.settings.integrator.seed;
2819 let integrated_phase = primary.settings.integrator.integrated_phase;
2820 let target_relative_accuracy = primary.settings.integrator.target_relative_accuracy;
2821 let target_absolute_accuracy = primary.settings.integrator.target_absolute_accuracy;
2822 let n_start = primary.settings.integrator.n_start;
2823 let n_increase = primary.settings.integrator.n_increase;
2824 let n_max = primary.settings.integrator.n_max;
2825
2826 let cont_dim_str = if is_tropical_sampling {
2827 format!("a median continious dimension of {}", dimension)
2828 } else {
2829 format!("{} continuous dimensions", dimension)
2830 };
2831
2832 let graph_string = if discrete_depth > 0 {
2833 let num_graphs = match &integration_state.sampling_state_for_slot(0).grid {
2834 Grid::Discrete(g) => g.bins.len(),
2835 _ => unreachable!(),
2836 };
2837 format!(
2838 "{discrete_depth} nested discrete grids with {} {} and ",
2839 num_graphs,
2840 if num_graphs > 1 { "graphs" } else { "graph" }
2841 )
2842 } else {
2843 String::new()
2844 };
2845
2846 let correlation_str = match sampling_correlation_mode {
2847 SamplingCorrelationMode::Correlated => "correlated",
2848 SamplingCorrelationMode::Uncorrelated => "uncorrelated",
2849 };
2850 let grid_str = format!("{graph_string}{cont_dim_str} using {sampling_str} ({correlation_str})");
2851
2852 let cores = n_cores.max(1);
2853 let n_slots = integration_state.slot_metas.len();
2854 let elapsed_seconds_offset = integration_state.elapsed_seconds;
2855
2856 let t_start = Instant::now();
2857
2858 info!(
2859 "Integrating using {} ltd with {} {} over {} ...",
2860 if use_ltd { "naive" } else { "cff" },
2861 cores,
2862 if cores > 1 { "cores" } else { "core" },
2863 grid_str
2864 );
2865 info!("");
2866
2867 let pool = ThreadPoolBuilder::new().num_threads(cores).build().unwrap();
2868
2869 let mut n_samples_evaluated = 0;
2870 let mut emitted_latest_observable_paths = vec![Vec::new(); n_slots];
2871 let mut emitted_latest_numerical_stability_paths = Vec::new();
2872 clear_iteration_abort_request();
2873 'integrateLoop: while integration_state.num_points < n_max {
2874 let cur_points = {
2876 let cur_points_not_final_iter = n_start + n_increase * integration_state.iter;
2877 if cur_points_not_final_iter + integration_state.num_points > n_max {
2878 n_max - integration_state.num_points
2879 } else {
2880 cur_points_not_final_iter
2881 }
2882 };
2883
2884 let target_points_per_core = (cur_points - 1) / cores + 1;
2886 let n_points_per_core = (0..cores)
2887 .map(|core_id| {
2888 if core_id + 1 == cores {
2889 cur_points - target_points_per_core * (cores - 1)
2890 } else {
2891 target_points_per_core
2892 }
2893 })
2894 .collect_vec();
2895
2896 let iteration_start = Instant::now();
2897 let mut current_batch_size = initial_batch_size(batching);
2898 let mut last_live_status = None;
2899 if batching.emit_initial_status_update && !is_interrupted() {
2900 status_emitter(build_status_update(StatusUpdateBuildRequest {
2901 kind: IntegrationStatusKind::Live,
2902 integration_state: &integration_state,
2903 cores,
2904 elapsed_time: t_start.elapsed(),
2905 iteration_elapsed_time: iteration_start.elapsed(),
2906 cur_points: 0,
2907 total_points_display: integration_state.num_points,
2908 n_samples_evaluated,
2909 targets: &targets,
2910 render_options: &view_options,
2911 live_progress: Some(status_update::LiveIterationProgress {
2912 completed_points: 0,
2913 target_points: cur_points,
2914 }),
2915 }))?;
2916 }
2917
2918 let current_max_evals = integration_state
2919 .all_integrals
2920 .iter()
2921 .map(ComplexAccumulator::get_worst_case)
2922 .collect_vec();
2923 let current_integral_estimates = integration_state
2924 .all_integrals
2925 .iter()
2926 .zip(&slots)
2927 .map(|(integral, slot)| {
2928 let accumulator = match slot.settings.integrator.integrated_phase {
2929 IntegratedPhase::Real => &integral.absolute_re,
2930 IntegratedPhase::Imag => &integral.absolute_im,
2931 IntegratedPhase::Both => return None,
2932 };
2933 (accumulator.processed_samples > 0)
2934 .then_some((accumulator.avg.0, accumulator.err.0))
2935 })
2936 .collect_vec();
2937
2938 let mut worker_states = n_points_per_core
2939 .iter()
2940 .enumerate()
2941 .map(|(core_id, &n_points)| {
2942 CoreIterationState::new(
2943 slots.iter().map(|slot| slot.integrand.clone()).collect(),
2944 integration_state.sampling_correlation_mode,
2945 &integration_state
2946 .sampling_states
2947 .iter()
2948 .map(|sampling_state| sampling_state.grid.clone())
2949 .collect_vec(),
2950 integration_state.monitored_discrete_path.as_deref(),
2951 integration_seed + integration_state.iter as u64,
2952 target_points_per_core * core_id,
2953 n_points,
2954 )
2955 })
2956 .collect_vec();
2957 while total_remaining_points(&worker_states) > 0 {
2958 let round_started_at = Instant::now();
2959 let processed_per_core: Vec<Result<usize>> = {
2960 let slot_settings = slots.iter().map(|slot| &slot.settings).collect_vec();
2961 let slot_targets = slots
2962 .iter()
2963 .map(|slot| {
2964 slot.reference_gaussian
2965 .as_ref()
2966 .map(EvaluationTarget::Reference)
2967 .unwrap_or(EvaluationTarget::Physical(&slot.model))
2968 })
2969 .collect_vec();
2970 pool.install(|| {
2971 worker_states
2972 .par_iter_mut()
2973 .map(|worker_state| {
2974 worker_state.evaluate_chunk(
2975 &slot_settings,
2976 &slot_targets,
2977 integration_state.iter,
2978 ¤t_max_evals,
2979 ¤t_integral_estimates,
2980 current_batch_size,
2981 )
2982 })
2983 .collect()
2984 })
2985 };
2986 for worker_state in &mut worker_states {
2990 worker_state.write_failed_samples(
2991 workspace.as_deref(),
2992 &integration_state.slot_metas,
2993 integration_state.iter + 1,
2994 )?;
2995 }
2996 let processed_this_round = processed_per_core
2997 .into_iter()
2998 .collect::<Result<Vec<_>>>()?
2999 .into_iter()
3000 .sum::<usize>();
3001
3002 if processed_this_round == 0 {
3003 break;
3004 }
3005
3006 let completed_points = total_completed_points(&worker_states);
3007 if batching.emit_live_status_updates
3008 && completed_points < cur_points
3009 && !is_interrupted()
3010 && !is_iteration_abort_requested()
3011 {
3012 let now = Instant::now();
3013 let should_emit_live_status = last_live_status.is_none_or(|previous| {
3014 now.duration_since(previous).as_secs_f64()
3015 >= batching.min_time_between_status_updates_seconds
3016 });
3017 if should_emit_live_status {
3018 let preview_state = build_preview_integration_state(
3019 &integration_state,
3020 &slots,
3021 cores,
3022 completed_points,
3023 elapsed_seconds_offset + t_start.elapsed().as_secs_f64(),
3024 &worker_states,
3025 );
3026 status_emitter(build_status_update(StatusUpdateBuildRequest {
3027 kind: IntegrationStatusKind::Live,
3028 integration_state: &preview_state,
3029 cores,
3030 elapsed_time: t_start.elapsed(),
3031 iteration_elapsed_time: iteration_start.elapsed(),
3032 cur_points: completed_points,
3033 total_points_display: integration_state.num_points + completed_points,
3034 n_samples_evaluated: n_samples_evaluated + completed_points,
3035 targets: &targets,
3036 render_options: &view_options,
3037 live_progress: Some(status_update::LiveIterationProgress {
3038 completed_points,
3039 target_points: cur_points,
3040 }),
3041 }))?;
3042 last_live_status = Some(now);
3043 }
3044 }
3045
3046 if is_interrupted() {
3047 warn!("{}", "Integration interrupted by user".yellow());
3048 emitted_latest_numerical_stability_paths =
3049 write_interrupted_numerical_stability_outputs(
3050 workspace.as_deref(),
3051 &integration_state,
3052 &slots,
3053 cores,
3054 total_completed_points(&worker_states),
3055 elapsed_seconds_offset + t_start.elapsed().as_secs_f64(),
3056 &worker_states,
3057 )?;
3058 break 'integrateLoop;
3059 }
3060
3061 if is_iteration_abort_requested() {
3062 break;
3063 }
3064
3065 if total_remaining_points(&worker_states) > 0 {
3066 current_batch_size =
3067 next_batch_size(batching, current_batch_size, round_started_at.elapsed());
3068 }
3069 }
3070
3071 let abort_current_iteration = is_iteration_abort_requested();
3072 clear_iteration_abort_request();
3073
3074 if is_interrupted() {
3075 warn!("{}", "Integration interrupted by user".yellow());
3076 emitted_latest_numerical_stability_paths =
3077 write_interrupted_numerical_stability_outputs(
3078 workspace.as_deref(),
3079 &integration_state,
3080 &slots,
3081 cores,
3082 total_completed_points(&worker_states),
3083 elapsed_seconds_offset + t_start.elapsed().as_secs_f64(),
3084 &worker_states,
3085 )?;
3086 break 'integrateLoop;
3087 }
3088
3089 let completed_points = total_completed_points(&worker_states);
3090 if abort_current_iteration && completed_points < cur_points {
3091 if completed_points == 0 {
3092 warn!(
3093 "{}",
3094 "Current iteration abort requested before any samples completed; restarting the iteration."
3095 .yellow()
3096 );
3097 continue;
3098 }
3099 warn!(
3100 "{}",
3101 format!(
3102 "Current iteration aborted by user after {} completed samples.",
3103 completed_points
3104 )
3105 .yellow()
3106 );
3107 continue;
3108 }
3109
3110 n_samples_evaluated += completed_points;
3111 apply_iteration_core_states(
3112 &mut integration_state,
3113 &slots,
3114 cores,
3115 completed_points,
3116 elapsed_seconds_offset + t_start.elapsed().as_secs_f64(),
3117 &worker_states,
3118 );
3119
3120 let (owner_core_slice, other_cores) = worker_states.split_at_mut(1);
3122 let owner_core = &mut owner_core_slice[0].slot_integrands;
3123 for other_core in other_cores {
3124 for (owner_slot, other_slot) in owner_core
3125 .iter_mut()
3126 .zip(other_core.slot_integrands.iter_mut())
3127 {
3128 owner_slot.merge_runtime_results(other_slot)?;
3129 }
3130 }
3131 for (slot, integrand) in slots.iter_mut().zip(owner_core.iter()) {
3132 slot.integrand = integrand.clone();
3133 }
3134
3135 for slot in &mut slots {
3138 slot.integrand
3139 .update_runtime_results(integration_state.iter);
3140 }
3141
3142 if let Some(ref workspace_path) = workspace {
3143 for (slot_index, slot) in slots.iter().enumerate() {
3144 write_observable_resume_state(
3145 &slot.integrand,
3146 Some(workspace_path.as_path()),
3147 &integration_state.slot_metas[slot_index],
3148 integration_state.iter,
3149 output_control,
3150 )?;
3151 }
3152 write_integration_state_to_workspace(workspace_path, &integration_state)?;
3153 write_integration_result_snapshots(
3154 workspace_path,
3155 &integration_state,
3156 &targets,
3157 output_control,
3158 )?;
3159 emitted_latest_numerical_stability_paths = write_numerical_stability_outputs(
3160 Some(workspace_path.as_path()),
3161 &integration_state,
3162 output_control,
3163 )?;
3164 }
3165
3166 for (slot_index, slot) in slots.iter().enumerate() {
3167 let workspace_path = workspace
3168 .as_deref()
3169 .map(|root| slot_workspace_path(root, &integration_state.slot_metas[slot_index]));
3170 emitted_latest_observable_paths[slot_index] =
3171 write_latest_observables_output(&slot.integrand, workspace_path.as_deref())?;
3172 write_observable_snapshot_archive(
3173 &slot.integrand,
3174 workspace_path.as_deref(),
3175 integration_state.iter,
3176 output_control,
3177 )?;
3178 }
3179
3180 status_emitter(build_status_update(StatusUpdateBuildRequest {
3181 kind: IntegrationStatusKind::Iteration,
3182 integration_state: &integration_state,
3183 cores,
3184 elapsed_time: t_start.elapsed(),
3185 iteration_elapsed_time: iteration_start.elapsed(),
3186 cur_points: completed_points,
3187 total_points_display: integration_state.num_points,
3188 n_samples_evaluated,
3189 targets: &targets,
3190 render_options: &view_options,
3191 live_progress: None,
3192 }))?;
3193
3194 let target_accuracy_status = evaluate_target_accuracy(
3195 &integration_state,
3196 integration_state.num_points,
3197 t_start.elapsed(),
3198 &targets,
3199 match integrated_phase {
3200 IntegratedPhase::Real | IntegratedPhase::Both => {
3201 IntegrationStatusPhaseDisplay::Real
3202 }
3203 IntegratedPhase::Imag => IntegrationStatusPhaseDisplay::Imag,
3204 },
3205 target_relative_accuracy,
3206 target_absolute_accuracy,
3207 );
3208 if target_accuracy_status.is_reached() {
3209 let reached_target = match (
3210 target_accuracy_status.relative_reached,
3211 target_accuracy_status.absolute_reached,
3212 ) {
3213 (true, true) => "relative and absolute",
3214 (true, false) => "relative",
3215 (false, true) => "absolute",
3216 (false, false) => unreachable!(),
3217 };
3218 info!(
3219 "Stopping integration after reaching the configured {reached_target} accuracy target."
3220 );
3221 break;
3222 }
3223 }
3224 set_interrupted(false);
3226 clear_iteration_abort_request();
3227
3228 if integration_state.num_points > 0 {
3229 let final_view_options = view_options.for_final();
3230 status_emitter(build_status_update(StatusUpdateBuildRequest {
3231 kind: IntegrationStatusKind::Final,
3232 integration_state: &integration_state,
3233 cores,
3234 elapsed_time: t_start.elapsed(),
3235 iteration_elapsed_time: Duration::ZERO,
3236 cur_points: 0,
3237 total_points_display: integration_state.num_points,
3238 n_samples_evaluated,
3239 targets: &targets,
3240 render_options: &final_view_options,
3241 live_progress: None,
3242 }))?;
3243 } else {
3244 info!("");
3245 warn!(
3246 "{}",
3247 "No final integration results to display since no iteration completed.".yellow()
3248 );
3249 info!("");
3250 }
3251
3252 if !slots.is_empty() {
3253 emit_results_output_summary(
3254 workspace.as_deref(),
3255 &slots,
3256 &emitted_latest_observable_paths,
3257 &emitted_latest_numerical_stability_paths,
3258 );
3259 }
3260
3261 Ok(build_integration_result(&integration_state, &targets))
3262}
3263
3264pub(crate) fn batch_integrate(
3267 integrand: &mut Integrand,
3268 model: &Model,
3269 input: BatchIntegrateInput,
3270) -> Result<BatchResult> {
3271 let samples = match input.samples {
3272 SampleInput::SampleList { samples } => samples,
3273 SampleInput::Grid {
3274 mut grid,
3275 num_points,
3276 seed,
3277 thread_id,
3278 } => {
3279 let mut rng = MonteCarloRng::new(seed, thread_id);
3280
3281 (0..num_points)
3282 .map(|_| {
3283 let mut sample = Sample::new();
3284 grid.sample(&mut rng, &mut sample);
3285 sample
3286 })
3287 .collect_vec()
3288 }
3289 };
3290
3291 let (evaluation_results, metadata_statistics) = evaluate_sample_list(
3292 integrand,
3293 &samples,
3294 model,
3295 input.num_cores,
3296 input.iter,
3297 input.max_eval,
3298 )?;
3299
3300 let integrand_output = generate_integrand_output(
3301 integrand,
3302 &evaluation_results,
3303 &samples,
3304 input.integrand_output_settings,
3305 input.settings.integrator.integrated_phase,
3306 );
3307
3308 let event_output = generate_event_output(
3309 integrand,
3310 evaluation_results,
3311 input.event_output_settings,
3312 input.settings,
3313 );
3314
3315 Ok(BatchResult {
3316 statistics: metadata_statistics,
3317 integrand_data: integrand_output,
3318 event_data: event_output,
3319 })
3320}
3321
3322fn generate_integrand_output(
3324 integrand: &Integrand,
3325 evaluation_results: &[EvaluationResult],
3326 samples: &[Sample<F<f64>>],
3327 integrand_output_settings: IntegralOutputSettings,
3328 integrated_phase: IntegratedPhase,
3329) -> BatchIntegrateOutput {
3330 fn effective_integrand_result(result: &EvaluationResult) -> Complex<F<f64>> {
3331 let jacobian = result.parameterization_jacobian.unwrap_or(F(1.0));
3332 result.integrand_result * Complex::new_re(jacobian)
3333 }
3334
3335 match integrand_output_settings {
3336 IntegralOutputSettings::Default => {
3337 let integrand_values = evaluation_results
3338 .iter()
3339 .map(effective_integrand_result)
3340 .collect();
3341
3342 BatchIntegrateOutput::Default(integrand_values, samples.to_vec())
3343 }
3344 IntegralOutputSettings::Accumulator => {
3345 let mut real_accumulator = StatisticsAccumulator::new();
3346 let mut imag_accumulator = StatisticsAccumulator::new();
3347 let mut grid = integrand.create_grid();
3348
3349 for (result, sample) in evaluation_results.iter().zip(samples.iter()) {
3350 let effective_result = effective_integrand_result(result);
3351 real_accumulator
3352 .add_sample(effective_result.re * sample.get_weight(), Some(sample));
3353 imag_accumulator
3354 .add_sample(effective_result.im * sample.get_weight(), Some(sample));
3355
3356 match integrated_phase {
3357 IntegratedPhase::Real => {
3358 grid.add_training_sample(sample, effective_result.re)
3359 .unwrap();
3360 }
3361 IntegratedPhase::Imag => {
3362 grid.add_training_sample(sample, effective_result.im)
3363 .unwrap();
3364 }
3365 IntegratedPhase::Both => {
3366 unimplemented!()
3367 }
3368 }
3369 }
3370
3371 BatchIntegrateOutput::Accumulator(
3372 Box::new((real_accumulator, imag_accumulator)),
3373 Box::new(grid),
3374 )
3375 }
3376 }
3377}
3378
3379fn generate_event_output(
3381 integrand: &Integrand,
3382 evaluation_results: Vec<EvaluationResult>,
3383 event_output_settings: EventOutputSettings,
3384 _settings: &RuntimeSettings,
3385) -> EventOutput {
3386 match event_output_settings {
3387 EventOutputSettings::None => EventOutput::None,
3388
3389 EventOutputSettings::EventList => {
3390 let mut event_groups = EventGroupList::default();
3391 for mut result in evaluation_results {
3392 event_groups.append(&mut result.event_groups);
3393 }
3394 EventOutput::EventList { event_groups }
3395 }
3396 EventOutputSettings::Histogram => integrand
3397 .observable_accumulator_bundle()
3398 .map(|histograms| EventOutput::Histogram { histograms })
3399 .unwrap_or(EventOutput::None),
3400 }
3401}
3402
3403pub fn slot_workspace_path(workspace: &Path, slot_meta: &SlotMeta) -> PathBuf {
3404 workspace.join("integrands").join(slot_meta.key())
3405}
3406
3407pub fn workspace_manifest_path(workspace: &Path) -> PathBuf {
3408 workspace.join("manifest.json")
3409}
3410
3411pub fn workspace_state_dir(workspace: &Path) -> PathBuf {
3412 workspace.join("state")
3413}
3414
3415pub fn workspace_state_path(workspace: &Path) -> PathBuf {
3416 workspace_state_dir(workspace).join("integration_state.bin")
3417}
3418
3419fn workspace_observable_state_dir(workspace: &Path, slot_meta: &SlotMeta) -> PathBuf {
3420 workspace_state_dir(workspace)
3421 .join("observables")
3422 .join(slot_meta.key())
3423}
3424
3425pub fn latest_observable_resume_state_path(workspace: &Path, slot_meta: &SlotMeta) -> PathBuf {
3426 workspace_observable_state_dir(workspace, slot_meta).join("latest.json")
3427}
3428
3429fn archived_observable_resume_state_path(
3430 workspace: &Path,
3431 slot_meta: &SlotMeta,
3432 iter: usize,
3433) -> PathBuf {
3434 workspace_observable_state_dir(workspace, slot_meta).join(format!("iter_{iter:04}.json"))
3435}
3436
3437pub fn workspace_result_snapshot_path(workspace: &Path) -> PathBuf {
3438 workspace.join("integration_result.json")
3439}
3440
3441pub fn workspace_result_archive_path(workspace: &Path, iter: usize) -> PathBuf {
3442 workspace
3443 .join("results")
3444 .join(format!("integration_result_iter_{iter:04}.json"))
3445}
3446
3447fn observable_output_extension(format: ObservableFileFormat) -> Option<&'static str> {
3448 match format {
3449 ObservableFileFormat::None => None,
3450 ObservableFileFormat::Hwu => Some("hwu"),
3451 ObservableFileFormat::Json => Some("json"),
3452 }
3453}
3454
3455fn latest_observable_output_path(workspace: &Path, format: ObservableFileFormat) -> PathBuf {
3456 let extension = observable_output_extension(format)
3457 .expect("user-facing observable outputs must use a real file format");
3458 workspace.join(format!("observables_final.{extension}"))
3459}
3460
3461fn archived_observable_output_path(
3462 workspace: &Path,
3463 format: ObservableFileFormat,
3464 iter: usize,
3465) -> PathBuf {
3466 let extension = observable_output_extension(format)
3467 .expect("user-facing observable outputs must use a real file format");
3468 workspace.join(format!("observables_final_iter_{iter:04}.{extension}"))
3469}
3470
3471fn numerical_stability_output_extension(format: ObservableFileFormat) -> Option<&'static str> {
3472 match format {
3473 ObservableFileFormat::None => None,
3474 ObservableFileFormat::Hwu => Some("hwu"),
3475 ObservableFileFormat::Json => Some("json"),
3476 }
3477}
3478
3479fn numerical_stability_scope_file_stem(scope: &str, iter: Option<usize>) -> String {
3480 match iter {
3481 Some(iter) => format!("{scope}_iter_{iter:04}"),
3482 None => scope.to_string(),
3483 }
3484}
3485
3486fn numerical_stability_output_path(
3487 workspace: &Path,
3488 scope: &str,
3489 format: ObservableFileFormat,
3490 iter: Option<usize>,
3491) -> PathBuf {
3492 let extension = numerical_stability_output_extension(format)
3493 .expect("numerical stability outputs must use a real file format");
3494 workspace.join("numerical_stability").join(format!(
3495 "{}.{extension}",
3496 numerical_stability_scope_file_stem(scope, iter)
3497 ))
3498}
3499
3500#[allow(clippy::infallible_destructuring_match)]
3501fn user_facing_observables_output_formats(integrand: &Integrand) -> Vec<ObservableFileFormat> {
3502 let process_integrand = match integrand {
3503 Integrand::ProcessIntegrand(process_integrand) => process_integrand,
3504 #[cfg(test)]
3505 Integrand::TestProbe(_) => return Vec::new(),
3506 };
3507 if !integrand.has_observables() {
3508 return Vec::new();
3509 }
3510
3511 process_integrand
3512 .get_settings()
3513 .integrator
3514 .observables_output
3515 .resolved_formats()
3516}
3517
3518#[allow(clippy::infallible_destructuring_match)]
3519fn user_facing_observables_output_enabled(integrand: &Integrand) -> bool {
3520 let process_integrand = match integrand {
3521 Integrand::ProcessIntegrand(process_integrand) => process_integrand,
3522 #[cfg(test)]
3523 Integrand::TestProbe(_) => return false,
3524 };
3525 if !integrand.has_observables() {
3526 return false;
3527 }
3528
3529 !process_integrand
3530 .get_settings()
3531 .integrator
3532 .observables_output
3533 .resolved_formats()
3534 .is_empty()
3535}
3536
3537fn write_atomic_bytes(path: &Path, bytes: &[u8]) -> Result<()> {
3538 let parent = path
3539 .parent()
3540 .ok_or_else(|| Report::msg("Atomic write target is missing a parent directory"))?;
3541 fs::create_dir_all(parent)?;
3542 let tmp_path = path.with_extension(format!(
3543 "{}.tmp",
3544 path.extension()
3545 .and_then(|ext| ext.to_str())
3546 .unwrap_or("tmp")
3547 ));
3548 fs::write(&tmp_path, bytes)?;
3549 fs::rename(&tmp_path, path)?;
3550 Ok(())
3551}
3552
3553fn write_atomic_json<T: Serialize>(path: &Path, value: &T) -> Result<()> {
3554 write_atomic_bytes(path, &serde_json::to_vec_pretty(value)?)
3555}
3556
3557fn write_observable_resume_state(
3558 integrand: &Integrand,
3559 workspace: Option<&Path>,
3560 slot_meta: &SlotMeta,
3561 iter: usize,
3562 output_control: WorkspaceSnapshotControl,
3563) -> Result<Option<PathBuf>> {
3564 let Some(bundle) = integrand.observable_snapshot_bundle() else {
3565 return Ok(None);
3566 };
3567 let Some(workspace) = workspace else {
3568 return Ok(None);
3569 };
3570 let latest_path = latest_observable_resume_state_path(workspace, slot_meta);
3571 write_atomic_json(&latest_path, &bundle)?;
3572 if output_control.write_iteration_archives {
3573 let archived_path = archived_observable_resume_state_path(workspace, slot_meta, iter);
3574 write_atomic_json(&archived_path, &bundle)?;
3575 }
3576 Ok(Some(latest_path))
3577}
3578
3579fn write_observable_snapshot_archive(
3580 integrand: &Integrand,
3581 workspace: Option<&Path>,
3582 iter: usize,
3583 output_control: WorkspaceSnapshotControl,
3584) -> Result<()> {
3585 if !user_facing_observables_output_enabled(integrand) {
3586 return Ok(());
3587 }
3588 if !output_control.write_iteration_archives {
3589 return Ok(());
3590 }
3591 let Some(workspace) = workspace else {
3592 return Ok(());
3593 };
3594
3595 for format in user_facing_observables_output_formats(integrand) {
3596 let path = archived_observable_output_path(workspace, format, iter);
3597 integrand.write_observable_snapshots(&path, format)?;
3598 }
3599
3600 Ok(())
3601}
3602
3603fn write_latest_observables_output(
3604 integrand: &Integrand,
3605 workspace: Option<&Path>,
3606) -> Result<Vec<PathBuf>> {
3607 if !user_facing_observables_output_enabled(integrand) {
3608 return Ok(Vec::new());
3609 }
3610 let Some(workspace) = workspace else {
3611 return Ok(Vec::new());
3612 };
3613
3614 let mut emitted_paths = Vec::new();
3615 for format in user_facing_observables_output_formats(integrand) {
3616 let path = latest_observable_output_path(workspace, format);
3617 integrand.write_observable_snapshots(&path, format)?;
3618 emitted_paths.push(path);
3619 }
3620
3621 Ok(emitted_paths)
3622}
3623
3624fn write_numerical_stability_snapshot(
3625 bundle: &ObservableSnapshotBundle,
3626 path: &Path,
3627 format: ObservableFileFormat,
3628) -> Result<()> {
3629 match format {
3630 ObservableFileFormat::None => Ok(()),
3631 ObservableFileFormat::Json => write_atomic_json(path, bundle),
3632 ObservableFileFormat::Hwu => {
3633 let parent = path
3634 .parent()
3635 .ok_or_else(|| Report::msg("Numerical stability output is missing a parent"))?;
3636 fs::create_dir_all(parent)?;
3637 bundle.write_hwu_file(path)
3638 }
3639 }
3640}
3641
3642fn write_numerical_stability_outputs(
3643 workspace: Option<&Path>,
3644 integration_state: &IntegrationState,
3645 output_control: WorkspaceSnapshotControl,
3646) -> Result<Vec<PathBuf>> {
3647 let Some(workspace) = workspace else {
3648 return Ok(Vec::new());
3649 };
3650
3651 let mut emitted_paths = Vec::new();
3652 let mut scopes = vec![(
3653 "global".to_string(),
3654 integration_state
3655 .stats
3656 .numerical_stability_snapshot()
3657 .as_observable_snapshot_bundle(),
3658 )];
3659 scopes.extend(
3660 integration_state
3661 .slot_metas
3662 .iter()
3663 .zip(integration_state.slot_stats.iter())
3664 .map(|(slot_meta, stats)| {
3665 (
3666 slot_meta.key(),
3667 stats
3668 .numerical_stability_snapshot()
3669 .as_observable_snapshot_bundle(),
3670 )
3671 }),
3672 );
3673
3674 for (scope, bundle) in scopes {
3675 for format in [ObservableFileFormat::Json, ObservableFileFormat::Hwu] {
3676 let latest_path = numerical_stability_output_path(workspace, &scope, format, None);
3677 write_numerical_stability_snapshot(&bundle, &latest_path, format)?;
3678 emitted_paths.push(latest_path);
3679 if output_control.write_iteration_archives {
3680 let archive_path = numerical_stability_output_path(
3681 workspace,
3682 &scope,
3683 format,
3684 Some(integration_state.iter),
3685 );
3686 write_numerical_stability_snapshot(&bundle, &archive_path, format)?;
3687 }
3688 }
3689 }
3690
3691 Ok(emitted_paths)
3692}
3693
3694fn write_integration_state_to_workspace(
3695 workspace_path: &Path,
3696 integration_state: &IntegrationState,
3697) -> Result<()> {
3698 let state_path = workspace_state_path(workspace_path);
3699 let state_bytes = bincode::encode_to_vec(integration_state, bincode::config::standard())
3700 .map_err(|err| {
3701 Report::msg(format!(
3702 "Could not serialize integration state for {}: {err}",
3703 state_path.display()
3704 ))
3705 })?;
3706 write_atomic_bytes(&state_path, &state_bytes)?;
3707 Ok(())
3708}
3709
3710fn write_integration_result_snapshots(
3711 workspace_path: &Path,
3712 integration_state: &IntegrationState,
3713 targets: &[Option<Complex<F<f64>>>],
3714 output_control: WorkspaceSnapshotControl,
3715) -> Result<()> {
3716 let result = build_integration_result(integration_state, targets);
3717 write_atomic_json(&workspace_result_snapshot_path(workspace_path), &result)?;
3718 if output_control.write_iteration_archives {
3719 write_atomic_json(
3720 &workspace_result_archive_path(workspace_path, integration_state.iter),
3721 &result,
3722 )?;
3723 }
3724 Ok(())
3725}
3726
3727fn workspace_relative_display_path(root: &Path, path: &Path) -> String {
3728 path.strip_prefix(root)
3729 .unwrap_or(path)
3730 .display()
3731 .to_string()
3732}
3733
3734fn emit_results_output_summary(
3735 workspace: Option<&Path>,
3736 slots: &[IntegrationSlot],
3737 emitted_paths: &[Vec<PathBuf>],
3738 numerical_stability_paths: &[PathBuf],
3739) {
3740 let rendered = render_results_output_summary_table(
3741 workspace,
3742 slots,
3743 emitted_paths,
3744 numerical_stability_paths,
3745 );
3746 let Some(rendered) = rendered else {
3747 return;
3748 };
3749
3750 info!("\n{rendered}");
3751}
3752
3753fn render_results_output_summary_table(
3754 workspace: Option<&Path>,
3755 slots: &[IntegrationSlot],
3756 emitted_paths: &[Vec<PathBuf>],
3757 numerical_stability_paths: &[PathBuf],
3758) -> Option<String> {
3759 let mut summary_rows = Vec::new();
3760 if let Some(workspace) = workspace {
3761 summary_rows.push((
3762 "workspace path".to_string(),
3763 workspace.display().to_string(),
3764 ));
3765 summary_rows.push((
3766 "results".to_string(),
3767 workspace_relative_display_path(workspace, &workspace_result_snapshot_path(workspace)),
3768 ));
3769 }
3770
3771 for (slot, slot_emitted_paths) in slots.iter().zip(emitted_paths.iter()) {
3772 for final_path in slot_emitted_paths {
3773 if !final_path.exists() {
3774 continue;
3775 }
3776 let display_path = workspace
3777 .map(|root| workspace_relative_display_path(root, final_path))
3778 .unwrap_or_else(|| final_path.display().to_string());
3779 let row_label = if slot_emitted_paths.len() > 1 {
3780 let format_label = final_path
3781 .extension()
3782 .and_then(|extension| extension.to_str())
3783 .unwrap_or("output");
3784 format!("{} ({format_label})", slot_key_label(&slot.meta))
3785 } else {
3786 slot_key_label(&slot.meta)
3787 };
3788 summary_rows.push((row_label, display_path));
3789 }
3790 }
3791
3792 for path in numerical_stability_paths {
3793 if !path.exists() {
3794 continue;
3795 }
3796 let display_path = workspace
3797 .map(|root| workspace_relative_display_path(root, path))
3798 .unwrap_or_else(|| path.display().to_string());
3799 let scope = path
3800 .file_stem()
3801 .and_then(|stem| stem.to_str())
3802 .unwrap_or("numerical_stability");
3803 summary_rows.push((format!("numerical stability ({scope})"), display_path));
3804 }
3805
3806 if summary_rows.is_empty() {
3807 return None;
3808 }
3809
3810 let mut builder = Builder::default();
3811 builder.push_record(["type".to_string(), "path".to_string()]);
3812 for (row_type, row_path) in summary_rows {
3813 builder.push_record([
3814 row_type.blue().bold().to_string(),
3815 row_path.green().to_string(),
3816 ]);
3817 }
3818
3819 let mut table = builder.build();
3820 table.with(Panel::header(
3821 "Integration results emitted".green().bold().to_string(),
3822 ));
3823 table.with(
3824 Style::rounded().horizontals([(
3825 1,
3826 HorizontalLine::new('─')
3827 .intersection('┼')
3828 .left('├')
3829 .right('┤'),
3830 )]),
3831 );
3832 table.with(Modify::new(Rows::first()).with(Alignment::left()));
3833 table.with(Modify::new(Columns::new(0..2)).with(Alignment::left()));
3834
3835 Some(utils::normalize_tabled_separator_rows(&table.to_string()))
3836}
3837
3838fn evaluate_sample_list(
3840 integrand: &mut Integrand,
3841 samples: &[Sample<F<f64>>],
3842 model: &Model,
3843 num_cores: usize,
3844 iter: usize,
3845 max_eval: Complex<F<f64>>,
3846) -> Result<(Vec<EvaluationResult>, StatisticsCounter)> {
3847 let list_size = samples.len();
3848 let nvec_per_core = (list_size - 1) / num_cores + 1;
3849 let num_chunks = samples.chunks(nvec_per_core).len();
3850
3851 let sample_chunks = samples.par_chunks(nvec_per_core);
3852 let integrands = (0..num_chunks)
3853 .map(|_| integrand.clone())
3854 .collect_vec()
3855 .into_par_iter();
3856
3857 let evaluation_results_per_core: Vec<
3858 Result<(Vec<EvaluationResult>, StatisticsCounter, Integrand)>,
3859 > = sample_chunks
3860 .zip(integrands)
3861 .map(|(chunk, mut integrand)| {
3862 let raw_batch = integrand.evaluate_samples_raw(
3863 chunk,
3864 EvaluationTarget::Physical(model),
3865 iter,
3866 false,
3867 false,
3868 max_eval,
3869 )?;
3870 Ok((raw_batch.samples, raw_batch.statistics, integrand))
3871 })
3872 .collect();
3873 let mut evaluation_results_per_core: Vec<(
3874 Vec<EvaluationResult>,
3875 StatisticsCounter,
3876 Integrand,
3877 )> = evaluation_results_per_core
3878 .into_iter()
3879 .collect::<Result<_>>()?;
3880
3881 for (_, _, worker_integrand) in evaluation_results_per_core.iter_mut() {
3882 integrand.merge_runtime_results(worker_integrand)?;
3883 }
3884
3885 let (evaluation_results, meta_data_statistics) = evaluation_results_per_core.into_iter().fold(
3886 (Vec::new(), StatisticsCounter::new_empty()),
3887 |(mut all_results, stats), (results, worker_stats, _)| {
3888 all_results.extend(results);
3889 (all_results, stats.merged(&worker_stats))
3890 },
3891 );
3892
3893 Ok((evaluation_results, meta_data_statistics))
3894}
3895
3896#[derive(Serialize, Deserialize)]
3900pub enum SampleInput {
3901 SampleList {
3902 samples: Vec<Sample<F<f64>>>,
3903 },
3904 Grid {
3905 grid: Box<Grid<F<f64>>>,
3906 num_points: usize,
3907 seed: u64,
3908 thread_id: usize,
3909 },
3910}
3911
3912type AccumulatorPair = (StatisticsAccumulator<F<f64>>, StatisticsAccumulator<F<f64>>);
3913
3914#[derive(Serialize, Deserialize)]
3915pub enum BatchIntegrateOutput {
3916 Default(Vec<Complex<F<f64>>>, Vec<Sample<F<f64>>>),
3917 Accumulator(Box<AccumulatorPair>, Box<Grid<F<f64>>>),
3918}
3919
3920#[derive(Serialize, Deserialize)]
3923pub enum EventOutput {
3924 None,
3925 EventList {
3926 event_groups: EventGroupList,
3927 },
3928 Histogram {
3929 histograms: ObservableAccumulatorBundle,
3930 },
3931}
3932
3933#[derive(Serialize, Deserialize, Encode, Decode)]
3935pub struct BatchResult {
3936 pub statistics: StatisticsCounter,
3937 #[bincode(with_serde)]
3938 pub integrand_data: BatchIntegrateOutput,
3939 #[bincode(with_serde)]
3940 pub event_data: EventOutput,
3941}
3942
3943pub struct BatchIntegrateInput<'a> {
3945 pub max_eval: Complex<F<f64>>,
3947 pub iter: usize,
3948 pub settings: &'a RuntimeSettings,
3949 pub samples: SampleInput,
3951 pub integrand_output_settings: IntegralOutputSettings,
3952 pub event_output_settings: EventOutputSettings,
3953 pub num_cores: usize,
3954}
3955
3956#[derive(Serialize, Deserialize)]
3958pub enum IntegralOutputSettings {
3959 Default,
3960 Accumulator,
3961}
3962
3963#[derive(Serialize, Deserialize)]
3965pub enum EventOutputSettings {
3966 None,
3967 EventList,
3968 Histogram,
3969}
3970
3971#[derive(Serialize, Deserialize, Encode, Decode)]
3972pub struct SerializableBatchIntegrateInput {
3973 #[bincode(with_serde)]
3974 pub max_eval: Complex<F<f64>>,
3975 pub iter: usize,
3976 #[bincode(with_serde)]
3977 pub samples: SampleInput,
3978 #[bincode(with_serde)]
3979 pub integrand_output_settings: IntegralOutputSettings,
3980 #[bincode(with_serde)]
3981 pub event_output_settings: EventOutputSettings,
3982 pub num_cores: usize,
3983}
3984
3985impl SerializableBatchIntegrateInput {
3986 pub(crate) fn into_batch_integrate_input(
3987 self,
3988 settings: &RuntimeSettings,
3989 ) -> BatchIntegrateInput<'_> {
3990 BatchIntegrateInput {
3991 max_eval: self.max_eval,
3992 iter: self.iter,
3993 samples: self.samples,
3994 integrand_output_settings: self.integrand_output_settings,
3995 event_output_settings: self.event_output_settings,
3996 num_cores: self.num_cores,
3997 settings,
3998 }
3999 }
4000}
4001
4002#[derive(Clone)]
4004pub struct MasterNode {
4005 grid: Grid<F<f64>>,
4006 integrator_settings: IntegratorSettings,
4007 master_accumulator_re: StatisticsAccumulator<F<f64>>,
4008 master_accumulator_im: StatisticsAccumulator<F<f64>>,
4009 statistics: StatisticsCounter,
4010 observable_accumulators: Option<ObservableAccumulatorBundle>,
4011 event_groups: EventGroupList,
4012 current_iter: usize,
4013}
4014
4015impl MasterNode {
4016 pub(crate) fn new(grid: Grid<F<f64>>, integrator_settings: IntegratorSettings) -> Self {
4017 MasterNode {
4018 grid,
4019 integrator_settings,
4020 master_accumulator_im: StatisticsAccumulator::new(),
4021 master_accumulator_re: StatisticsAccumulator::new(),
4022 statistics: StatisticsCounter::new_empty(),
4023 observable_accumulators: None,
4024 event_groups: EventGroupList::default(),
4025 current_iter: 0,
4026 }
4027 }
4028
4029 fn update_grid_with_grid(&mut self, other_grid: &Grid<F<f64>>) -> Result<(), String> {
4031 self.grid.merge(other_grid)
4032 }
4033
4034 fn update_grid_with_samples(
4036 &mut self,
4037 samples_points: &[Sample<F<f64>>],
4038 results: &[Complex<F<f64>>],
4039 ) -> Result<(), String> {
4040 let integrated_phase = self.integrator_settings.integrated_phase;
4041
4042 for (sample_point, result) in samples_points.iter().zip(results.iter()) {
4043 match integrated_phase {
4044 IntegratedPhase::Real => self.grid.add_training_sample(sample_point, result.re)?,
4045 IntegratedPhase::Imag => self.grid.add_training_sample(sample_point, result.im)?,
4046 IntegratedPhase::Both => {
4047 unimplemented!("integrated phase both not yet implemented")
4048 }
4049 }
4050 }
4051
4052 Ok(())
4053 }
4054
4055 pub(crate) fn update_accumulators_with_accumulators(
4057 &mut self,
4058 mut real_accumulator: StatisticsAccumulator<F<f64>>,
4059 mut imaginary_accumulator: StatisticsAccumulator<F<f64>>,
4060 ) {
4061 self.master_accumulator_re
4062 .merge_samples(&mut real_accumulator);
4063
4064 self.master_accumulator_im
4065 .merge_samples(&mut imaginary_accumulator);
4066 }
4067
4068 fn update_accumuators_with_samples(
4070 &mut self,
4071 sample_points: &[Sample<F<f64>>],
4072 results: &[Complex<F<f64>>],
4073 ) {
4074 for (sample_point, result) in sample_points.iter().zip(results.iter()) {
4075 self.master_accumulator_re
4076 .add_sample(result.re * sample_point.get_weight(), Some(sample_point));
4077
4078 self.master_accumulator_im
4079 .add_sample(result.im * sample_point.get_weight(), Some(sample_point));
4080 }
4081 }
4082
4083 fn update_metadata_statistics(&mut self, statistics: StatisticsCounter) {
4085 self.statistics = self.statistics.merged(&statistics);
4086 }
4087
4088 pub(crate) fn update_iter(&mut self) {
4090 self.grid.update(
4091 F(self.integrator_settings.discrete_dim_learning_rate),
4092 F(self.integrator_settings.continuous_dim_learning_rate),
4093 );
4094 self.master_accumulator_re.update_iter(false);
4095 self.master_accumulator_im.update_iter(false);
4096 if let Some(observable_accumulators) = self.observable_accumulators.as_mut() {
4097 observable_accumulators.update_results();
4098 }
4099
4100 self.current_iter += 1;
4101 }
4102
4103 pub(crate) fn write_batch_input(
4105 &mut self,
4106 num_cores: usize,
4107 num_samples: usize,
4108 export_grid: bool,
4109 output_accumulator: bool,
4110 workspace_path: &str,
4111 job_id: usize,
4112 ) -> Result<(), Report> {
4113 let max_eval = Complex::new(
4114 self.master_accumulator_re
4115 .max_eval_positive
4116 .max(self.master_accumulator_re.max_eval_negative),
4117 self.master_accumulator_im
4118 .max_eval_positive
4119 .max(self.master_accumulator_im.max_eval_negative),
4120 );
4121
4122 let samples = if export_grid {
4123 SampleInput::Grid {
4124 grid: Box::new(self.grid.clone()),
4125 num_points: num_samples,
4126 seed: self.integrator_settings.seed,
4127 thread_id: job_id,
4128 }
4129 } else {
4130 let mut rng = rand::rng();
4131 let mut samples_temp = vec![Sample::new(); num_samples];
4132 for sample in samples_temp.iter_mut() {
4133 self.grid.sample(&mut rng, sample);
4134 }
4135 SampleInput::SampleList {
4136 samples: samples_temp,
4137 }
4138 };
4139
4140 let integrand_output_settings = if output_accumulator {
4141 IntegralOutputSettings::Accumulator
4142 } else {
4143 IntegralOutputSettings::Default
4144 };
4145
4146 let input = SerializableBatchIntegrateInput {
4147 num_cores,
4148 max_eval,
4149 iter: self.current_iter,
4150 samples,
4151 integrand_output_settings,
4152 event_output_settings: EventOutputSettings::None,
4153 };
4154
4155 let input_bytes = bincode::encode_to_vec(&input, bincode::config::standard())?;
4156 let job_name = format!("job_{}", job_id);
4157 let job_path = std::path::Path::new(workspace_path).join(job_name);
4158
4159 std::fs::write(job_path, input_bytes)?;
4160
4161 Ok(())
4162 }
4163
4164 pub(crate) fn process_batch_output(&mut self, output: BatchResult) -> Result<(), String> {
4166 self.update_metadata_statistics(output.statistics);
4167
4168 match output.integrand_data {
4169 BatchIntegrateOutput::Default(results, samples) => {
4170 self.update_accumuators_with_samples(&samples, &results);
4171 self.update_grid_with_samples(&samples, &results)?;
4172 }
4173 BatchIntegrateOutput::Accumulator(accumulators, grid) => {
4174 let (real_accumulator, imag_accumulator) = *accumulators;
4175 self.update_accumulators_with_accumulators(real_accumulator, imag_accumulator);
4176 self.update_grid_with_grid(&grid)?;
4177 }
4178 }
4179
4180 match output.event_data {
4181 EventOutput::None => {}
4182 EventOutput::EventList { mut event_groups } => {
4183 self.event_groups.append(&mut event_groups);
4184 }
4185 EventOutput::Histogram { mut histograms } => {
4186 if let Some(existing) = self.observable_accumulators.as_mut() {
4187 existing
4188 .merge_samples(&mut histograms)
4189 .map_err(|err| err.to_string())?;
4190 } else {
4191 self.observable_accumulators = Some(histograms);
4192 }
4193 }
4194 }
4195
4196 Ok(())
4197 }
4198
4199 pub(crate) fn display_status(&self) {
4201 let status_block = [
4202 render_integral_result(
4203 &self.master_accumulator_re,
4204 "itg",
4205 self.current_iter,
4206 "re",
4207 None,
4208 ),
4209 render_integral_result(
4210 &self.master_accumulator_im,
4211 "itg",
4212 self.current_iter,
4213 "im",
4214 None,
4215 ),
4216 self.statistics.render_status_table(),
4217 ]
4218 .join("\n");
4219
4220 info!("\n{status_block}");
4221 }
4222}
4223
4224pub fn emit_integration_status_via_tracing(
4225 kind: IntegrationStatusKind,
4226 status_block: impl AsRef<str>,
4227) -> Result<()> {
4228 let status_block = status_block.as_ref();
4229 match kind {
4230 IntegrationStatusKind::Live => {}
4231 IntegrationStatusKind::Iteration => {
4232 info!("\n{status_block}");
4233 info!("");
4234 }
4235 IntegrationStatusKind::Final => {
4236 info!("");
4237 info!("{}", "Final integration results:".bold().green());
4238 info!("");
4239 info!("\n{status_block}");
4240 info!("");
4241 }
4242 }
4243
4244 Ok(())
4245}
4246
4247pub fn render_saved_integration_summary(
4248 integration_state: &IntegrationState,
4249 targets: &[Option<Complex<F<f64>>>],
4250 view_options: &IntegrationStatusViewOptions,
4251 tabled_options: &TabledRenderOptions,
4252) -> String {
4253 render_tabled::render_status_update(
4254 &build_saved_status_update(integration_state, targets, view_options),
4255 tabled_options,
4256 )
4257}
4258
4259pub fn render_status_update_tabled(
4260 update: &StatusUpdate,
4261 tabled_options: &TabledRenderOptions,
4262) -> String {
4263 render_tabled::render_status_update(update, tabled_options)
4264}
4265
4266fn integral_estimate(
4267 re: &StatisticsAccumulator<F<f64>>,
4268 im: &StatisticsAccumulator<F<f64>>,
4269) -> IntegralEstimate {
4270 IntegralEstimate {
4271 neval: re.processed_samples,
4272 real_zero: re.num_zero_evaluations,
4273 im_zero: im.num_zero_evaluations,
4274 result: Complex::new(re.avg, im.avg),
4275 error: Complex::new(re.err, im.err),
4276 real_chisq: re.chi_sq,
4277 im_chisq: im.chi_sq,
4278 }
4279}
4280
4281pub fn build_integration_result(
4282 integration_state: &IntegrationState,
4283 targets: &[Option<Complex<F<f64>>>],
4284) -> IntegrationResult {
4285 let integration_statistics = integration_state.stats.snapshot();
4286 let slots = integration_state
4287 .slot_metas
4288 .iter()
4289 .enumerate()
4290 .map(|(slot_index, slot_meta)| {
4291 let accumulator = &integration_state.all_integrals[slot_index];
4292 let discrete_context =
4293 integration_state.monitored_discrete_context_for_slot(slot_index);
4294 SlotIntegrationResult {
4295 key: slot_meta.key(),
4296 process: slot_meta.process_name.clone(),
4297 integrand: slot_meta.integrand_name.clone(),
4298 target: targets[slot_index],
4299 integral: integral_estimate(&accumulator.re, &accumulator.im),
4300 table_results: build_table_result_summary(
4301 slot_meta,
4302 accumulator,
4303 integration_state.iter,
4304 targets[slot_index],
4305 ),
4306 integration_statistics: integration_statistics.clone(),
4307 max_weight_info: build_max_weight_info_summary(
4308 &integration_state
4309 .sampling_state_for_slot(slot_index)
4310 .discrete_axis_labels,
4311 accumulator,
4312 ),
4313 grid_breakdown: ComponentDiscreteBreakdown {
4314 re: discrete_context.as_ref().and_then(|context| {
4315 integration_state.slot_re_summaries[slot_index]
4316 .as_ref()
4317 .zip(
4318 integration_state
4319 .slot_first_non_trivial_discrete_breakdown_metadata[slot_index]
4320 .as_ref(),
4321 )
4322 .and_then(|(summary, metadata)| {
4323 summary_at_path(summary, &context.path).and_then(|summary| {
4324 summary.first_non_trivial_breakdown(metadata, &context.pdfs)
4325 })
4326 })
4327 }),
4328 im: discrete_context.as_ref().and_then(|context| {
4329 integration_state.slot_im_summaries[slot_index]
4330 .as_ref()
4331 .zip(
4332 integration_state
4333 .slot_first_non_trivial_discrete_breakdown_metadata[slot_index]
4334 .as_ref(),
4335 )
4336 .and_then(|(summary, metadata)| {
4337 summary_at_path(summary, &context.path).and_then(|summary| {
4338 summary.first_non_trivial_breakdown(metadata, &context.pdfs)
4339 })
4340 })
4341 }),
4342 },
4343 absolute: AbsoluteIntegrationResult {
4344 integral: integral_estimate(&accumulator.absolute_re, &accumulator.absolute_im),
4345 table_results: build_absolute_table_result_summary(
4346 slot_meta,
4347 accumulator,
4348 integration_state.iter,
4349 ),
4350 max_weight_info: build_absolute_max_weight_info_summary(
4351 &integration_state
4352 .sampling_state_for_slot(slot_index)
4353 .discrete_axis_labels,
4354 accumulator,
4355 ),
4356 grid_breakdown: ComponentDiscreteBreakdown {
4357 re: discrete_context.as_ref().and_then(|context| {
4358 integration_state.slot_absolute_re_summaries[slot_index]
4359 .as_ref()
4360 .zip(
4361 integration_state
4362 .slot_first_non_trivial_discrete_breakdown_metadata
4363 [slot_index]
4364 .as_ref(),
4365 )
4366 .and_then(|(summary, metadata)| {
4367 summary.first_non_trivial_breakdown(metadata, &context.pdfs)
4368 })
4369 }),
4370 im: discrete_context.as_ref().and_then(|context| {
4371 integration_state.slot_absolute_im_summaries[slot_index]
4372 .as_ref()
4373 .zip(
4374 integration_state
4375 .slot_first_non_trivial_discrete_breakdown_metadata
4376 [slot_index]
4377 .as_ref(),
4378 )
4379 .and_then(|(summary, metadata)| {
4380 summary.first_non_trivial_breakdown(metadata, &context.pdfs)
4381 })
4382 }),
4383 },
4384 },
4385 }
4386 })
4387 .collect();
4388
4389 IntegrationResult { slots }
4390}
4391
4392pub fn print_integral_result(
4393 itg: &StatisticsAccumulator<F<f64>>,
4394 label: &str,
4395 i_iter: usize,
4396 tag: &str,
4397 trgt: Option<F<f64>>,
4398) {
4399 info!("{}", render_integral_result(itg, label, i_iter, tag, trgt));
4400}
4401
4402#[allow(clippy::format_in_format_args)]
4403fn render_integral_result(
4404 itg: &StatisticsAccumulator<F<f64>>,
4405 label: &str,
4406 i_iter: usize,
4407 tag: &str,
4408 trgt: Option<F<f64>>,
4409) -> String {
4410 let slot_meta = SlotMeta {
4411 process_name: label.to_string(),
4412 integrand_name: String::new(),
4413 };
4414 let mut cells = build_integral_result_cells(itg, &slot_meta, i_iter, tag, trgt);
4415 cells.integrand = format!("{label} {}:", format!("{:-2}", tag).blue().bold());
4416 let delta = match (cells.delta_sigma.as_ref(), cells.delta_percent.as_ref()) {
4417 (Some(delta_sigma), Some(delta_percent)) => format!("{delta_sigma}, {delta_percent}"),
4418 _ => String::new(),
4419 };
4420
4421 format!(
4422 "| {} {} {} {} {} {}",
4423 cells.integrand, cells.value, cells.relative_error, cells.chi_sq, delta, cells.mwi
4424 )
4425}
4426
4427#[cfg(test)]
4428mod tests {
4429 use super::*;
4430 use crate::integrands::TestProbeIntegrand;
4431 use colored::control;
4432 use ratatui::{Terminal, backend::TestBackend};
4433 use std::fs;
4434 use symbolica::numerical_integration::ContinuousGrid;
4435
4436 fn integration_workspace_manifest(version: Option<u32>) -> IntegrationWorkspaceManifest {
4437 let mut manifest = serde_json::json!({
4438 "slots": [],
4439 "targets": [],
4440 "effective_model_parameters": [],
4441 "integrand_fingerprints": [],
4442 "training_slot": 0,
4443 "integrator_settings_slot": 0,
4444 "sampling_correlation_mode": "correlated"
4445 });
4446 if let Some(version) = version {
4447 manifest["version"] = version.into();
4448 }
4449 serde_json::from_value(manifest).unwrap()
4450 }
4451
4452 #[test]
4453 fn integration_workspace_manifest_accepts_only_the_current_version() {
4454 integration_workspace_manifest(Some(IntegrationWorkspaceManifest::CURRENT_VERSION))
4455 .validate_version()
4456 .unwrap();
4457
4458 let unversioned = integration_workspace_manifest(None);
4459 assert_eq!(unversioned.version, 0);
4460 let unversioned_error = unversioned.validate_version().unwrap_err().to_string();
4461 assert!(unversioned_error.contains("unversioned"));
4462 assert!(unversioned_error.contains("--restart"));
4463
4464 let previous = integration_workspace_manifest(Some(2));
4465 let previous_error = previous.validate_version().unwrap_err().to_string();
4466 assert!(previous_error.contains("--restart"));
4467
4468 let future =
4469 integration_workspace_manifest(Some(IntegrationWorkspaceManifest::CURRENT_VERSION + 1));
4470 let future_error = future.validate_version().unwrap_err().to_string();
4471 assert!(future_error.contains("newer format version"));
4472 assert!(future_error.contains("--restart"));
4473 }
4474
4475 fn make_accumulator(
4476 re_avg: f64,
4477 re_err: f64,
4478 re_chi_sq: f64,
4479 im_avg: f64,
4480 im_err: f64,
4481 im_chi_sq: f64,
4482 ) -> ComplexAccumulator {
4483 let mut accumulator = ComplexAccumulator::new();
4484 accumulator.re.avg = F(re_avg);
4485 accumulator.re.err = F(re_err);
4486 accumulator.re.chi_sq = F(re_chi_sq);
4487 accumulator.re.processed_samples = 100_000;
4488 accumulator.re.max_eval_positive = F(1.0);
4489 accumulator.im.avg = F(im_avg);
4490 accumulator.im.err = F(im_err);
4491 accumulator.im.chi_sq = F(im_chi_sq);
4492 accumulator.im.processed_samples = 100_000;
4493 accumulator.im.max_eval_positive = F(1.0);
4494 accumulator.absolute_re.avg = F(re_avg.abs() * 2.0);
4495 accumulator.absolute_re.err = F(re_err.abs());
4496 accumulator.absolute_re.chi_sq = F(re_chi_sq);
4497 accumulator.absolute_re.processed_samples = 100_000;
4498 accumulator.absolute_re.max_eval_positive = F(2.0);
4499 accumulator.absolute_im.avg = F(im_avg.abs() * 2.0);
4500 accumulator.absolute_im.err = F(im_err.abs());
4501 accumulator.absolute_im.chi_sq = F(im_chi_sq);
4502 accumulator.absolute_im.processed_samples = 100_000;
4503 accumulator.absolute_im.max_eval_positive = F(2.0);
4504 accumulator
4505 }
4506
4507 #[test]
4508 fn absolute_channel_sum_retains_outer_cube_covariance() {
4509 let mut accumulator = ComplexAccumulator::new();
4510 for a in [1.0, 2.0, 3.0] {
4514 accumulator.add_sample(
4515 Complex::new_zero(),
4516 Some(Complex::new(F(2.0 * a), F(4.0 * a))),
4517 F(1.0),
4518 None,
4519 );
4520 }
4521 accumulator.update_iter(false);
4522 assert_eq!(accumulator.re.avg, F(0.0));
4523 assert_eq!(accumulator.im.avg, F(0.0));
4524 assert_eq!(accumulator.absolute_re.avg, F(4.0));
4525 assert_eq!(accumulator.absolute_im.avg, F(8.0));
4526 assert_eq!(accumulator.absolute_re.processed_samples, 3);
4527 assert!((accumulator.absolute_re.err - F((4.0_f64 / 3.0).sqrt())).abs() < F(1.0e-12));
4528 assert!((accumulator.absolute_im.err - F((16.0_f64 / 3.0).sqrt())).abs() < F(1.0e-12));
4529 }
4530
4531 #[test]
4532 fn complex_accumulator_tracks_componentwise_absolute_weighted_samples_and_merges() {
4533 let first_sample = Sample::Continuous(F(1.0), vec![F(0.25)]);
4534 let second_sample = Sample::Continuous(F(1.0), vec![F(0.75)]);
4535 let mut accumulator = ComplexAccumulator::new();
4536 let mut other = ComplexAccumulator::new();
4537
4538 accumulator.add_sample(
4539 Complex::new(F(-2.0), F(3.0)),
4540 None,
4541 F(2.0),
4542 Some(&first_sample),
4543 );
4544 other.add_sample(
4545 Complex::new(F(1.0), F(-4.0)),
4546 None,
4547 F(2.0),
4548 Some(&second_sample),
4549 );
4550 accumulator.merge(&other);
4551 accumulator.update_iter(false);
4552
4553 assert_eq!(accumulator.re.avg, F(-1.0));
4554 assert_eq!(accumulator.im.avg, F(-1.0));
4555 assert_eq!(accumulator.absolute_re.avg, F(3.0));
4556 assert_eq!(accumulator.absolute_im.avg, F(7.0));
4557 assert!((accumulator.absolute_re.err - F(1.0)).abs() < F(1.0e-12));
4558 assert!((accumulator.absolute_im.err - F(1.0)).abs() < F(1.0e-12));
4559 assert_eq!(accumulator.absolute_re.processed_samples, 2);
4560 assert_eq!(accumulator.absolute_im.processed_samples, 2);
4561 assert_eq!(accumulator.absolute_re.max_eval_positive, F(4.0));
4562 assert_eq!(accumulator.absolute_im.max_eval_positive, F(8.0));
4563 assert_eq!(
4564 accumulator
4565 .absolute_re
4566 .max_eval_positive_xs
4567 .as_ref()
4568 .map(|sample| format!("{sample:?}")),
4569 Some(format!("{first_sample:?}"))
4570 );
4571 assert_eq!(
4572 accumulator
4573 .absolute_im
4574 .max_eval_positive_xs
4575 .as_ref()
4576 .map(|sample| format!("{sample:?}")),
4577 Some(format!("{second_sample:?}"))
4578 );
4579 }
4580
4581 #[test]
4582 fn shallow_absolute_monitor_uses_local_path_pdfs_and_has_no_descendants() {
4583 let continuous_grid =
4584 || Grid::Continuous(ContinuousGrid::new(1, 8, 10, None, false).unwrap());
4585 let monitored_grid = Grid::Discrete(
4586 DiscreteGrid::new(
4587 vec![Some(continuous_grid()), Some(continuous_grid())],
4588 F(10.0),
4589 false,
4590 )
4591 .unwrap(),
4592 );
4593 let mut production_grid =
4594 Grid::Discrete(DiscreteGrid::new(vec![Some(monitored_grid)], F(10.0), false).unwrap());
4595 let Grid::Discrete(root) = &mut production_grid else {
4596 unreachable!()
4597 };
4598 let Some(Grid::Discrete(monitored)) = root.bins[0].sub_grid.as_mut() else {
4599 unreachable!()
4600 };
4601 monitored.bins[0].pdf = F(0.25);
4602 monitored.bins[1].pdf = F(0.75);
4603
4604 let mut monitor = shallow_discrete_monitor_grid(&production_grid, &[0])
4605 .expect("nested discrete path should produce a shallow monitor");
4606 assert_eq!(monitor.bins[0].pdf, F(0.25));
4607 assert_eq!(monitor.bins[1].pdf, F(0.75));
4608 assert!(monitor.bins.iter().all(|bin| bin.sub_grid.is_none()));
4609
4610 let sample = |bin_index, local_weight, x| {
4611 Sample::Discrete(
4612 F(local_weight),
4613 0,
4614 Some(Box::new(Sample::Discrete(
4615 F(local_weight),
4616 bin_index,
4617 Some(Box::new(Sample::Continuous(F(1.0), vec![F(x)]))),
4618 ))),
4619 )
4620 };
4621 for (sample, evaluation) in [
4622 (sample(0, 4.0, 0.1), F(-3.0)),
4623 (sample(0, 4.0, 0.2), F(5.0)),
4624 (sample(1, 4.0 / 3.0, 0.3), F(-6.0)),
4625 (sample(1, 4.0 / 3.0, 0.4), F(10.0)),
4626 ] {
4627 add_shallow_monitor_sample(Some(&mut monitor), Some(&[0]), &sample, evaluation)
4628 .expect("valid local discrete sample should be accumulated");
4629 }
4630
4631 let mut summary = DiscreteGridAccumulatorSummary::from_shallow_grid(&monitor);
4632 summary.merge_shallow_iteration_grid(&monitor);
4633 summary.update_iter();
4634 assert!(summary.bins.iter().all(|bin| bin.sub_summary.is_none()));
4635 assert_eq!(summary.bins[0].accumulator.avg, F(4.0));
4636 assert_eq!(summary.bins[1].accumulator.avg, F(8.0));
4637 assert_eq!(summary.bins[0].accumulator.processed_samples, 2);
4638 assert_eq!(summary.bins[1].accumulator.processed_samples, 2);
4639
4640 let mismatched_sample =
4641 Sample::Discrete(F(1.0), 1, Some(Box::new(Sample::Discrete(F(1.0), 0, None))));
4642 assert!(
4643 add_shallow_monitor_sample(Some(&mut monitor), Some(&[0]), &mismatched_sample, F(1.0),)
4644 .is_err()
4645 );
4646 }
4647
4648 #[test]
4649 fn shallow_monitor_creation_does_not_change_production_sampling_sequence() {
4650 let continuous_grid =
4651 || Grid::Continuous(ContinuousGrid::new(1, 8, 10, None, false).unwrap());
4652 let monitored_grid = Grid::Discrete(
4653 DiscreteGrid::new(
4654 vec![Some(continuous_grid()), Some(continuous_grid())],
4655 F(10.0),
4656 false,
4657 )
4658 .unwrap(),
4659 );
4660 let sampling_grid =
4661 Grid::Discrete(DiscreteGrid::new(vec![Some(monitored_grid)], F(10.0), false).unwrap());
4662 let settings = RuntimeSettings::default();
4663 let integrand = Integrand::TestProbe(TestProbeIntegrand::new(settings, 3));
4664 let mut without_monitor = CoreIterationState::new(
4665 vec![integrand.clone()],
4666 SamplingCorrelationMode::Correlated,
4667 std::slice::from_ref(&sampling_grid),
4668 None,
4669 37,
4670 11,
4671 0,
4672 );
4673 let mut with_monitor = CoreIterationState::new(
4674 vec![integrand],
4675 SamplingCorrelationMode::Correlated,
4676 std::slice::from_ref(&sampling_grid),
4677 Some(&[0]),
4678 37,
4679 11,
4680 0,
4681 );
4682
4683 for _ in 0..16 {
4684 let mut without_sample = Sample::new();
4685 let mut with_sample = Sample::new();
4686 let without_state = &mut without_monitor.sampling_states[0];
4687 without_state
4688 .sampling_grid
4689 .sample(&mut without_state.rng, &mut without_sample);
4690 let with_state = &mut with_monitor.sampling_states[0];
4691 with_state
4692 .sampling_grid
4693 .sample(&mut with_state.rng, &mut with_sample);
4694 assert_eq!(format!("{without_sample:?}"), format!("{with_sample:?}"));
4695 }
4696 }
4697
4698 struct StatisticsFixture {
4699 precision: crate::settings::runtime::Precision,
4700 total_time: Duration,
4701 integrand_time: Duration,
4702 evaluator_time: Duration,
4703 parameterization_time: Duration,
4704 event_time: Duration,
4705 generated_event_count: usize,
4706 accepted_event_count: usize,
4707 }
4708
4709 fn make_statistics_counter(fixture: StatisticsFixture) -> StatisticsCounter {
4710 let mut evaluation = EvaluationResult::zero();
4711 evaluation.evaluation_metadata.total_timing = fixture.total_time;
4712 evaluation.evaluation_metadata.integrand_evaluation_time = fixture.integrand_time;
4713 evaluation.evaluation_metadata.evaluator_evaluation_time = fixture.evaluator_time;
4714 evaluation.evaluation_metadata.parameterization_time = fixture.parameterization_time;
4715 evaluation.evaluation_metadata.event_processing_time = fixture.event_time;
4716 evaluation.evaluation_metadata.generated_event_count = fixture.generated_event_count;
4717 evaluation.evaluation_metadata.accepted_event_count = fixture.accepted_event_count;
4718 let estimated_relative_accuracy = match fixture.precision {
4719 crate::settings::runtime::Precision::Double => Some(F(2.4e-16)),
4720 crate::settings::runtime::Precision::Quad => Some(F(1.3e-26)),
4721 crate::settings::runtime::Precision::Arb => None,
4722 };
4723 evaluation.evaluation_metadata.stability_results.push(
4724 crate::integrands::evaluation::StabilityResult {
4725 precision: fixture.precision,
4726 estimated_relative_accuracy,
4727 estimated_decimal_digits: estimated_relative_accuracy
4728 .filter(|accuracy| accuracy.0 != 0.0 && accuracy.0.is_finite())
4729 .map(|accuracy| F(-accuracy.0.abs().log10())),
4730 status: crate::integrands::evaluation::StabilityStatus::Stable(2),
4731 total_time: fixture.total_time,
4732 },
4733 );
4734 StatisticsCounter::from_evaluation_results(&[evaluation])
4735 }
4736
4737 fn make_integration_state() -> IntegrationState {
4738 let sampling_grid = Grid::Continuous(ContinuousGrid::new(1, 64, 100, None, false).unwrap());
4739 let mut state = IntegrationState::new_from_settings(
4740 SamplingCorrelationMode::Correlated,
4741 vec![SamplingSlotState::new(sampling_grid, Vec::new())],
4742 vec![
4743 SlotMeta {
4744 process_name: "proc_a".to_string(),
4745 integrand_name: "itg_a".to_string(),
4746 },
4747 SlotMeta {
4748 process_name: "proc_b".to_string(),
4749 integrand_name: "itg_b".to_string(),
4750 },
4751 ],
4752 None,
4753 None,
4754 None,
4755 vec![None, None],
4756 );
4757 state.iter = 1;
4758 state.num_points = 100_000;
4759 state.slot_stats = vec![
4760 make_statistics_counter(StatisticsFixture {
4761 precision: crate::settings::runtime::Precision::Double,
4762 total_time: Duration::from_micros(462),
4763 integrand_time: Duration::from_micros(414),
4764 evaluator_time: Duration::from_micros(279),
4765 parameterization_time: Duration::from_nanos(5_800),
4766 event_time: Duration::from_micros(12),
4767 generated_event_count: 7,
4768 accepted_event_count: 5,
4769 }),
4770 make_statistics_counter(StatisticsFixture {
4771 precision: crate::settings::runtime::Precision::Quad,
4772 total_time: Duration::from_micros(900),
4773 integrand_time: Duration::from_micros(810),
4774 evaluator_time: Duration::from_micros(120),
4775 parameterization_time: Duration::from_micros(34),
4776 event_time: Duration::from_micros(40),
4777 generated_event_count: 9,
4778 accepted_event_count: 6,
4779 }),
4780 ];
4781 state.stats = state.slot_stats[0].merged(&state.slot_stats[1]);
4782 state
4783 .stats
4784 .add_integrator_overhead(Duration::from_micros(110), 2);
4785 state.all_integrals = vec![
4786 make_accumulator(7.5e-5, 9.8e-5, 0.394, 3.2e-5, 1.5e-5, 0.378),
4787 make_accumulator(2.1e-5, 2.0e-6, 0.221, -1.7e-5, 3.0e-6, 0.187),
4788 ];
4789 state
4790 }
4791
4792 fn make_preview_test_slots(slot_metas: &[SlotMeta]) -> Vec<IntegrationSlot> {
4793 slot_metas
4794 .iter()
4795 .cloned()
4796 .map(|meta| {
4797 let settings = RuntimeSettings::default();
4798 IntegrationSlot::new(
4799 meta,
4800 settings.clone(),
4801 Model::default(),
4802 Integrand::TestProbe(TestProbeIntegrand::new(settings, 3)),
4803 None,
4804 )
4805 })
4806 .collect()
4807 }
4808
4809 fn make_preview_test_core_state(state: &IntegrationState) -> CoreIterationState {
4810 let slot_stats = vec![
4811 make_statistics_counter(StatisticsFixture {
4812 precision: crate::settings::runtime::Precision::Arb,
4813 total_time: Duration::from_millis(8),
4814 integrand_time: Duration::from_millis(7),
4815 evaluator_time: Duration::from_millis(3),
4816 parameterization_time: Duration::from_micros(150),
4817 event_time: Duration::from_micros(500),
4818 generated_event_count: 40,
4819 accepted_event_count: 30,
4820 }),
4821 make_statistics_counter(StatisticsFixture {
4822 precision: crate::settings::runtime::Precision::Arb,
4823 total_time: Duration::from_millis(12),
4824 integrand_time: Duration::from_millis(10),
4825 evaluator_time: Duration::from_millis(2),
4826 parameterization_time: Duration::from_micros(300),
4827 event_time: Duration::from_millis(1),
4828 generated_event_count: 60,
4829 accepted_event_count: 45,
4830 }),
4831 ];
4832 let grid_template = state
4833 .sampling_state_for_slot(0)
4834 .grid
4835 .clone_without_samples();
4836 CoreIterationState {
4837 slot_integrands: Vec::new(),
4838 stats: slot_stats[0].merged(&slot_stats[1]),
4839 slot_stats,
4840 integrals: vec![ComplexAccumulator::new(); state.slot_metas.len()],
4841 sampling_correlation_mode: SamplingCorrelationMode::Correlated,
4842 sampling_states: vec![CoreSamplingSlotState {
4843 sampling_grid: grid_template.clone(),
4844 rng: MonteCarloRng::new(7, 0),
4845 }],
4846 slot_re_grids: (0..state.slot_metas.len())
4847 .map(|_| grid_template.clone())
4848 .collect(),
4849 slot_im_grids: (0..state.slot_metas.len())
4850 .map(|_| grid_template.clone())
4851 .collect(),
4852 monitored_discrete_path: None,
4853 slot_absolute_re_grids: vec![None; state.slot_metas.len()],
4854 slot_absolute_im_grids: vec![None; state.slot_metas.len()],
4855 remaining_points: 0,
4856 completed_points: 12,
4857 failed_samples: Vec::new(),
4858 }
4859 }
4860
4861 #[test]
4862 fn failed_sample_records_preserve_replay_and_append_across_iterations() {
4863 use crate::integrands::evaluation::{StabilityResult, StabilityStatus};
4864 use crate::settings::runtime::Precision;
4865
4866 let directory = std::env::temp_dir().join(format!(
4867 "gammaloop_failed_sample_records_{}",
4868 std::process::id()
4869 ));
4870 if directory.exists() {
4871 fs::remove_dir_all(&directory).unwrap();
4872 }
4873 let state = make_integration_state();
4874 let mut worker = make_preview_test_core_state(&state);
4875 let sample = Sample::Discrete(
4876 F(7.5),
4877 0,
4878 Some(Box::new(Sample::Discrete(
4879 F(2.5),
4880 3,
4881 Some(Box::new(Sample::Continuous(
4882 F(1.25),
4883 vec![F(0.12345678901234567), F(0.9)],
4884 ))),
4885 ))),
4886 );
4887 let mut metadata = EvaluationMetaData::new_empty();
4888 metadata.is_nan = true;
4889 metadata.record_threshold_counterterm_error("AlmostPrimalInfeasible: no interior center");
4890 for precision in [Precision::Double, Precision::Quad, Precision::Arb] {
4891 metadata.stability_results.push(StabilityResult {
4892 precision,
4893 estimated_relative_accuracy: None,
4894 estimated_decimal_digits: None,
4895 status: StabilityStatus::Unstable(1),
4896 total_time: Duration::ZERO,
4897 });
4898 }
4899 let path = directory.join("numerical_stability/failed_samples.jsonl");
4900 worker
4901 .write_failed_samples(Some(&directory), &state.slot_metas, 1)
4902 .unwrap();
4903 assert!(
4904 !path.exists(),
4905 "successful batches should not create a failure log"
4906 );
4907 for iteration in [1, 2] {
4908 worker
4909 .failed_samples
4910 .push((1, sample.clone(), metadata.clone()));
4911 worker
4912 .write_failed_samples(Some(&directory), &state.slot_metas, iteration)
4913 .unwrap();
4914 assert!(worker.failed_samples.is_empty());
4915 worker
4916 .write_failed_samples(Some(&directory), &state.slot_metas, iteration)
4917 .unwrap();
4918 }
4919 let records = fs::read_to_string(path).unwrap();
4920 assert_eq!(
4921 records.lines().count(),
4922 2,
4923 "drained records must not be duplicated"
4924 );
4925 for (index, line) in records.lines().enumerate() {
4926 let row: serde_json::Value = serde_json::from_str(line).unwrap();
4927 assert_eq!(row["iteration"], index + 1);
4928 assert_eq!(row["slot"]["process_name"], "proc_b");
4929 assert_eq!(row["evaluation_metadata"]["is_nan"], true);
4930 assert_eq!(
4931 row["evaluation_metadata"]["stability_results"]
4932 .as_array()
4933 .unwrap()
4934 .len(),
4935 3
4936 );
4937 assert!(
4938 row["evaluation_metadata"]["threshold_counterterm_error"]
4939 .as_str()
4940 .unwrap()
4941 .contains("AlmostPrimalInfeasible")
4942 );
4943 let replay: Sample<F<f64>> = serde_json::from_value(row["sample"].clone()).unwrap();
4944 assert_eq!(replay.get_weight(), sample.get_weight());
4945 assert_eq!(
4946 serde_json::to_value(replay).unwrap(),
4947 serde_json::to_value(&sample).unwrap()
4948 );
4949 }
4950 fs::remove_dir_all(directory).unwrap();
4951 }
4952
4953 fn make_discrete_integration_state() -> IntegrationState {
4954 let make_continuous_grid =
4955 || Grid::Continuous(ContinuousGrid::new(1, 64, 100, None, false).unwrap());
4956 let sampling_grid = Grid::Discrete(
4957 DiscreteGrid::new(
4958 vec![Some(make_continuous_grid()), Some(make_continuous_grid())],
4959 F(10.0),
4960 false,
4961 )
4962 .unwrap(),
4963 );
4964 let mut state = IntegrationState::new_from_settings(
4965 SamplingCorrelationMode::Correlated,
4966 vec![SamplingSlotState::new(
4967 sampling_grid,
4968 vec!["graph".to_string()],
4969 )],
4970 vec![
4971 SlotMeta {
4972 process_name: "proc_a".to_string(),
4973 integrand_name: "itg_a".to_string(),
4974 },
4975 SlotMeta {
4976 process_name: "proc_b".to_string(),
4977 integrand_name: "itg_b".to_string(),
4978 },
4979 ],
4980 Some(vec![]),
4981 Some("graph".to_string()),
4982 Some(vec!["GL0".to_string(), "GL1".to_string()]),
4983 vec![
4984 Some(PersistedDiscreteBreakdownMetadata {
4985 axis_label: "graph".to_string(),
4986 fixed_coordinates: Vec::new(),
4987 bin_labels: vec!["GL0".to_string(), "GL1".to_string()],
4988 }),
4989 Some(PersistedDiscreteBreakdownMetadata {
4990 axis_label: "graph".to_string(),
4991 fixed_coordinates: Vec::new(),
4992 bin_labels: vec!["GL0".to_string(), "GL1".to_string()],
4993 }),
4994 ],
4995 );
4996 state.first_non_trivial_discrete_bin_descriptions =
4997 Some(vec!["GL0".to_string(), "GL1".to_string()]);
4998 state.iter = 2;
4999 state.num_points = 210_000;
5000 state.slot_stats = vec![
5001 make_statistics_counter(StatisticsFixture {
5002 precision: crate::settings::runtime::Precision::Double,
5003 total_time: Duration::from_micros(462),
5004 integrand_time: Duration::from_micros(414),
5005 evaluator_time: Duration::from_micros(279),
5006 parameterization_time: Duration::from_nanos(5_800),
5007 event_time: Duration::from_micros(12),
5008 generated_event_count: 7,
5009 accepted_event_count: 5,
5010 }),
5011 make_statistics_counter(StatisticsFixture {
5012 precision: crate::settings::runtime::Precision::Quad,
5013 total_time: Duration::from_micros(900),
5014 integrand_time: Duration::from_micros(810),
5015 evaluator_time: Duration::from_micros(120),
5016 parameterization_time: Duration::from_micros(34),
5017 event_time: Duration::from_micros(40),
5018 generated_event_count: 9,
5019 accepted_event_count: 6,
5020 }),
5021 ];
5022 state.stats = state.slot_stats[0].merged(&state.slot_stats[1]);
5023 state
5024 .stats
5025 .add_integrator_overhead(Duration::from_micros(110), 2);
5026 state.all_integrals = vec![
5027 make_accumulator(7.5e-5, 9.8e-5, 0.394, 3.2e-5, 1.5e-5, 0.378),
5028 make_accumulator(2.1e-5, 2.0e-6, 0.221, -1.7e-5, 3.0e-6, 0.187),
5029 ];
5030 if let Grid::Discrete(discrete_grid) = &mut state.sampling_state_for_slot_mut(0).grid {
5031 discrete_grid.bins[0].pdf = F(0.75);
5032 discrete_grid.bins[1].pdf = F(0.25);
5033 }
5034
5035 for summaries in [&mut state.slot_re_summaries, &mut state.slot_im_summaries] {
5036 for (slot_index, summary) in summaries.iter_mut().enumerate() {
5037 let summary = summary.as_mut().expect("discrete summary expected");
5038 summary.bins[0].accumulator.avg = F(1.0e-5 * (slot_index as f64 + 1.0));
5039 summary.bins[0].accumulator.err = F(2.0e-6);
5040 summary.bins[0].accumulator.chi_sq = F(0.2);
5041 summary.bins[0].accumulator.processed_samples = 150;
5042 summary.bins[0].accumulator.max_eval_positive = F(0.75);
5043 summary.bins[0].accumulator.max_eval_positive_xs =
5044 Some(Sample::Continuous(F(1.0), vec![F(0.25)]));
5045 summary.bins[1].accumulator.avg = F(5.0e-6 * (slot_index as f64 + 1.0));
5046 summary.bins[1].accumulator.err = F(1.0e-6);
5047 summary.bins[1].accumulator.chi_sq = F(0.1);
5048 summary.bins[1].accumulator.processed_samples = 50;
5049 summary.bins[1].accumulator.max_eval_positive = F(0.25);
5050 summary.bins[1].accumulator.max_eval_positive_xs =
5051 Some(Sample::Continuous(F(1.0), vec![F(0.75)]));
5052 }
5053 }
5054 for (summaries, component_scale) in [
5055 (&mut state.slot_absolute_re_summaries, 4.0),
5056 (&mut state.slot_absolute_im_summaries, 3.0),
5057 ] {
5058 for (slot_index, summary) in summaries.iter_mut().enumerate() {
5059 let summary = summary.as_mut().expect("shallow absolute summary expected");
5060 let slot_scale = slot_index as f64 + 1.0;
5061 summary.bins[0].accumulator.avg = F(component_scale * 1.0e-5 * slot_scale);
5062 summary.bins[0].accumulator.err = F(3.0e-6);
5063 summary.bins[0].accumulator.chi_sq = F(0.3);
5064 summary.bins[0].accumulator.processed_samples = 150;
5065 summary.bins[0].accumulator.max_eval_positive = F(1.25);
5066 summary.bins[0].accumulator.max_eval_positive_xs =
5067 Some(Sample::Continuous(F(1.0), vec![F(0.35)]));
5068 summary.bins[1].accumulator.avg = F(component_scale * 0.5e-5 * slot_scale);
5069 summary.bins[1].accumulator.err = F(1.5e-6);
5070 summary.bins[1].accumulator.chi_sq = F(0.15);
5071 summary.bins[1].accumulator.processed_samples = 50;
5072 summary.bins[1].accumulator.max_eval_positive = F(0.5);
5073 summary.bins[1].accumulator.max_eval_positive_xs =
5074 Some(Sample::Continuous(F(1.0), vec![F(0.85)]));
5075 }
5076 }
5077
5078 state
5079 }
5080
5081 fn make_single_graph_discrete_integration_state() -> IntegrationState {
5082 let mut state = make_discrete_integration_state();
5083 let Grid::Discrete(grid) = &mut state.sampling_state_for_slot_mut(0).grid else {
5084 unreachable!("discrete fixture must use a discrete grid")
5085 };
5086 grid.bins.truncate(1);
5087 grid.bins[0].pdf = F(1.0);
5088 for summaries in [
5089 &mut state.slot_re_summaries,
5090 &mut state.slot_im_summaries,
5091 &mut state.slot_absolute_re_summaries,
5092 &mut state.slot_absolute_im_summaries,
5093 ] {
5094 for summary in summaries.iter_mut().flatten() {
5095 summary.bins.truncate(1);
5096 }
5097 }
5098 state.first_non_trivial_discrete_bin_descriptions = Some(vec!["GL22".to_string()]);
5099 for metadata in &mut state.slot_first_non_trivial_discrete_breakdown_metadata {
5100 if let Some(metadata) = metadata.as_mut() {
5101 metadata.bin_labels = vec!["GL22".to_string()];
5102 }
5103 }
5104 state
5105 }
5106
5107 fn default_view_options() -> IntegrationStatusViewOptions {
5108 IntegrationStatusViewOptions {
5109 phase_display: IntegrationStatusPhaseDisplay::Both,
5110 training_phase_display: IntegrationStatusPhaseDisplay::Real,
5111 training_slot: 0,
5112 slot_training_phase_displays: vec![IntegrationStatusPhaseDisplay::Real],
5113 per_slot_training_phase: false,
5114 target_relative_accuracy: None,
5115 target_absolute_accuracy: None,
5116 show_statistics: true,
5117 show_max_weight_details: true,
5118 show_top_discrete_grid: false,
5119 show_discrete_contributions_sum: false,
5120 contribution_sort: ContributionSortMode::Error,
5121 show_max_weight_info_for_discrete_bins: false,
5122 }
5123 }
5124
5125 fn default_tabled_options() -> TabledRenderOptions {
5126 TabledRenderOptions {
5127 max_table_width: DEFAULT_MAX_SHARED_TABLE_WIDTH,
5128 show_statistics: true,
5129 show_max_weight_details: true,
5130 show_top_discrete_grid: false,
5131 show_discrete_contributions_sum: false,
5132 show_max_weight_info_for_discrete_bins: false,
5133 }
5134 }
5135
5136 fn tabled_options_for_view(view_options: &IntegrationStatusViewOptions) -> TabledRenderOptions {
5137 TabledRenderOptions {
5138 show_statistics: view_options.show_statistics,
5139 show_max_weight_details: view_options.show_max_weight_details,
5140 show_top_discrete_grid: view_options.show_top_discrete_grid,
5141 show_discrete_contributions_sum: view_options.show_discrete_contributions_sum,
5142 show_max_weight_info_for_discrete_bins: view_options
5143 .show_max_weight_info_for_discrete_bins,
5144 ..default_tabled_options()
5145 }
5146 }
5147
5148 #[test]
5149 fn correlated_core_iteration_state_populates_slot_statistics() {
5150 let settings_a = RuntimeSettings::default();
5151 let settings_b = RuntimeSettings::default();
5152 let integrand_a = Integrand::TestProbe(TestProbeIntegrand::new(settings_a.clone(), 3));
5153 let integrand_b = Integrand::TestProbe(TestProbeIntegrand::new(settings_b.clone(), 3));
5154 let sampling_grid_template = SamplingSlotState::from_integrand(&integrand_a).grid;
5155 let mut core_state = CoreIterationState::new(
5156 vec![integrand_a, integrand_b],
5157 SamplingCorrelationMode::Correlated,
5158 &[sampling_grid_template],
5159 None,
5160 1337,
5161 0,
5162 8,
5163 );
5164 let model = Model::default();
5165 let slot_settings = [&settings_a, &settings_b];
5166 let slot_targets = [EvaluationTarget::Physical(&model); 2];
5167 let current_max_evals = [Complex::new(F(0.0), F(0.0)), Complex::new(F(0.0), F(0.0))];
5168
5169 let processed = core_state
5170 .evaluate_chunk(
5171 &slot_settings,
5172 &slot_targets,
5173 0,
5174 ¤t_max_evals,
5175 &[None, None],
5176 8,
5177 )
5178 .expect("correlated chunk evaluation should succeed");
5179
5180 assert_eq!(processed, 8);
5181 assert!(
5182 core_state
5183 .slot_stats
5184 .iter()
5185 .all(|stats| stats.snapshot().num_evals == processed),
5186 "{:?}",
5187 core_state
5188 .slot_stats
5189 .iter()
5190 .map(|stats| stats.snapshot().num_evals)
5191 .collect_vec()
5192 );
5193 }
5194
5195 fn render_update(request: StatusUpdateBuildRequest<'_>) -> String {
5196 let tabled_options = tabled_options_for_view(request.render_options);
5197 render_status_update_tabled(&build_status_update(request), &tabled_options)
5198 }
5199
5200 fn render_ratatui_update(
5201 request: StatusUpdateBuildRequest<'_>,
5202 configure: impl FnOnce(&mut RatatuiDashboardState),
5203 ) -> String {
5204 let mut dashboard = RatatuiDashboardState::new();
5205 dashboard.update(build_status_update(request));
5206 configure(&mut dashboard);
5207
5208 let backend = TestBackend::new(180, 48);
5209 let mut terminal = Terminal::new(backend).expect("test terminal");
5210 terminal
5211 .draw(|frame| dashboard.draw(frame))
5212 .expect("ratatui draw");
5213
5214 let buffer = terminal.backend().buffer();
5215 (0..buffer.area.height)
5216 .map(|y| {
5217 let mut line = String::new();
5218 for x in 0..buffer.area.width {
5219 line.push_str(buffer[(x, y)].symbol());
5220 }
5221 line.trim_end().to_string()
5222 })
5223 .collect::<Vec<_>>()
5224 .join("\n")
5225 }
5226
5227 #[test]
5228 fn max_weight_details_table_renders_titled_table_without_wrapping_parentheses() {
5229 let mut accumulator = ComplexAccumulator::new();
5230 accumulator.re.max_eval_positive = F(2.3840672847728);
5231 accumulator.re.max_eval_positive_xs = Some(Sample::Discrete(
5232 F(1.0),
5233 0,
5234 Some(Box::new(Sample::Discrete(
5235 F(1.0),
5236 0,
5237 Some(Box::new(Sample::Continuous(
5238 F(1.0),
5239 vec![
5240 F(0.9695746085826609),
5241 F(0.5327714835649985),
5242 F(0.003410284786539597),
5243 ],
5244 ))),
5245 ))),
5246 ));
5247
5248 let mut state = IntegrationState::new_from_settings(
5249 SamplingCorrelationMode::Correlated,
5250 vec![SamplingSlotState::new(
5251 Grid::Continuous(ContinuousGrid::new(1, 64, 100, None, false).unwrap()),
5252 Vec::new(),
5253 )],
5254 vec![SlotMeta {
5255 process_name: "proc".to_string(),
5256 integrand_name: "itg".to_string(),
5257 }],
5258 None,
5259 None,
5260 None,
5261 vec![None],
5262 );
5263 state.all_integrals = vec![accumulator];
5264 let view_options = IntegrationStatusViewOptions {
5265 show_statistics: false,
5266 show_top_discrete_grid: false,
5267 show_discrete_contributions_sum: false,
5268 ..default_view_options()
5269 };
5270 let rendered = render_status_update_tabled(
5271 &build_status_update(
5272 StatusUpdateBuildRequest::new(
5273 IntegrationStatusKind::Iteration,
5274 &state,
5275 &[None],
5276 &view_options,
5277 )
5278 .with_timing(
5279 1,
5280 Duration::from_secs(0),
5281 Duration::from_secs(0),
5282 0,
5283 0,
5284 0,
5285 ),
5286 ),
5287 &default_tabled_options(),
5288 );
5289
5290 assert!(rendered.contains("Maximum weight details"), "{rendered}");
5291 assert!(rendered.contains("Integrand"), "{rendered}");
5292 assert!(rendered.contains("Max eval"), "{rendered}");
5293 assert!(rendered.contains("Max eval coordinates"), "{rendered}");
5294 assert!(rendered.contains("proc@itg"), "{rendered}");
5295 assert!(rendered.contains("re [+]"), "{rendered}");
5296 assert!(rendered.contains("idx: 0, idx: 0, xs: [ "), "{rendered}");
5297 assert!(rendered.contains("e-01"), "{rendered}");
5298 assert!(rendered.contains("e-03 ]"), "{rendered}");
5299 assert!(!rendered.contains(", 0.532"), "{rendered}");
5300 assert!(!rendered.contains("( graph:"), "{rendered}");
5301 }
5302
5303 #[test]
5304 fn iteration_status_block_uses_compact_header_and_optional_statistics() {
5305 let state = make_integration_state();
5306 let view_options = IntegrationStatusViewOptions {
5307 show_statistics: false,
5308 show_max_weight_details: false,
5309 ..default_view_options()
5310 };
5311 let rendered = render_update(
5312 StatusUpdateBuildRequest::new(
5313 IntegrationStatusKind::Iteration,
5314 &state,
5315 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
5316 &view_options,
5317 )
5318 .with_timing(
5319 4,
5320 Duration::from_secs(1),
5321 Duration::from_secs(1),
5322 100_000,
5323 100_000,
5324 100_000,
5325 ),
5326 );
5327
5328 assert!(
5329 rendered.contains("Iteration # 1 ( completed )"),
5330 "{rendered}"
5331 );
5332 assert!(
5333 rendered.contains("# samples per iteration = 100.00K # samples total = 100.00K"),
5334 "{rendered}"
5335 );
5336 assert!(rendered.contains("/sample/core (4 cores)"), "{rendered}");
5337 assert!(rendered.contains("Contribution"), "{rendered}");
5338 assert!(rendered.contains("proc_a@itg_a"), "{rendered}");
5339 assert!(rendered.contains("proc_b@itg_b"), "{rendered}");
5340 assert!(rendered.contains("+7.5(9.8)e-5"), "{rendered}");
5341 assert!(rendered.contains("Δ = 0.255σ"), "{rendered}");
5342 assert!(!rendered.contains("Integration statistics"), "{rendered}");
5343 let lines = rendered.lines().collect::<Vec<_>>();
5344 assert_eq!(lines[1].matches('│').count(), 2, "{rendered}");
5345 assert!(lines[2].contains('┬'), "{rendered}");
5346 assert!(
5347 lines.last().is_some_and(|line| line.contains('┴')),
5348 "{rendered}"
5349 );
5350 }
5351
5352 #[test]
5353 fn iteration_status_block_omits_delta_columns_when_no_target_is_provided() {
5354 let state = make_integration_state();
5355 let view_options = IntegrationStatusViewOptions {
5356 show_statistics: true,
5357 show_max_weight_details: false,
5358 ..default_view_options()
5359 };
5360 let rendered = render_update(
5361 StatusUpdateBuildRequest::new(
5362 IntegrationStatusKind::Iteration,
5363 &state,
5364 &[None, None],
5365 &view_options,
5366 )
5367 .with_timing(
5368 4,
5369 Duration::from_secs(1),
5370 Duration::from_secs(1),
5371 100_000,
5372 100_000,
5373 100_000,
5374 ),
5375 );
5376
5377 assert!(!rendered.contains("Δ [σ]"), "{rendered}");
5378 assert!(!rendered.contains("Δ ="), "{rendered}");
5379 assert!(rendered.contains("Integration statistics"), "{rendered}");
5380 assert!(rendered.contains("mwi"), "{rendered}");
5381 }
5382
5383 #[test]
5384 fn tabled_all_rows_insert_value_only_absolute_continuations_and_filter_phases() {
5385 let state = make_discrete_integration_state();
5386 let targets = [Some(Complex::new(F(1.0e-4), F(2.0e-5))), None];
5387 let view_options = IntegrationStatusViewOptions {
5388 show_statistics: true,
5389 show_max_weight_details: true,
5390 show_top_discrete_grid: false,
5391 show_discrete_contributions_sum: false,
5392 ..default_view_options()
5393 };
5394 let update = build_status_update(StatusUpdateBuildRequest::new(
5395 IntegrationStatusKind::Iteration,
5396 &state,
5397 &targets,
5398 &view_options,
5399 ));
5400 let rendered = render_update(StatusUpdateBuildRequest::new(
5401 IntegrationStatusKind::Iteration,
5402 &state,
5403 &targets,
5404 &view_options,
5405 ));
5406 let lines = rendered.lines().collect_vec();
5407
5408 let row_text = |component, view| {
5409 update
5410 .main_results
5411 .find_row(status_update::ContributionKind::All, component)
5412 .and_then(|row| row.slot_cell(0))
5413 .and_then(|cell| cell.statistics(view).value.as_ref())
5414 .map(|value| value.display.to_plain_string())
5415 .expect("All row value should exist")
5416 };
5417 let signed_re = row_text(
5418 status_update::ComponentKind::Real,
5419 status_update::IntegralView::Signed,
5420 );
5421 let absolute_re = row_text(
5422 status_update::ComponentKind::Real,
5423 status_update::IntegralView::Absolute,
5424 );
5425 let signed_im = row_text(
5426 status_update::ComponentKind::Imag,
5427 status_update::IntegralView::Signed,
5428 );
5429 let absolute_im = row_text(
5430 status_update::ComponentKind::Imag,
5431 status_update::IntegralView::Absolute,
5432 );
5433 let line_index = |needle: &str| {
5434 lines
5435 .iter()
5436 .position(|line| line.contains(needle))
5437 .unwrap_or_else(|| panic!("missing {needle:?} in:\n{rendered}"))
5438 };
5439 let signed_re_line = line_index(&signed_re);
5440 let absolute_re_line = line_index(&absolute_re);
5441 let signed_im_line = line_index(&signed_im);
5442 let absolute_im_line = line_index(&absolute_im);
5443 assert!(signed_re_line < absolute_re_line);
5444 assert!(absolute_re_line < signed_im_line);
5445 assert!(signed_im_line < absolute_im_line);
5446 assert!(lines[absolute_re_line].contains("|re|"));
5447 assert!(lines[absolute_im_line].contains("|im|"));
5448
5449 for (component, absolute_line) in [
5450 (status_update::ComponentKind::Real, absolute_re_line),
5451 (status_update::ComponentKind::Imag, absolute_im_line),
5452 ] {
5453 let row = update
5454 .main_results
5455 .find_row(status_update::ContributionKind::All, component)
5456 .expect("All row should exist");
5457 let statistics = row
5458 .slot_cell(0)
5459 .expect("slot cells should exist")
5460 .statistics(status_update::IntegralView::Absolute);
5461 for metadata in [
5462 statistics.relative_error.as_ref(),
5463 statistics.chi_sq.as_ref(),
5464 statistics.max_weight_impact.as_ref(),
5465 ]
5466 .into_iter()
5467 .flatten()
5468 {
5469 let metadata = metadata.display.to_plain_string();
5470 assert!(
5471 !lines[absolute_line].contains(&metadata),
5472 "absolute continuation unexpectedly contains {metadata:?}: {}",
5473 lines[absolute_line]
5474 );
5475 }
5476 let (delta_sigma, delta_percent) =
5477 update.target_deltas_for_row_slot(row, 0, status_update::IntegralView::Absolute);
5478 assert!(delta_sigma.is_none());
5479 assert!(delta_percent.is_none());
5480 }
5481 assert_eq!(rendered.matches("Maximum weight details").count(), 1);
5482 assert_eq!(
5483 rendered.matches("Integration statistics [global]").count(),
5484 1
5485 );
5486
5487 for (phase, included, excluded) in [
5488 (IntegrationStatusPhaseDisplay::Real, "|re|", "|im|"),
5489 (IntegrationStatusPhaseDisplay::Imag, "|im|", "|re|"),
5490 ] {
5491 let phase_options = IntegrationStatusViewOptions {
5492 phase_display: phase,
5493 show_statistics: false,
5494 show_max_weight_details: false,
5495 ..default_view_options()
5496 };
5497 let phase_rendered = render_update(StatusUpdateBuildRequest::new(
5498 IntegrationStatusKind::Iteration,
5499 &state,
5500 &targets,
5501 &phase_options,
5502 ));
5503 assert!(phase_rendered.contains(included), "{phase_rendered}");
5504 assert!(!phase_rendered.contains(excluded), "{phase_rendered}");
5505 }
5506 }
5507
5508 #[test]
5509 fn absolute_sum_status_adds_shallow_means_and_combines_errors_in_quadrature() {
5510 let state = make_discrete_integration_state();
5511 let targets = [Some(Complex::new(F(1.0e-4), F(2.0e-5))), None];
5512 let view_options = IntegrationStatusViewOptions {
5513 show_discrete_contributions_sum: true,
5514 ..default_view_options()
5515 };
5516 let update = build_status_update(StatusUpdateBuildRequest::new(
5517 IntegrationStatusKind::Iteration,
5518 &state,
5519 &targets,
5520 &view_options,
5521 ));
5522 let row = update
5523 .main_results
5524 .find_row(
5525 status_update::ContributionKind::Sum,
5526 status_update::ComponentKind::Real,
5527 )
5528 .expect("absolute Sum row should exist");
5529 let value = row
5530 .slot_cell(0)
5531 .expect("slot cells should exist")
5532 .statistics(status_update::IntegralView::Absolute)
5533 .value
5534 .as_ref()
5535 .expect("absolute Sum estimate should exist")
5536 .raw;
5537
5538 assert!((value.0 - F(6.0e-5)).abs() < F(1.0e-18));
5539 let expected_error = (F(3.0e-6) * F(3.0e-6) + F(1.5e-6) * F(1.5e-6)).sqrt();
5540 assert!((value.1 - expected_error).abs() < F(1.0e-18));
5541 let (delta_sigma, delta_percent) =
5542 update.target_deltas_for_row_slot(row, 0, status_update::IntegralView::Absolute);
5543 assert!(delta_sigma.is_none());
5544 assert!(delta_percent.is_none());
5545 }
5546
5547 #[test]
5548 fn tabled_statistics_panel_uses_global_scope_label() {
5549 let state = make_integration_state();
5550 let view_options = IntegrationStatusViewOptions {
5551 show_statistics: true,
5552 show_max_weight_details: false,
5553 ..default_view_options()
5554 };
5555 let rendered = render_update(
5556 StatusUpdateBuildRequest::new(
5557 IntegrationStatusKind::Iteration,
5558 &state,
5559 &[None, None],
5560 &view_options,
5561 )
5562 .with_timing(
5563 4,
5564 Duration::from_secs(1),
5565 Duration::from_secs(1),
5566 100_000,
5567 100_000,
5568 100_000,
5569 ),
5570 );
5571
5572 assert!(
5573 rendered.contains("Integration statistics [global]"),
5574 "{rendered}"
5575 );
5576 }
5577
5578 #[test]
5579 fn live_iteration_status_block_uses_progress_header() {
5580 let mut state = make_integration_state();
5581 state.iter = 2;
5582 state.num_points = 125_000;
5583 let view_options = IntegrationStatusViewOptions {
5584 show_statistics: false,
5585 show_max_weight_details: false,
5586 ..default_view_options()
5587 };
5588 let rendered = render_update(
5589 StatusUpdateBuildRequest::new(
5590 IntegrationStatusKind::Live,
5591 &state,
5592 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
5593 &view_options,
5594 )
5595 .with_timing(
5596 4,
5597 Duration::from_secs(1),
5598 Duration::from_secs(1),
5599 25_000,
5600 125_000,
5601 125_000,
5602 )
5603 .with_live_progress(Some(status_update::LiveIterationProgress {
5604 completed_points: 25_000,
5605 target_points: 100_000,
5606 })),
5607 );
5608
5609 assert!(
5610 rendered.contains("Iteration # 2 ( running )"),
5611 "{rendered}"
5612 );
5613 assert!(
5614 rendered.contains("Iteration progress 25.00K/100.00K"),
5615 "{rendered}"
5616 );
5617 assert!(rendered.contains("25.0%"), "{rendered}");
5618 assert!(rendered.contains("# samples total = 125.00K"), "{rendered}");
5619 }
5620
5621 #[test]
5622 fn iteration_status_block_shows_discrete_contributions_and_discrete_max_weights() {
5623 let state = make_discrete_integration_state();
5624 let view_options = IntegrationStatusViewOptions {
5625 show_statistics: false,
5626 show_max_weight_details: true,
5627 show_top_discrete_grid: true,
5628 show_discrete_contributions_sum: true,
5629 contribution_sort: ContributionSortMode::Index,
5630 show_max_weight_info_for_discrete_bins: true,
5631 ..default_view_options()
5632 };
5633 let rendered = render_update(
5634 StatusUpdateBuildRequest::new(
5635 IntegrationStatusKind::Iteration,
5636 &state,
5637 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
5638 &view_options,
5639 )
5640 .with_timing(
5641 4,
5642 Duration::from_secs(2),
5643 Duration::from_secs(2),
5644 110_000,
5645 210_000,
5646 210_000,
5647 ),
5648 );
5649
5650 assert!(rendered.contains("Sum"), "{rendered}");
5651 assert!(rendered.contains("Contribution (idx=graph)"), "{rendered}");
5652 assert!(
5653 !rendered.contains("│ (idx=graph) │"),
5654 "{rendered}"
5655 );
5656 assert!(rendered.contains("#0: GL0"), "{rendered}");
5657 assert!(rendered.contains("75.0%"), "{rendered}");
5658 assert!(rendered.contains("25.0%"), "{rendered}");
5659 assert!(
5660 rendered.contains("Maximum weight details by discrete bin"),
5661 "{rendered}"
5662 );
5663 assert!(
5664 rendered.contains("xs: [ 2.5000000000000000e-01 ]"),
5665 "{rendered}"
5666 );
5667 }
5668
5669 #[test]
5670 fn grouped_graph_descriptions_include_all_group_members() {
5671 let description =
5672 graph_group_description(["GL0".to_string(), "GL1".to_string(), "GL2".to_string()]);
5673
5674 assert_eq!(description, "[GL0,GL1,GL2]");
5675 }
5676
5677 #[test]
5678 fn explicit_single_graph_subset_monitors_the_root_graph_axis() {
5679 let grid = Grid::Discrete(
5680 DiscreteGrid::new(
5681 vec![Some(Grid::Continuous(
5682 ContinuousGrid::new(1, 64, 100, None, false).unwrap(),
5683 ))],
5684 F(10.0),
5685 false,
5686 )
5687 .unwrap(),
5688 );
5689 let labels = vec!["graph".to_string()];
5690
5691 assert!(monitored_discrete_layout(&grid, &labels, false).is_none());
5692 assert_eq!(
5693 monitored_discrete_layout(&grid, &labels, true),
5694 Some((Vec::new(), "graph".to_string(), 1))
5695 );
5696 }
5697
5698 #[test]
5699 fn tabled_single_graph_subset_keeps_the_graph_row() {
5700 let state = make_single_graph_discrete_integration_state();
5701 let view_options = IntegrationStatusViewOptions {
5702 show_statistics: false,
5703 show_max_weight_details: false,
5704 show_top_discrete_grid: true,
5705 contribution_sort: ContributionSortMode::Index,
5706 ..default_view_options()
5707 };
5708 let rendered = render_update(StatusUpdateBuildRequest::new(
5709 IntegrationStatusKind::Iteration,
5710 &state,
5711 &[None, None],
5712 &view_options,
5713 ));
5714
5715 assert!(rendered.contains("Contribution (idx=graph)"), "{rendered}");
5716 assert!(rendered.contains("#0: GL22"), "{rendered}");
5717 assert!(!rendered.contains("GL1"), "{rendered}");
5718 }
5719
5720 #[test]
5721 fn iteration_status_block_hides_spanned_metadata_header_separators() {
5722 let state = make_discrete_integration_state();
5723 let view_options = IntegrationStatusViewOptions {
5724 show_statistics: false,
5725 show_max_weight_details: false,
5726 show_top_discrete_grid: true,
5727 show_discrete_contributions_sum: true,
5728 contribution_sort: ContributionSortMode::Index,
5729 show_max_weight_info_for_discrete_bins: false,
5730 ..default_view_options()
5731 };
5732 let rendered = render_update(
5733 StatusUpdateBuildRequest::new(
5734 IntegrationStatusKind::Iteration,
5735 &state,
5736 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
5737 &view_options,
5738 )
5739 .with_timing(
5740 4,
5741 Duration::from_secs(2),
5742 Duration::from_secs(2),
5743 110_000,
5744 210_000,
5745 210_000,
5746 ),
5747 );
5748
5749 let header_line = rendered
5750 .lines()
5751 .find(|line| line.contains("χ²/dof") && line.contains("mwi"))
5752 .expect("expected spanned metadata header line");
5753
5754 let chi_to_mwi =
5755 &header_line[header_line.find("χ²/dof").unwrap()..header_line.find("mwi").unwrap()];
5756 assert!(!chi_to_mwi.contains('│'), "{rendered}");
5757
5758 let delta_sigma_to_percent =
5759 &header_line[header_line.find("Δ [σ]").unwrap()..header_line.find("Δ [%]").unwrap()];
5760 assert!(!delta_sigma_to_percent.contains('│'), "{rendered}");
5761 }
5762
5763 #[test]
5764 fn integration_result_always_contains_first_non_trivial_discrete_breakdown() {
5765 let state = make_discrete_integration_state();
5766 let result =
5767 build_integration_result(&state, &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None]);
5768
5769 let slot = result
5770 .slot("proc_a@itg_a")
5771 .expect("discrete slot must be present");
5772 let breakdown = slot
5773 .grid_breakdown
5774 .re
5775 .as_ref()
5776 .expect("discrete breakdown must be persisted");
5777
5778 assert_eq!(breakdown.axis_label, "graph");
5779 assert!(breakdown.fixed_coordinates.is_empty());
5780 assert_eq!(breakdown.entries.len(), 2);
5781 assert_eq!(breakdown.entries[0].bin_index, 0);
5782 assert_eq!(breakdown.entries[0].bin_label.as_deref(), Some("GL0"));
5783 assert_eq!(breakdown.entries[0].pdf, F(0.75));
5784 assert_eq!(breakdown.entries[0].processed_samples, 150);
5785 assert_eq!(breakdown.entries[1].bin_index, 1);
5786 assert_eq!(breakdown.entries[1].bin_label.as_deref(), Some("GL1"));
5787 assert_eq!(breakdown.entries[1].pdf, F(0.25));
5788 assert_eq!(breakdown.entries[1].processed_samples, 50);
5789 }
5790
5791 #[test]
5792 fn integration_result_persists_absolute_bundle_and_shallow_breakdown() {
5793 let mut state = make_discrete_integration_state();
5794 let accumulator = &mut state.all_integrals[0];
5795 accumulator.absolute_re.avg = F(3.5e-4);
5796 accumulator.absolute_re.err = F(2.5e-5);
5797 accumulator.absolute_re.chi_sq = F(0.42);
5798 accumulator.absolute_re.processed_samples = 200;
5799 accumulator.absolute_re.max_eval_positive = F(8.0);
5800 accumulator.absolute_re.max_eval_positive_xs = Some(Sample::Discrete(
5801 F(4.0),
5802 0,
5803 Some(Box::new(Sample::Continuous(F(1.0), vec![F(0.25)]))),
5804 ));
5805 accumulator.absolute_im.avg = F(1.25e-4);
5806 accumulator.absolute_im.err = F(1.0e-5);
5807 accumulator.absolute_im.chi_sq = F(0.24);
5808 accumulator.absolute_im.processed_samples = 200;
5809 accumulator.absolute_im.max_eval_positive = F(5.0);
5810
5811 let re_summary = state.slot_absolute_re_summaries[0]
5812 .as_mut()
5813 .expect("absolute real summary should exist");
5814 re_summary.bins[0].accumulator.avg = F(2.0e-4);
5815 re_summary.bins[0].accumulator.err = F(2.0e-5);
5816 re_summary.bins[0].accumulator.processed_samples = 150;
5817 re_summary.bins[1].accumulator.avg = F(1.5e-4);
5818 re_summary.bins[1].accumulator.err = F(1.5e-5);
5819 re_summary.bins[1].accumulator.processed_samples = 50;
5820 let im_summary = state.slot_absolute_im_summaries[0]
5821 .as_mut()
5822 .expect("absolute imaginary summary should exist");
5823 im_summary.bins[0].accumulator.avg = F(7.5e-5);
5824 im_summary.bins[0].accumulator.processed_samples = 150;
5825 im_summary.bins[1].accumulator.avg = F(5.0e-5);
5826 im_summary.bins[1].accumulator.processed_samples = 50;
5827
5828 let result =
5829 build_integration_result(&state, &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None]);
5830 let absolute = &result
5831 .slot("proc_a@itg_a")
5832 .expect("absolute result slot should be present")
5833 .absolute;
5834
5835 assert_eq!(absolute.integral.result.re, F(3.5e-4));
5836 assert_eq!(absolute.integral.result.im, F(1.25e-4));
5837 assert_eq!(absolute.integral.neval, 200);
5838 assert_eq!(absolute.table_results[0].component, "|re|");
5839 assert_eq!(absolute.table_results[1].component, "|im|");
5840 assert!(
5841 absolute
5842 .table_results
5843 .iter()
5844 .all(|row| row.target_delta_sigma.is_none() && row.target_delta_percent.is_none())
5845 );
5846 assert_eq!(absolute.max_weight_info.len(), 2);
5847 assert!(
5848 absolute
5849 .max_weight_info
5850 .iter()
5851 .all(|entry| entry.sign == "+" && entry.max_eval > F(0.0))
5852 );
5853 let breakdown = absolute
5854 .grid_breakdown
5855 .re
5856 .as_ref()
5857 .expect("absolute shallow breakdown should be persisted");
5858 assert_eq!(breakdown.entries.len(), 2);
5859 assert_eq!(breakdown.entries[0].pdf, F(0.75));
5860 assert_eq!(breakdown.entries[0].value, F(2.0e-4));
5861 assert_eq!(breakdown.entries[1].pdf, F(0.25));
5862 assert_eq!(breakdown.entries[1].value, F(1.5e-4));
5863
5864 let json = serde_json::to_string(&result).expect("integration result should serialize");
5865 assert!(json.contains("\"absolute\""));
5866 let decoded: IntegrationResult =
5867 serde_json::from_str(&json).expect("integration result should deserialize");
5868 assert_eq!(
5869 decoded
5870 .slot("proc_a@itg_a")
5871 .expect("round-tripped slot should exist")
5872 .absolute
5873 .integral
5874 .result
5875 .re,
5876 F(3.5e-4)
5877 );
5878 }
5879
5880 #[test]
5881 fn integration_state_roundtrip_preserves_absolute_accumulators_and_summaries() {
5882 let mut state = make_discrete_integration_state();
5883 state.all_integrals[0].absolute_re.avg = F(4.5);
5884 state.slot_absolute_re_summaries[0]
5885 .as_mut()
5886 .expect("absolute summary should exist")
5887 .bins[1]
5888 .accumulator
5889 .avg = F(2.25);
5890
5891 let encoded = bincode::encode_to_vec(&state, bincode::config::standard())
5892 .expect("integration state should serialize");
5893 let (decoded, consumed): (IntegrationState, usize) =
5894 bincode::decode_from_slice(&encoded, bincode::config::standard())
5895 .expect("integration state should deserialize");
5896
5897 assert_eq!(consumed, encoded.len());
5898 assert_eq!(decoded.all_integrals[0].absolute_re.avg, F(4.5));
5899 assert_eq!(
5900 decoded.slot_absolute_re_summaries[0]
5901 .as_ref()
5902 .expect("round-tripped absolute summary should exist")
5903 .bins[1]
5904 .accumulator
5905 .avg,
5906 F(2.25)
5907 );
5908 assert!(
5909 decoded.slot_absolute_re_summaries[0]
5910 .as_ref()
5911 .expect("round-tripped absolute summary should exist")
5912 .bins
5913 .iter()
5914 .all(|bin| bin.sub_summary.is_none())
5915 );
5916 }
5917
5918 #[test]
5919 fn orientation_descriptions_use_colored_signs() {
5920 let mut state = make_discrete_integration_state();
5921 state.first_non_trivial_discrete_label = Some("orientation".to_string());
5922 state.first_non_trivial_discrete_bin_descriptions =
5923 Some(vec!["+-0".to_string(), "0++".to_string()]);
5924 for metadata in &mut state.slot_first_non_trivial_discrete_breakdown_metadata {
5925 if let Some(metadata) = metadata.as_mut() {
5926 metadata.axis_label = "orientation".to_string();
5927 metadata.bin_labels = vec!["+-0".to_string(), "0++".to_string()];
5928 }
5929 }
5930
5931 control::set_override(true);
5932 let expected_plus = "+".green().bold().to_string();
5933 let expected_minus = "-".red().bold().to_string();
5934
5935 let view_options = IntegrationStatusViewOptions {
5936 show_statistics: false,
5937 show_max_weight_details: false,
5938 show_top_discrete_grid: true,
5939 show_discrete_contributions_sum: false,
5940 contribution_sort: ContributionSortMode::Index,
5941 show_max_weight_info_for_discrete_bins: false,
5942 ..default_view_options()
5943 };
5944 let ansi = render_tabled::render_status_update(
5945 &build_status_update(
5946 StatusUpdateBuildRequest::new(
5947 IntegrationStatusKind::Iteration,
5948 &state,
5949 &[None, None],
5950 &view_options,
5951 )
5952 .with_timing(
5953 1,
5954 Duration::from_secs(0),
5955 Duration::from_secs(0),
5956 0,
5957 0,
5958 0,
5959 ),
5960 ),
5961 &tabled_options_for_view(&view_options),
5962 );
5963 control::set_override(false);
5964
5965 assert!(ansi.contains(&expected_plus), "{ansi}");
5966 assert!(ansi.contains(&expected_minus), "{ansi}");
5967 assert!(ansi.contains("0"), "{ansi}");
5968 }
5969
5970 #[test]
5971 fn mismatched_bin_descriptions_fall_back_to_indices_with_warning() {
5972 let (descriptions, warning) = coalesce_first_non_trivial_discrete_bin_descriptions(
5973 "graph",
5974 &[
5975 (
5976 "proc_a@itg_a".to_string(),
5977 vec!["GL0".to_string(), "GL1".to_string()],
5978 ),
5979 (
5980 "proc_b@itg_b".to_string(),
5981 vec!["GX0".to_string(), "GX1".to_string()],
5982 ),
5983 ],
5984 );
5985
5986 assert!(descriptions.is_none());
5987 let warning = warning.expect("mismatch should produce a warning");
5988 assert!(warning.contains("graph"), "{warning}");
5989 assert!(warning.contains("proc_a@itg_a"), "{warning}");
5990 assert!(warning.contains("proc_b@itg_b"), "{warning}");
5991 }
5992
5993 #[test]
5994 fn format_max_eval_sample_keeps_full_discrete_coordinates() {
5995 let axis_labels = vec!["graph".to_string(), "sampling channel".to_string()];
5996 let full_sample = Sample::Discrete(
5997 F(1.0),
5998 0,
5999 Some(Box::new(Sample::Discrete(
6000 F(1.0),
6001 1,
6002 Some(Box::new(Sample::Continuous(F(1.0), vec![F(0.25)]))),
6003 ))),
6004 );
6005 let nested_sample = Sample::Discrete(
6006 F(1.0),
6007 1,
6008 Some(Box::new(Sample::Continuous(F(1.0), vec![F(0.75)]))),
6009 );
6010
6011 assert_eq!(
6012 display::format_max_eval_sample(&full_sample, &axis_labels, &[]),
6013 "graph: 0, sampling channel: 1, xs: [ 2.5000000000000000e-01 ]"
6014 );
6015 assert_eq!(
6016 display::format_max_eval_sample(&nested_sample, &axis_labels, &[0]),
6017 "graph: 0, sampling channel: 1, xs: [ 7.5000000000000000e-01 ]"
6018 );
6019 }
6020
6021 #[test]
6022 fn format_max_eval_sample_wraps_long_coordinate_lists() {
6023 let axis_labels = vec!["graph".to_string(), "orientation".to_string()];
6024 let sample = Sample::Discrete(
6025 F(1.0),
6026 0,
6027 Some(Box::new(Sample::Discrete(
6028 F(1.0),
6029 9,
6030 Some(Box::new(Sample::Continuous(
6031 F(1.0),
6032 vec![
6033 F(0.125),
6034 F(0.25),
6035 F(0.375),
6036 F(0.5),
6037 F(0.625),
6038 F(0.75),
6039 F(0.875),
6040 ],
6041 ))),
6042 ))),
6043 );
6044
6045 assert_eq!(
6046 display::format_max_eval_sample(&sample, &axis_labels, &[]),
6047 "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 ]"
6048 );
6049 }
6050
6051 #[test]
6052 fn discrete_bin_prefixes_use_fixed_width_within_each_range() {
6053 assert_eq!(status_update::format_discrete_bin_prefix(2, 99), "#2: ");
6054 assert_eq!(status_update::format_discrete_bin_prefix(12, 99), "#12:");
6055 assert_eq!(status_update::format_discrete_bin_prefix(2, 999), "#2: ");
6056 assert_eq!(status_update::format_discrete_bin_prefix(12, 999), "#12: ");
6057 assert_eq!(status_update::format_discrete_bin_prefix(123, 999), "#123:");
6058 assert_eq!(
6059 status_update::format_discrete_bin_prefix(2, 9_999),
6060 "#2: "
6061 );
6062 assert_eq!(
6063 status_update::format_discrete_bin_prefix(1234, 9_999),
6064 "#1234:"
6065 );
6066 }
6067
6068 #[test]
6069 fn final_summary_omits_discrete_max_weight_block_when_disabled() {
6070 let state = make_discrete_integration_state();
6071 let rendered = render_saved_integration_summary(
6072 &state,
6073 &[None, None],
6074 &IntegrationStatusViewOptions {
6075 show_statistics: true,
6076 show_max_weight_details: true,
6077 show_top_discrete_grid: false,
6078 show_discrete_contributions_sum: false,
6079 contribution_sort: ContributionSortMode::Index,
6080 show_max_weight_info_for_discrete_bins: false,
6081 ..default_view_options()
6082 },
6083 &default_tabled_options(),
6084 );
6085
6086 assert!(rendered.contains("Maximum weight details"), "{rendered}");
6087 assert!(
6088 !rendered.contains("Maximum weight details by discrete bin"),
6089 "{rendered}"
6090 );
6091 assert!(
6092 !rendered.contains("# samples per iteration = 0"),
6093 "{rendered}"
6094 );
6095 }
6096
6097 #[test]
6098 fn results_output_summary_renders_tabled_workspace_rows() {
6099 let workspace = Path::new("/tmp/gl_workspace");
6100 let rendered = render_results_output_summary_table(Some(workspace), &[], &[], &[])
6101 .expect("workspace summary should render");
6102
6103 assert!(
6104 rendered.contains("Integration results emitted"),
6105 "{rendered}"
6106 );
6107 assert!(rendered.contains("type"), "{rendered}");
6108 assert!(rendered.contains("path"), "{rendered}");
6109 assert!(rendered.contains("workspace path"), "{rendered}");
6110 assert!(rendered.contains("/tmp/gl_workspace"), "{rendered}");
6111 assert!(rendered.contains("results"), "{rendered}");
6112 assert!(rendered.contains("integration_result.json"), "{rendered}");
6113 assert!(rendered.contains("├"), "{rendered}");
6114 assert!(!rendered.contains("iteration snapshots"), "{rendered}");
6115 assert!(!rendered.contains("iter_*"), "{rendered}");
6116 }
6117
6118 #[test]
6119 fn results_output_summary_skips_missing_observable_outputs() {
6120 let settings = RuntimeSettings::default();
6121 let slot = IntegrationSlot::new(
6122 SlotMeta {
6123 process_name: "proc".to_string(),
6124 integrand_name: "default".to_string(),
6125 },
6126 settings.clone(),
6127 Model::default(),
6128 Integrand::TestProbe(TestProbeIntegrand::new(settings, 3)),
6129 None,
6130 );
6131 let rendered = render_results_output_summary_table(
6132 None,
6133 &[slot],
6134 &[vec![PathBuf::from(
6135 "/tmp/definitely_missing_observables_final.json",
6136 )]],
6137 &[],
6138 );
6139
6140 assert!(rendered.is_none(), "{rendered:?}");
6141 }
6142
6143 #[test]
6144 fn results_output_summary_lists_all_emitted_observable_files() {
6145 let base = std::env::temp_dir().join(format!(
6146 "gammaloop_results_output_summary_{}",
6147 std::process::id()
6148 ));
6149 if base.exists() {
6150 let _ = fs::remove_dir_all(&base);
6151 }
6152 let workspace = base.join("workspace");
6153 let slot_dir = workspace.join("integrands").join("proc@default");
6154 fs::create_dir_all(&slot_dir).unwrap();
6155 fs::write(workspace_result_snapshot_path(&workspace), b"{}").unwrap();
6156 let json_path = slot_dir.join("observables_final.json");
6157 let hwu_path = slot_dir.join("observables_final.hwu");
6158 fs::write(&json_path, b"{}").unwrap();
6159 fs::write(&hwu_path, b"# hwu\n").unwrap();
6160
6161 let settings = RuntimeSettings::default();
6162 let slot = IntegrationSlot::new(
6163 SlotMeta {
6164 process_name: "proc".to_string(),
6165 integrand_name: "default".to_string(),
6166 },
6167 settings.clone(),
6168 Model::default(),
6169 Integrand::TestProbe(TestProbeIntegrand::new(settings, 3)),
6170 None,
6171 );
6172 let rendered = render_results_output_summary_table(
6173 Some(&workspace),
6174 &[slot],
6175 &[vec![hwu_path.clone(), json_path.clone()]],
6176 &[],
6177 )
6178 .expect("summary should render");
6179
6180 assert!(rendered.contains("proc@default (hwu)"), "{rendered}");
6181 assert!(rendered.contains("proc@default (json)"), "{rendered}");
6182 assert!(rendered.contains("observables_final.hwu"), "{rendered}");
6183 assert!(rendered.contains("observables_final.json"), "{rendered}");
6184
6185 fs::remove_dir_all(&base).unwrap();
6186 }
6187
6188 #[test]
6189 fn ratatui_overview_shows_eta_and_all_slot_metrics() {
6190 let state = make_integration_state();
6191 let view_options = IntegrationStatusViewOptions {
6192 show_statistics: true,
6193 show_max_weight_details: false,
6194 ..default_view_options()
6195 };
6196 let rendered = render_ratatui_update(
6197 StatusUpdateBuildRequest::new(
6198 IntegrationStatusKind::Live,
6199 &state,
6200 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
6201 &view_options,
6202 )
6203 .with_timing(
6204 4,
6205 Duration::from_secs(15),
6206 Duration::from_secs(10),
6207 25_000,
6208 125_000,
6209 125_000,
6210 )
6211 .with_live_progress(Some(status_update::LiveIterationProgress {
6212 completed_points: 25_000,
6213 target_points: 100_000,
6214 })),
6215 |_| {},
6216 );
6217
6218 assert!(rendered.contains("Iteration progress"), "{rendered}");
6219 assert!(rendered.contains("ETA 30s"), "{rendered}");
6220 assert!(rendered.contains("25.00K / 100.00K (25.0%)"), "{rendered}");
6221 assert!(rendered.contains("# samples total 125.00K"), "{rendered}");
6222 assert!(rendered.contains("#samples/s"), "{rendered}");
6223 assert!(
6224 rendered.contains("480µs /sample/core (4 cores)"),
6225 "{rendered}"
6226 );
6227 assert!(rendered.contains("/sample/core (4 cores)"), "{rendered}");
6228 assert!(rendered.contains("Integrands"), "{rendered}");
6229 assert!(rendered.contains("Focused integrand"), "{rendered}");
6230 assert!(rendered.contains("Results summary"), "{rendered}");
6231 assert!(
6232 rendered.contains("Integration statistics [global]"),
6233 "{rendered}"
6234 );
6235 assert!(rendered.contains("% err"), "{rendered}");
6236 assert!(rendered.contains("chi^2"), "{rendered}");
6237 assert!(rendered.contains("m.w.i"), "{rendered}");
6238 assert!(
6239 rendered.contains("Timing composition [global]"),
6240 "{rendered}"
6241 );
6242 assert!(rendered.contains("Precision mix [global]"), "{rendered}");
6243 }
6244
6245 #[test]
6246 fn live_preview_merges_and_finalizes_absolute_accumulators_without_mutating_saved_state() {
6247 let mut state = make_integration_state();
6248 state.all_integrals = vec![ComplexAccumulator::new(); state.slot_metas.len()];
6249 let slots = make_preview_test_slots(&state.slot_metas);
6250 let mut core_state = make_preview_test_core_state(&state);
6251 core_state.integrals[0].add_sample(Complex::new(F(-2.0), F(3.0)), None, F(2.0), None);
6252 core_state.integrals[0].add_sample(Complex::new(F(1.0), F(-4.0)), None, F(2.0), None);
6253
6254 let preview_state = build_preview_integration_state(
6255 &state,
6256 &slots,
6257 4,
6258 core_state.completed_points,
6259 1.5,
6260 &[core_state],
6261 );
6262
6263 assert_eq!(state.all_integrals[0].absolute_re.processed_samples, 0);
6264 assert_eq!(state.all_integrals[0].absolute_im.processed_samples, 0);
6265 assert_eq!(preview_state.all_integrals[0].absolute_re.avg, F(3.0));
6266 assert_eq!(preview_state.all_integrals[0].absolute_im.avg, F(7.0));
6267 assert_eq!(
6268 preview_state.all_integrals[0].absolute_re.processed_samples,
6269 2
6270 );
6271 assert_eq!(
6272 preview_state.all_integrals[0].absolute_im.processed_samples,
6273 2
6274 );
6275 }
6276
6277 #[test]
6278 fn live_status_updates_use_current_batch_statistics_in_bottom_panels() {
6279 let state = make_integration_state();
6280 let slots = make_preview_test_slots(&state.slot_metas);
6281 let core_state = make_preview_test_core_state(&state);
6282 let preview_state = build_preview_integration_state(
6283 &state,
6284 &slots,
6285 4,
6286 core_state.completed_points,
6287 1.5,
6288 &[core_state],
6289 );
6290 let view_options = IntegrationStatusViewOptions {
6291 show_statistics: true,
6292 show_max_weight_details: false,
6293 ..default_view_options()
6294 };
6295
6296 let initial_update = build_status_update(
6297 StatusUpdateBuildRequest::new(
6298 IntegrationStatusKind::Live,
6299 &state,
6300 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
6301 &view_options,
6302 )
6303 .with_timing(
6304 4,
6305 Duration::from_secs(1),
6306 Duration::from_millis(400),
6307 0,
6308 state.num_points,
6309 state.num_points,
6310 )
6311 .with_live_progress(Some(status_update::LiveIterationProgress {
6312 completed_points: 0,
6313 target_points: 100,
6314 })),
6315 );
6316 let preview_update = build_status_update(
6317 StatusUpdateBuildRequest::new(
6318 IntegrationStatusKind::Live,
6319 &preview_state,
6320 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
6321 &view_options,
6322 )
6323 .with_timing(
6324 4,
6325 Duration::from_millis(1500),
6326 Duration::from_millis(900),
6327 12,
6328 preview_state.num_points,
6329 preview_state.num_points,
6330 )
6331 .with_live_progress(Some(status_update::LiveIterationProgress {
6332 completed_points: 12,
6333 target_points: 100,
6334 })),
6335 );
6336
6337 let initial_statistics = initial_update
6338 .statistics
6339 .as_ref()
6340 .expect("statistics should be present");
6341 let preview_statistics = preview_update
6342 .statistics
6343 .as_ref()
6344 .expect("statistics should be present");
6345 let initial_snapshot = initial_statistics.global_snapshot();
6346 let preview_snapshot = preview_statistics.global_snapshot();
6347
6348 assert!(preview_snapshot.num_evals > initial_snapshot.num_evals);
6349 assert!(preview_snapshot.generated_event_count > initial_snapshot.generated_event_count);
6350 assert!(preview_snapshot.accepted_event_count > initial_snapshot.accepted_event_count);
6351
6352 let initial_timing_mix = initial_statistics
6353 .timing_mix_segments(status_update::StatisticsScope::Global)
6354 .iter()
6355 .map(|segment| segment.percentage)
6356 .collect_vec();
6357 let preview_timing_mix = preview_statistics
6358 .timing_mix_segments(status_update::StatisticsScope::Global)
6359 .iter()
6360 .map(|segment| segment.percentage)
6361 .collect_vec();
6362 assert_ne!(preview_timing_mix, initial_timing_mix);
6363
6364 let initial_precision_mix = initial_statistics
6365 .precision_mix_segments(status_update::StatisticsScope::Global)
6366 .iter()
6367 .map(|segment| segment.percentage)
6368 .collect_vec();
6369 let preview_precision_mix = preview_statistics
6370 .precision_mix_segments(status_update::StatisticsScope::Global)
6371 .iter()
6372 .map(|segment| segment.percentage)
6373 .collect_vec();
6374 assert_ne!(preview_precision_mix, initial_precision_mix);
6375 assert!(
6376 preview_precision_mix
6377 .iter()
6378 .any(|percentage| (*percentage - 50.0).abs() < f64::EPSILON),
6379 "{preview_precision_mix:?}"
6380 );
6381
6382 let preview_render = render_ratatui_update(
6383 StatusUpdateBuildRequest::new(
6384 IntegrationStatusKind::Live,
6385 &preview_state,
6386 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
6387 &view_options,
6388 )
6389 .with_timing(
6390 4,
6391 Duration::from_millis(1500),
6392 Duration::from_millis(900),
6393 12,
6394 preview_state.num_points,
6395 preview_state.num_points,
6396 )
6397 .with_live_progress(Some(status_update::LiveIterationProgress {
6398 completed_points: 12,
6399 target_points: 100,
6400 })),
6401 |_| {},
6402 );
6403 let preview_total_time = crate::utils::format_evaluation_time_from_f64(
6404 preview_snapshot.average_total_time_seconds,
6405 );
6406 assert!(
6407 preview_render.contains(&preview_total_time),
6408 "{preview_render}"
6409 );
6410 assert!(
6411 preview_render.contains("Precision mix [global]"),
6412 "{preview_render}"
6413 );
6414 assert!(
6415 preview_render.contains("Timing composition [global]"),
6416 "{preview_render}"
6417 );
6418 }
6419
6420 #[test]
6421 fn ratatui_statistics_panels_can_toggle_to_focused_slot_scope() {
6422 let state = make_integration_state();
6423 let view_options = IntegrationStatusViewOptions {
6424 show_statistics: true,
6425 show_max_weight_details: false,
6426 ..default_view_options()
6427 };
6428 let rendered = render_ratatui_update(
6429 StatusUpdateBuildRequest::new(
6430 IntegrationStatusKind::Live,
6431 &state,
6432 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
6433 &view_options,
6434 )
6435 .with_timing(
6436 4,
6437 Duration::from_secs(15),
6438 Duration::from_secs(10),
6439 25_000,
6440 125_000,
6441 125_000,
6442 ),
6443 |dashboard| {
6444 dashboard.focus_next_slot();
6445 dashboard.toggle_statistics_scope();
6446 },
6447 );
6448
6449 assert!(
6450 rendered.contains("Integration statistics [proc_b@itg_b]"),
6451 "{rendered}"
6452 );
6453 assert!(
6454 rendered.contains("Timing composition [proc_b@itg_b]"),
6455 "{rendered}"
6456 );
6457 assert!(
6458 rendered.contains("Precision mix [proc_b@itg_b]"),
6459 "{rendered}"
6460 );
6461 assert!(rendered.contains("Scope i"), "{rendered}");
6462 }
6463
6464 #[test]
6465 fn slot_scoped_statistics_rows_hide_shared_integrator_overhead() {
6466 let state = make_integration_state();
6467 let update = build_status_update(
6468 StatusUpdateBuildRequest::new(
6469 IntegrationStatusKind::Iteration,
6470 &state,
6471 &[None, None],
6472 &default_view_options(),
6473 )
6474 .with_timing(
6475 4,
6476 Duration::from_secs(1),
6477 Duration::from_secs(1),
6478 100_000,
6479 100_000,
6480 100_000,
6481 ),
6482 );
6483 let statistics = update.statistics.expect("statistics section should exist");
6484 let rows = statistics.table_rows(status_update::StatisticsScope::Slot(1));
6485 let integrator_entry = rows[2]
6486 .entries
6487 .last()
6488 .expect("integrator entry should be present");
6489
6490 assert_eq!(integrator_entry.label.to_plain_string(), "integrator");
6491 assert_eq!(integrator_entry.value.to_plain_string(), "N/A");
6492 }
6493
6494 #[test]
6495 fn ratatui_overview_chart_phase_toggle_updates_convergence_label() {
6496 let state = make_integration_state();
6497 let view_options = IntegrationStatusViewOptions {
6498 show_statistics: false,
6499 show_max_weight_details: false,
6500 ..default_view_options()
6501 };
6502 let rendered = render_ratatui_update(
6503 StatusUpdateBuildRequest::new(
6504 IntegrationStatusKind::Live,
6505 &state,
6506 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
6507 &view_options,
6508 )
6509 .with_timing(
6510 4,
6511 Duration::from_secs(15),
6512 Duration::from_secs(10),
6513 25_000,
6514 125_000,
6515 125_000,
6516 )
6517 .with_live_progress(Some(status_update::LiveIterationProgress {
6518 completed_points: 25_000,
6519 target_points: 100_000,
6520 })),
6521 |dashboard| dashboard.toggle_chart_component(),
6522 );
6523
6524 assert!(
6525 rendered.contains("Convergence ⟨I⟩ : im (not selected for training)"),
6526 "{rendered}"
6527 );
6528 }
6529
6530 #[test]
6531 fn ratatui_integrand_targets_are_trimmed_for_display() {
6532 let state = make_integration_state();
6533 let view_options = IntegrationStatusViewOptions {
6534 show_statistics: false,
6535 show_max_weight_details: false,
6536 ..default_view_options()
6537 };
6538 let rendered = render_ratatui_update(
6539 StatusUpdateBuildRequest::new(
6540 IntegrationStatusKind::Live,
6541 &state,
6542 &[
6543 Some(Complex::new(
6544 F(7.600000000000001e-6),
6545 F(7.430000000000004e-5),
6546 )),
6547 Some(Complex::new(F(6.629999999999999e-5), F(0.0))),
6548 ],
6549 &view_options,
6550 )
6551 .with_timing(
6552 4,
6553 Duration::from_secs(15),
6554 Duration::from_secs(10),
6555 25_000,
6556 125_000,
6557 125_000,
6558 )
6559 .with_live_progress(Some(status_update::LiveIterationProgress {
6560 completed_points: 25_000,
6561 target_points: 100_000,
6562 })),
6563 |_| {},
6564 );
6565
6566 assert!(rendered.contains("trgt"), "{rendered}");
6567 assert!(rendered.contains("+7.6e-6"), "{rendered}");
6568 assert!(rendered.contains("+7.43e-5"), "{rendered}");
6569 assert!(rendered.contains("+6.63e-5"), "{rendered}");
6570 assert!(rendered.contains("+0e0"), "{rendered}");
6571 }
6572
6573 #[test]
6574 fn ratatui_overview_shows_eta_to_target_when_configured() {
6575 let state = make_integration_state();
6576 let view_options = IntegrationStatusViewOptions {
6577 target_relative_accuracy: Some(0.05),
6578 show_statistics: false,
6579 show_max_weight_details: false,
6580 ..default_view_options()
6581 };
6582 let rendered = render_ratatui_update(
6583 StatusUpdateBuildRequest::new(
6584 IntegrationStatusKind::Live,
6585 &state,
6586 &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
6587 &view_options,
6588 )
6589 .with_timing(
6590 4,
6591 Duration::from_secs(25),
6592 Duration::from_secs(10),
6593 25_000,
6594 125_000,
6595 125_000,
6596 )
6597 .with_live_progress(Some(status_update::LiveIterationProgress {
6598 completed_points: 25_000,
6599 target_points: 100_000,
6600 })),
6601 |_| {},
6602 );
6603
6604 assert!(rendered.contains("ETA to ⟨I⟩ target"), "{rendered}");
6605 assert!(rendered.contains("(% err <= 5%)"), "{rendered}");
6606 }
6607
6608 #[test]
6609 fn eta_to_target_specification_formats_relative_and_absolute_targets() {
6610 let state = make_integration_state();
6611 let relative_view_options = IntegrationStatusViewOptions {
6612 target_relative_accuracy: Some(0.05),
6613 ..default_view_options()
6614 };
6615 let update = build_status_update(
6616 StatusUpdateBuildRequest::new(
6617 IntegrationStatusKind::Live,
6618 &state,
6619 &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
6620 &relative_view_options,
6621 )
6622 .with_timing(
6623 4,
6624 Duration::from_secs(25),
6625 Duration::from_secs(10),
6626 25_000,
6627 125_000,
6628 125_000,
6629 )
6630 .with_live_progress(Some(status_update::LiveIterationProgress {
6631 completed_points: 25_000,
6632 target_points: 100_000,
6633 })),
6634 );
6635 assert_eq!(
6636 update.meta.eta_to_target_specification(),
6637 Some("% err <= 5%")
6638 );
6639
6640 let relative_scientific_view_options = IntegrationStatusViewOptions {
6641 target_relative_accuracy: Some(1.0e-6),
6642 ..default_view_options()
6643 };
6644 let update = build_status_update(
6645 StatusUpdateBuildRequest::new(
6646 IntegrationStatusKind::Live,
6647 &state,
6648 &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
6649 &relative_scientific_view_options,
6650 )
6651 .with_timing(
6652 4,
6653 Duration::from_secs(25),
6654 Duration::from_secs(10),
6655 25_000,
6656 125_000,
6657 125_000,
6658 )
6659 .with_live_progress(Some(status_update::LiveIterationProgress {
6660 completed_points: 25_000,
6661 target_points: 100_000,
6662 })),
6663 );
6664 assert_eq!(
6665 update.meta.eta_to_target_specification(),
6666 Some("% err <= 1e-4%")
6667 );
6668
6669 let absolute_view_options = IntegrationStatusViewOptions {
6670 target_absolute_accuracy: Some(1.0e-6),
6671 ..default_view_options()
6672 };
6673 let update = build_status_update(
6674 StatusUpdateBuildRequest::new(
6675 IntegrationStatusKind::Live,
6676 &state,
6677 &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
6678 &absolute_view_options,
6679 )
6680 .with_timing(
6681 4,
6682 Duration::from_secs(25),
6683 Duration::from_secs(10),
6684 25_000,
6685 125_000,
6686 125_000,
6687 )
6688 .with_live_progress(Some(status_update::LiveIterationProgress {
6689 completed_points: 25_000,
6690 target_points: 100_000,
6691 })),
6692 );
6693 assert_eq!(
6694 update.meta.eta_to_target_specification(),
6695 Some("err <= 1e-6")
6696 );
6697
6698 let combined_view_options = IntegrationStatusViewOptions {
6699 target_relative_accuracy: Some(0.05),
6700 target_absolute_accuracy: Some(1.0e-6),
6701 ..default_view_options()
6702 };
6703 let update = build_status_update(
6704 StatusUpdateBuildRequest::new(
6705 IntegrationStatusKind::Live,
6706 &state,
6707 &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
6708 &combined_view_options,
6709 )
6710 .with_timing(
6711 4,
6712 Duration::from_secs(25),
6713 Duration::from_secs(10),
6714 25_000,
6715 125_000,
6716 125_000,
6717 )
6718 .with_live_progress(Some(status_update::LiveIterationProgress {
6719 completed_points: 25_000,
6720 target_points: 100_000,
6721 })),
6722 );
6723 assert_eq!(
6724 update.meta.eta_to_target_specification(),
6725 Some("% err <= 5% or err <= 1e-6")
6726 );
6727 }
6728
6729 #[test]
6730 fn target_accuracy_status_uses_either_absolute_or_relative_condition() {
6731 let state = make_integration_state();
6732
6733 let reached_by_absolute = status_update::evaluate_target_accuracy(
6734 &state,
6735 100_000,
6736 Duration::from_secs(10),
6737 &[None, None],
6738 IntegrationStatusPhaseDisplay::Real,
6739 None,
6740 Some(1.0e-4),
6741 );
6742 assert!(reached_by_absolute.is_reached());
6743
6744 let reached_by_relative = status_update::evaluate_target_accuracy(
6745 &state,
6746 100_000,
6747 Duration::from_secs(10),
6748 &[Some(Complex::new(F(1.0e-3), F(0.0))), None],
6749 IntegrationStatusPhaseDisplay::Real,
6750 Some(0.1),
6751 None,
6752 );
6753 assert!(reached_by_relative.is_reached());
6754 }
6755
6756 #[test]
6757 fn target_accuracy_status_requires_all_slots_to_reach_relative_target() {
6758 let mut state = make_integration_state();
6759 state.all_integrals[0].re.avg = F(1.0);
6760 state.all_integrals[0].re.err = F(1.0e-5);
6761 state.all_integrals[1].re.avg = F(1.0);
6762 state.all_integrals[1].re.err = F(2.0e-3);
6763
6764 let status = status_update::evaluate_target_accuracy(
6765 &state,
6766 100_000,
6767 Duration::from_secs(10),
6768 &[None, None],
6769 IntegrationStatusPhaseDisplay::Real,
6770 Some(1.0e-3),
6771 None,
6772 );
6773
6774 assert!(!status.is_reached());
6775 assert!(!status.relative_reached);
6776 assert!(status.eta_to_target.is_some());
6777 }
6778
6779 #[test]
6780 fn target_accuracy_status_requires_all_displayed_components_to_reach_target() {
6781 let mut state = make_integration_state();
6782 state.all_integrals[0].re.avg = F(1.0);
6783 state.all_integrals[0].re.err = F(1.0e-5);
6784 state.all_integrals[0].im.avg = F(1.0);
6785 state.all_integrals[0].im.err = F(2.0e-3);
6786
6787 let status = status_update::evaluate_target_accuracy(
6788 &state,
6789 100_000,
6790 Duration::from_secs(10),
6791 &[Some(Complex::new(F(1.0), F(1.0))), None],
6792 IntegrationStatusPhaseDisplay::Both,
6793 Some(1.0e-3),
6794 None,
6795 );
6796
6797 assert!(!status.is_reached());
6798 assert!(!status.relative_reached);
6799 assert!(status.eta_to_target.is_some());
6800 }
6801
6802 #[test]
6803 fn target_accuracy_status_reports_infinite_eta_for_unreachable_absolute_target() {
6804 let state = make_integration_state();
6805 let status = status_update::evaluate_target_accuracy(
6806 &state,
6807 100_000,
6808 Duration::from_secs(10),
6809 &[None, None],
6810 IntegrationStatusPhaseDisplay::Real,
6811 None,
6812 Some(1.0e-99),
6813 );
6814
6815 assert!(!status.is_reached());
6816 assert_eq!(status.eta_to_target, Some(Duration::MAX));
6817 }
6818
6819 #[test]
6820 fn target_accuracy_status_prefers_finite_relative_eta_over_infinite_absolute_eta() {
6821 let state = make_integration_state();
6822 let status = status_update::evaluate_target_accuracy(
6823 &state,
6824 100_000,
6825 Duration::from_secs(10),
6826 &[None, None],
6827 IntegrationStatusPhaseDisplay::Real,
6828 Some(1.0e-3),
6829 Some(1.0e-99),
6830 );
6831
6832 assert!(!status.is_reached());
6833 assert!(status.eta_to_target.is_some());
6834 assert_ne!(status.eta_to_target, Some(Duration::MAX));
6835 }
6836
6837 #[test]
6838 fn ratatui_overview_displays_infinite_eta_to_target() {
6839 let state = make_integration_state();
6840 let view_options = IntegrationStatusViewOptions {
6841 target_absolute_accuracy: Some(1.0e-99),
6842 show_statistics: false,
6843 show_max_weight_details: false,
6844 ..default_view_options()
6845 };
6846 let rendered = render_ratatui_update(
6847 StatusUpdateBuildRequest::new(
6848 IntegrationStatusKind::Live,
6849 &state,
6850 &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
6851 &view_options,
6852 )
6853 .with_timing(
6854 4,
6855 Duration::from_secs(25),
6856 Duration::from_secs(10),
6857 25_000,
6858 125_000,
6859 125_000,
6860 )
6861 .with_live_progress(Some(status_update::LiveIterationProgress {
6862 completed_points: 25_000,
6863 target_points: 100_000,
6864 })),
6865 |_| {},
6866 );
6867
6868 assert!(rendered.contains("ETA to ⟨I⟩ target"), "{rendered}");
6869 assert!(rendered.contains("∞"), "{rendered}");
6870 }
6871
6872 #[test]
6873 fn ratatui_overview_prefers_finite_relative_eta_over_infinite_absolute_eta() {
6874 let state = make_integration_state();
6875 let view_options = IntegrationStatusViewOptions {
6876 target_relative_accuracy: Some(1.0e-3),
6877 target_absolute_accuracy: Some(1.0e-99),
6878 show_statistics: false,
6879 show_max_weight_details: false,
6880 ..default_view_options()
6881 };
6882 let rendered = render_ratatui_update(
6883 StatusUpdateBuildRequest::new(
6884 IntegrationStatusKind::Live,
6885 &state,
6886 &[Some(Complex::new(F(1.0e-4), F(0.0))), None],
6887 &view_options,
6888 )
6889 .with_timing(
6890 4,
6891 Duration::from_secs(25),
6892 Duration::from_secs(10),
6893 25_000,
6894 125_000,
6895 125_000,
6896 )
6897 .with_live_progress(Some(status_update::LiveIterationProgress {
6898 completed_points: 25_000,
6899 target_points: 100_000,
6900 })),
6901 |_| {},
6902 );
6903
6904 assert!(rendered.contains("ETA to ⟨I⟩ target"), "{rendered}");
6905 assert!(!rendered.contains("∞"), "{rendered}");
6906 }
6907
6908 #[test]
6909 fn ratatui_recent_history_window_tracks_current_iteration_points() {
6910 let mut dashboard = RatatuiDashboardState::new();
6911 let mut state = make_integration_state();
6912 let view_options = IntegrationStatusViewOptions {
6913 show_statistics: false,
6914 show_max_weight_details: false,
6915 ..default_view_options()
6916 };
6917
6918 state.iter = 1;
6919 dashboard.update(build_status_update(
6920 StatusUpdateBuildRequest::new(
6921 IntegrationStatusKind::Live,
6922 &state,
6923 &[None, None],
6924 &view_options,
6925 )
6926 .with_timing(
6927 4,
6928 Duration::from_secs(10),
6929 Duration::from_secs(10),
6930 500_000,
6931 1_000_000,
6932 1_000_000,
6933 )
6934 .with_live_progress(Some(status_update::LiveIterationProgress {
6935 completed_points: 500_000,
6936 target_points: 1_000_000,
6937 })),
6938 ));
6939
6940 state.iter = 2;
6941 dashboard.update(build_status_update(
6942 StatusUpdateBuildRequest::new(
6943 IntegrationStatusKind::Live,
6944 &state,
6945 &[None, None],
6946 &view_options,
6947 )
6948 .with_timing(
6949 4,
6950 Duration::from_secs(20),
6951 Duration::from_secs(5),
6952 0,
6953 2_000_000,
6954 2_000_000,
6955 )
6956 .with_live_progress(Some(status_update::LiveIterationProgress {
6957 completed_points: 0,
6958 target_points: 1_000_000,
6959 })),
6960 ));
6961 dashboard.update(build_status_update(
6962 StatusUpdateBuildRequest::new(
6963 IntegrationStatusKind::Live,
6964 &state,
6965 &[None, None],
6966 &view_options,
6967 )
6968 .with_timing(
6969 4,
6970 Duration::from_secs(25),
6971 Duration::from_secs(10),
6972 200_000,
6973 2_200_000,
6974 2_200_000,
6975 )
6976 .with_live_progress(Some(status_update::LiveIterationProgress {
6977 completed_points: 200_000,
6978 target_points: 1_000_000,
6979 })),
6980 ));
6981
6982 dashboard.toggle_chart_history_window();
6983 for _ in 0..5 {
6984 dashboard.narrow_chart_history_window();
6985 }
6986
6987 assert_eq!(
6988 dashboard.visible_history_sample_bounds(),
6989 Some((2_000_000, 2_200_000))
6990 );
6991 }
6992
6993 #[test]
6994 fn ratatui_full_history_preserves_origin_after_compaction() {
6995 let mut dashboard = RatatuiDashboardState::new();
6996 let mut state = make_integration_state();
6997 let view_options = IntegrationStatusViewOptions {
6998 show_statistics: false,
6999 show_max_weight_details: false,
7000 ..default_view_options()
7001 };
7002
7003 for index in 0..4_300 {
7004 state.iter = 1 + index / 40;
7005 let total_points = index * 1_000;
7006 dashboard.update(build_status_update(
7007 StatusUpdateBuildRequest::new(
7008 IntegrationStatusKind::Live,
7009 &state,
7010 &[None, None],
7011 &view_options,
7012 )
7013 .with_timing(
7014 4,
7015 Duration::from_secs(index as u64),
7016 Duration::from_secs(10),
7017 total_points % 1_000_000,
7018 total_points,
7019 total_points,
7020 )
7021 .with_live_progress(Some(status_update::LiveIterationProgress {
7022 completed_points: total_points % 1_000_000,
7023 target_points: 1_000_000,
7024 })),
7025 ));
7026 }
7027
7028 dashboard.toggle_chart_history_window();
7029 dashboard.toggle_chart_history_window();
7030
7031 assert_eq!(
7032 dashboard.visible_history_sample_bounds(),
7033 Some((0, 4_299_000))
7034 );
7035 }
7036
7037 #[test]
7038 fn ratatui_convergence_title_shows_active_y_span() {
7039 let state = make_integration_state();
7040 let view_options = IntegrationStatusViewOptions {
7041 show_statistics: false,
7042 show_max_weight_details: false,
7043 ..default_view_options()
7044 };
7045 let rendered = render_ratatui_update(
7046 StatusUpdateBuildRequest::new(
7047 IntegrationStatusKind::Live,
7048 &state,
7049 &[None, None],
7050 &view_options,
7051 )
7052 .with_timing(
7053 4,
7054 Duration::from_secs(15),
7055 Duration::from_secs(10),
7056 25_000,
7057 125_000,
7058 125_000,
7059 )
7060 .with_live_progress(Some(status_update::LiveIterationProgress {
7061 completed_points: 25_000,
7062 target_points: 100_000,
7063 })),
7064 |dashboard| dashboard.widen_chart_y_sigma_span(),
7065 );
7066
7067 assert!(rendered.contains("y ±5σ"), "{rendered}");
7068 }
7069
7070 #[test]
7071 fn ratatui_convergence_axis_uses_small_dimensionful_uncertainty_verbatim() {
7072 let mut state = make_integration_state();
7073 state.all_integrals[0] = make_accumulator(-2.12e-15, 5.2e-16, 1.0, 1.0e-15, 1.0e-16, 1.0);
7074 let view_options = IntegrationStatusViewOptions {
7075 show_statistics: false,
7076 show_max_weight_details: false,
7077 ..default_view_options()
7078 };
7079 let rendered = render_ratatui_update(
7080 StatusUpdateBuildRequest::new(
7081 IntegrationStatusKind::Live,
7082 &state,
7083 &[None, None],
7084 &view_options,
7085 )
7086 .with_timing(
7087 4,
7088 Duration::from_secs(15),
7089 Duration::from_secs(10),
7090 25_000,
7091 125_000,
7092 125_000,
7093 )
7094 .with_live_progress(Some(status_update::LiveIterationProgress {
7095 completed_points: 25_000,
7096 target_points: 100_000,
7097 })),
7098 |_| {},
7099 );
7100
7101 assert!(rendered.contains("-4.200e-15"), "{rendered}");
7102 assert!(rendered.contains("-4.000e-17"), "{rendered}");
7103 assert!(!rendered.contains("e-12"), "{rendered}");
7104 }
7105
7106 #[test]
7107 fn ratatui_convergence_waits_for_non_zero_uncertainty() {
7108 let mut state = make_integration_state();
7109 state.all_integrals[0] = make_accumulator(1.0e-15, 0.0, 1.0, 1.0e-15, 0.0, 1.0);
7110 let view_options = IntegrationStatusViewOptions {
7111 show_statistics: false,
7112 show_max_weight_details: false,
7113 ..default_view_options()
7114 };
7115 let rendered = render_ratatui_update(
7116 StatusUpdateBuildRequest::new(
7117 IntegrationStatusKind::Live,
7118 &state,
7119 &[None, None],
7120 &view_options,
7121 )
7122 .with_timing(
7123 4,
7124 Duration::from_secs(15),
7125 Duration::from_secs(10),
7126 25_000,
7127 125_000,
7128 125_000,
7129 )
7130 .with_live_progress(Some(status_update::LiveIterationProgress {
7131 completed_points: 25_000,
7132 target_points: 100_000,
7133 })),
7134 |_| {},
7135 );
7136
7137 assert!(
7138 rendered.contains("Waiting for a finite central value and non-zero MC uncertainty"),
7139 "{rendered}"
7140 );
7141 }
7142
7143 #[test]
7144 fn ratatui_discrete_tab_renders_selected_bin_detail() {
7145 let state = make_discrete_integration_state();
7146 let view_options = IntegrationStatusViewOptions {
7147 show_statistics: false,
7148 show_max_weight_details: false,
7149 show_top_discrete_grid: false,
7150 show_discrete_contributions_sum: false,
7151 show_max_weight_info_for_discrete_bins: false,
7152 ..default_view_options()
7153 };
7154 let rendered = render_ratatui_update(
7155 StatusUpdateBuildRequest::new(
7156 IntegrationStatusKind::Iteration,
7157 &state,
7158 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
7159 &view_options,
7160 )
7161 .with_timing(
7162 4,
7163 Duration::from_secs(12),
7164 Duration::from_secs(12),
7165 110_000,
7166 210_000,
7167 210_000,
7168 ),
7169 |dashboard| dashboard.select_tab(1),
7170 );
7171
7172 assert!(rendered.contains("Selected bin"), "{rendered}");
7173 assert!(
7174 rendered.contains("Discrete bins ⟨I⟩ for focused integrand"),
7175 "{rendered}"
7176 );
7177 assert!(rendered.contains("sample %"), "{rendered}");
7178 assert!(rendered.contains("GL0"), "{rendered}");
7179 assert!(rendered.contains("GL1"), "{rendered}");
7180 assert!(rendered.contains("pdf"), "{rendered}");
7181 assert!(rendered.contains("Per integrand details"), "{rendered}");
7182 assert!(rendered.contains("# samples"), "{rendered}");
7183 }
7184
7185 #[test]
7186 fn ratatui_single_graph_subset_keeps_the_graph_row() {
7187 let state = make_single_graph_discrete_integration_state();
7188 let view_options = IntegrationStatusViewOptions {
7189 show_statistics: false,
7190 show_max_weight_details: false,
7191 ..default_view_options()
7192 };
7193 let rendered = render_ratatui_update(
7194 StatusUpdateBuildRequest::new(
7195 IntegrationStatusKind::Iteration,
7196 &state,
7197 &[None, None],
7198 &view_options,
7199 ),
7200 |dashboard| dashboard.select_tab(1),
7201 );
7202
7203 assert!(
7204 rendered.contains("Discrete bins ⟨I⟩ for focused integrand"),
7205 "{rendered}"
7206 );
7207 assert!(rendered.contains("GL22"), "{rendered}");
7208 assert!(!rendered.contains("GL1"), "{rendered}");
7209 }
7210
7211 #[test]
7212 fn ratatui_max_weight_tab_preserves_full_scientific_exponent() {
7213 let mut state = make_discrete_integration_state();
7214 let max_eval = 3.8477312059601813_f64;
7215 let expected = format!("{:+.16e}", max_eval);
7216 state.all_integrals[0].re.max_eval_positive = F(max_eval);
7217 state.all_integrals[0].re.max_eval_positive_xs = Some(Sample::Discrete(
7218 F(1.0),
7219 0,
7220 Some(Box::new(Sample::Continuous(
7221 F(1.0),
7222 vec![F(0.4502578156709256)],
7223 ))),
7224 ));
7225
7226 let view_options = IntegrationStatusViewOptions {
7227 show_statistics: false,
7228 ..default_view_options()
7229 };
7230 let rendered = render_ratatui_update(
7231 StatusUpdateBuildRequest::new(
7232 IntegrationStatusKind::Iteration,
7233 &state,
7234 &[Some(Complex::new(F(1.0e-4), F(2.0e-5))), None],
7235 &view_options,
7236 )
7237 .with_timing(
7238 4,
7239 Duration::from_secs(12),
7240 Duration::from_secs(12),
7241 110_000,
7242 210_000,
7243 210_000,
7244 ),
7245 |dashboard| dashboard.select_tab(2),
7246 );
7247
7248 assert!(rendered.contains(&expected), "{rendered}");
7249 assert!(rendered.contains("graph: 0, xs: ["), "{rendered}");
7250 }
7251
7252 #[test]
7253 fn target_accuracy_status_treats_zero_relative_reference_as_inactive() {
7254 let state = make_integration_state();
7255 let targets = vec![Some(Complex::new(F(0.0), F(0.0))); state.all_integrals.len()];
7256 let status = status_update::evaluate_target_accuracy(
7257 &state,
7258 100_000,
7259 Duration::from_secs(10),
7260 &targets,
7261 IntegrationStatusPhaseDisplay::Real,
7262 Some(0.05),
7263 None,
7264 );
7265
7266 assert!(!status.relative_reached);
7267 assert!(!status.absolute_reached);
7268 assert_eq!(status.eta_to_target, None);
7269
7270 let status = status_update::evaluate_target_accuracy(
7272 &state,
7273 100_000,
7274 Duration::from_secs(10),
7275 &[Some(Complex::new(F(0.0), F(0.0))), None],
7276 IntegrationStatusPhaseDisplay::Real,
7277 Some(0.05),
7278 None,
7279 );
7280 assert!(status.eta_to_target.is_some());
7281 }
7282}
7283
7284#[test]
7285fn test_threading() {
7286 use symbolica::numerical_integration::ContinuousGrid;
7287
7288 fn test_fn(x: f64) -> f64 {
7289 x * x * (x * 6.).sin()
7290 }
7291
7292 let mut acc_1 = StatisticsAccumulator::<f64>::new();
7293 let mut acc_2 = StatisticsAccumulator::<f64>::new();
7294
7295 let samples_per_sample = 4;
7296 let samples_per_iter = 100000;
7297 let n_iter = 10;
7298
7299 let mut rng = MonteCarloRng::new(42, 0);
7300
7301 let mut grid = Grid::<f64>::Continuous(ContinuousGrid::new(1, 64, 100, None, false).unwrap());
7302
7303 for _i_iter in 0..n_iter {
7304 let mut multiplice_accs = vec![StatisticsAccumulator::<f64>::new(); samples_per_sample];
7305 for _i_sample in 0..samples_per_iter {
7306 let n_samples = (0..samples_per_sample)
7307 .map(|_| {
7308 let mut sample = Sample::new();
7309 grid.sample(&mut rng, &mut sample);
7310 sample
7311 })
7312 .collect_vec();
7313
7314 let n_evals = n_samples
7315 .iter()
7316 .map(|s| match s {
7317 Sample::Continuous(_, xs) => test_fn(xs[0]),
7318 _ => unreachable!(),
7319 })
7320 .collect_vec();
7321
7322 for (i_eval, (sample, eval)) in n_samples.iter().zip(&n_evals).enumerate() {
7323 acc_1.add_sample(eval * sample.get_weight(), Some(sample));
7324 multiplice_accs[i_eval].add_sample(eval * sample.get_weight(), Some(sample));
7325 }
7326 }
7327
7328 for acc in multiplice_accs {
7329 acc_2.merge_samples(&mut acc.clone());
7330 }
7331 acc_1.update_iter(false);
7332 acc_2.update_iter(false);
7333 }
7334
7335 println!("acc1: {:?}", acc_1);
7336 println!("acc2: {:?}", acc_2);
7337}