use crate::{DataSource, Date, Diagnostic, FailureMode};
use probl_engine::report::{self, GroupResult, Status};
use probl_engine::value::Value;
use probl_sema::ir;
use probl_syntax::SourceFile;
use std::ops::Range;
use std::sync::Arc;
pub struct Outcome {
program: Arc<ir::Program>,
results: Vec<report::ReportResult>,
format: report::Format,
text: String,
pub(crate) printed: Vec<String>,
reports: Vec<Report>,
evidence: Option<Evidence>,
unresolved: Unresolved,
sampling: Option<Sampling>,
stats: Stats,
data: Vec<DataSource>,
today: Option<Date>,
failure_mode: FailureMode,
failures: Vec<Failure>,
failed_share: Option<f64>,
finished: bool,
}
impl Outcome {
pub(crate) fn new(
mut outcome: probl_engine::Outcome,
program: &Arc<ir::Program>,
file: &Arc<SourceFile>,
) -> Outcome {
let reports = program
.reports
.iter()
.zip(&outcome.results)
.map(|(site, result)| Report {
label: site.label.clone(),
groups: result
.groups
.iter()
.map(|g| Group::new(g, site.key_label.is_some()))
.collect(),
})
.collect();
let runs = outcome.sample.as_ref().map(|s| s.runs);
let (failures, failed_share) = failures(&outcome, file);
let mut text = std::mem::take(&mut outcome.output);
if !failures.is_empty() {
text.truncate(text.trim_end_matches('\n').len());
text.push_str("\n\n");
text.push_str(&failure_text(&failures, outcome.format));
text.push('\n');
}
let unresolved = match runs {
Some(runs) => outcome.unresolved.scale(1.0 / runs as f64),
None => outcome.unresolved,
};
Outcome {
evidence: evidence(&outcome),
unresolved: Unresolved(unresolved),
sampling: outcome.sample.as_ref().map(|s| Sampling {
runs: s.runs,
seed: s.seed,
effective_runs: s.effective,
}),
stats: Stats::new(&outcome, file),
data: outcome.data.iter().map(DataSource::new).collect(),
today: outcome.today.map(Date::from_days),
failure_mode: FailureMode::new(outcome.on_error),
failures,
failed_share,
finished: !outcome.finished.is_zero(),
text,
printed: Vec::new(),
reports,
results: outcome.results,
format: outcome.format,
program: program.clone(),
}
}
pub fn text(&self) -> &str {
&self.text
}
pub fn render(&self, reports: Range<usize>) -> String {
let end = reports.end.min(self.results.len());
let start = reports.start.min(end);
report::render_results(
&self.program.reports[start..end],
&self.results[start..end],
self.format,
)
}
pub fn reports(&self) -> &[Report] {
&self.reports
}
pub fn report(&self, label: &str) -> Option<&Report> {
self.reports.iter().find(|r| r.label == label)
}
pub fn printed(&self) -> &[String] {
&self.printed
}
pub fn evidence(&self) -> Option<&Evidence> {
self.evidence.as_ref()
}
pub fn unresolved(&self) -> &Unresolved {
&self.unresolved
}
pub fn sampling(&self) -> Option<Sampling> {
self.sampling
}
pub fn stats(&self) -> &Stats {
&self.stats
}
pub fn data(&self) -> &[DataSource] {
&self.data
}
pub fn today(&self) -> Option<Date> {
self.today
}
pub fn failure_mode(&self) -> FailureMode {
self.failure_mode
}
pub fn failures(&self) -> &[Failure] {
&self.failures
}
pub fn failed_share(&self) -> Option<f64> {
self.failed_share
}
pub fn finished(&self) -> bool {
self.finished
}
}
#[derive(Clone, Debug)]
pub struct Failure {
diagnostic: Diagnostic,
share: Option<f64>,
standard_error: Option<f64>,
weight: f64,
runs: Option<u64>,
first_run: Option<u64>,
}
impl Failure {
pub fn diagnostic(&self) -> &Diagnostic {
&self.diagnostic
}
pub fn share(&self) -> Option<f64> {
self.share
}
pub fn standard_error(&self) -> Option<f64> {
self.standard_error
}
pub fn weight(&self) -> f64 {
self.weight
}
pub fn runs(&self) -> Option<u64> {
self.runs
}
pub fn first_run(&self) -> Option<u64> {
self.first_run
}
}
const LISTED_FAILURES: usize = 10;
fn failures(outcome: &probl_engine::Outcome, file: &Arc<SourceFile>) -> (Vec<Failure>, Option<f64>) {
let all = &outcome.failures;
let comparable = !all.before_evidence;
let total = outcome.finished + all.weight;
let total_squares = outcome.sample.as_ref().map(|s| s.squares + all.squares);
let share_of = |w: probl_engine::Weight| (comparable && !total.is_zero()).then(|| w.ratio(total));
let pct = |p: f64| report::pct(p, outcome.format);
let others = match (outcome.sample.is_some(), outcome.finished.is_zero()) {
(false, false) => "the other worlds finished",
(false, true) => "no world finished",
(true, false) => "the other runs finished",
(true, true) => "no run finished",
};
let failures = all
.groups
.iter()
.map(|g| {
let share = share_of(g.weight);
let standard_error = match (share, total_squares) {
(Some(p), Some(all_squares)) => {
let spread =
g.squares.scale((1.0 - p) * (1.0 - p)) + all_squares.saturating_sub(g.squares).scale(p * p);
Some(spread.ratio(total * total).sqrt())
}
_ => None,
};
let note = match (&outcome.sample, share) {
(Some(s), _) => {
let first = g
.first_run
.map_or(String::new(), |r| format!(", first in run {}", r + 1));
format!(
"it failed in {} of {} runs{first}; {others}",
report::thousands(g.runs as i64),
report::thousands(s.runs as i64)
)
}
(None, Some(p)) => format!("it failed in {} of the worlds; {others}", pct(p)),
(None, None) => format!("it failed in worlds that hadn't met all the evidence yet; {others}"),
};
let error = g.error.clone().with_note(note);
Failure {
diagnostic: Diagnostic::new(error.to_diagnostic(), file),
share,
standard_error,
weight: g.weight.to_f64(),
runs: outcome.sample.is_some().then_some(g.runs),
first_run: g.first_run.map(|r| u64::from(r) + 1),
}
})
.collect();
let failed_share = if all.is_empty() {
Some(0.0)
} else {
share_of(all.weight)
};
(failures, failed_share)
}
fn failure_text(failures: &[Failure], format: report::Format) -> String {
let rows: Vec<(String, String)> = failures
.iter()
.take(LISTED_FAILURES)
.map(|f| {
let (line, _) = f.diagnostic.line_column();
let label = format!("line {line}: {}", f.diagnostic.message());
let share = match (f.share, f.standard_error) {
(Some(p), Some(se)) => Some(report::estimate(p, se)),
(Some(p), None) => Some(report::pct(p, format)),
(None, _) => None,
};
let amount = match (f.runs, share) {
(Some(runs), share) => {
let runs = match runs {
1 => "1 run".to_string(),
n => format!("{} runs", report::thousands(n as i64)),
};
match share {
Some(share) => format!("{runs} ({share})"),
None => runs,
}
}
(None, Some(share)) => share,
(None, None) => format!("weight {}", plain_number(f.weight)),
};
(label, amount)
})
.collect();
let width = rows.iter().map(|(l, _)| l.chars().count()).max().unwrap_or(0);
let mut text = String::from("failed");
for (label, amount) in rows {
let pad = width - label.chars().count();
text.push_str(&format!("\n {label}{} {amount}", " ".repeat(pad)));
}
if failures.len() > LISTED_FAILURES {
text.push_str(&format!("\n and {} more", failures.len() - LISTED_FAILURES));
}
text
}
fn plain_number(x: f64) -> String {
if x != 0.0 && !(1e-3..1e6).contains(&x.abs()) {
format!("{x:.3e}")
} else {
let digits = if x == 0.0 {
0
} else {
(3 - x.abs().log10().floor() as i32).max(0) as usize
};
format!("{x:.digits$}")
}
}
impl std::fmt::Debug for Outcome {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Outcome")
.field("text", &self.text)
.field("reports", &self.reports)
.field("evidence", &self.evidence)
.field("unresolved", &self.unresolved)
.field("sampling", &self.sampling)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
pub struct Report {
label: String,
groups: Vec<Group>,
}
impl Report {
pub fn label(&self) -> &str {
&self.label
}
pub fn groups(&self) -> &[Group] {
&self.groups
}
}
#[derive(Clone, Debug)]
pub struct Group {
key: Option<String>,
probability: Option<Estimate>,
distribution: Option<Vec<(String, Estimate)>>,
numeric: Option<NumericSummary>,
}
impl Group {
fn new(group: &GroupResult, by: bool) -> Group {
Group {
key: by.then(|| report::display(&group.key)),
probability: group.fact.map(Estimate::new),
distribution: match (&group.fact, &group.values) {
(None, Some(values)) => Some(
values
.iter()
.map(|(value, q)| (label(value), Estimate::new(*q)))
.collect(),
),
_ => None,
},
numeric: group.numeric.clone().map(NumericSummary::new),
}
}
pub fn key(&self) -> Option<&str> {
self.key.as_deref()
}
pub fn probability(&self) -> Option<&Estimate> {
self.probability.as_ref()
}
pub fn distribution(&self) -> Option<&[(String, Estimate)]> {
self.distribution.as_deref()
}
pub fn numeric(&self) -> Option<&NumericSummary> {
self.numeric.as_ref()
}
}
fn label(value: &Value) -> String {
report::display(value)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Estimate {
point: Option<f64>,
complete: bool,
bounds: Option<Interval>,
sampling: Option<SamplingUncertainty>,
}
impl Estimate {
fn new(q: report::Quantity) -> Estimate {
Estimate {
point: q.point,
complete: q.complete,
bounds: q.bounds.map(|(lower, upper)| Interval { lower, upper }),
sampling: q.sampling.map(SamplingUncertainty::new),
}
}
pub fn point(&self) -> Option<f64> {
self.point
}
pub fn is_complete(&self) -> bool {
self.complete
}
pub fn bounds(&self) -> Option<Interval> {
self.bounds
}
pub fn sampling(&self) -> Option<&SamplingUncertainty> {
self.sampling.as_ref()
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SamplingStatus {
Estimated,
NotEstimable,
NotComputed,
IntegratedZero,
}
impl SamplingStatus {
fn new(status: Status) -> SamplingStatus {
match status {
Status::Estimated => SamplingStatus::Estimated,
Status::NotEstimable => SamplingStatus::NotEstimable,
Status::NotComputed => SamplingStatus::NotComputed,
Status::IntegratedZero => SamplingStatus::IntegratedZero,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SamplingUncertainty {
status: SamplingStatus,
standard_error: Option<f64>,
confidence_interval: Option<ConfidenceInterval>,
contributing_runs: u64,
effective_runs: f64,
}
impl SamplingUncertainty {
fn new(u: report::Uncertainty) -> SamplingUncertainty {
SamplingUncertainty {
status: SamplingStatus::new(u.status),
standard_error: u.se,
confidence_interval: u.wilson.map(|(lower, upper)| ConfidenceInterval {
interval: Interval { lower, upper },
level: 0.95,
method: ConfidenceMethod::Wilson,
}),
contributing_runs: u.support.contributing_runs,
effective_runs: u.support.effective,
}
}
pub fn status(&self) -> SamplingStatus {
self.status
}
pub fn standard_error(&self) -> Option<f64> {
self.standard_error
}
pub fn confidence_interval(&self) -> Option<&ConfidenceInterval> {
self.confidence_interval.as_ref()
}
pub fn contributing_runs(&self) -> u64 {
self.contributing_runs
}
pub fn effective_runs(&self) -> f64 {
self.effective_runs
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Interval {
lower: f64,
upper: f64,
}
impl Interval {
pub fn lower(&self) -> f64 {
self.lower
}
pub fn upper(&self) -> f64 {
self.upper
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ConfidenceInterval {
interval: Interval,
level: f64,
method: ConfidenceMethod,
}
impl ConfidenceInterval {
pub fn interval(&self) -> &Interval {
&self.interval
}
pub fn level(&self) -> f64 {
self.level
}
pub fn method(&self) -> ConfidenceMethod {
self.method
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ConfidenceMethod {
Wilson,
}
#[derive(Clone, Debug)]
pub struct NumericSummary {
mean: Estimate,
sd: Estimate,
numeric: report::Numeric,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SummaryError {
InvalidQuantile,
Unavailable,
}
impl std::fmt::Display for SummaryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
SummaryError::InvalidQuantile => "a quantile must be a number from 0 to 1",
SummaryError::Unavailable => "the report has no such summary",
})
}
}
impl std::error::Error for SummaryError {}
impl NumericSummary {
fn new(numeric: report::Numeric) -> NumericSummary {
NumericSummary {
mean: Estimate::new(numeric.mean),
sd: Estimate::new(numeric.sd),
numeric,
}
}
pub fn mean(&self) -> Option<&Estimate> {
Some(&self.mean)
}
pub fn sd(&self) -> Option<&Estimate> {
Some(&self.sd)
}
pub fn quantile(&self, q: f64) -> Result<Estimate, SummaryError> {
if !(0.0..=1.0).contains(&q) {
return Err(SummaryError::InvalidQuantile);
}
self.numeric
.quantile(q)
.map(Estimate::new)
.ok_or(SummaryError::Unavailable)
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum EvidenceKind {
Probability,
Density,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Evidence {
kind: EvidenceKind,
log_value: Option<f64>,
complete: bool,
log_bounds: Option<Interval>,
relative_standard_error: Option<f64>,
sampling_status: Option<SamplingStatus>,
}
fn evidence(outcome: &probl_engine::Outcome) -> Option<Evidence> {
let z = outcome.evidence?;
let complete = outcome.unresolved.is_zero();
Some(match &outcome.sample {
None => Evidence {
kind: EvidenceKind::Probability,
log_value: Some(z.ln()),
complete,
log_bounds: (!complete).then(|| Interval {
lower: z.ln(),
upper: (z + outcome.unresolved).ln(),
}),
relative_standard_error: None,
sampling_status: None,
},
Some(sampled) => {
let known = sampled.evidence_se.is_finite();
Evidence {
kind: if sampled.densities {
EvidenceKind::Density
} else {
EvidenceKind::Probability
},
log_value: Some(z.ln()),
complete,
log_bounds: None,
relative_standard_error: known.then_some(sampled.evidence_se),
sampling_status: Some(if known {
SamplingStatus::Estimated
} else {
SamplingStatus::NotEstimable
}),
}
}
})
}
impl Evidence {
pub fn kind(&self) -> EvidenceKind {
self.kind
}
pub fn log_value(&self) -> Option<f64> {
self.log_value
}
pub fn is_complete(&self) -> bool {
self.complete
}
pub fn log_bounds(&self) -> Option<Interval> {
self.log_bounds
}
pub fn relative_standard_error(&self) -> Option<f64> {
self.relative_standard_error
}
pub fn sampling_status(&self) -> Option<SamplingStatus> {
self.sampling_status
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Unresolved(probl_engine::Weight);
impl Unresolved {
pub fn is_zero(&self) -> bool {
self.0.is_zero()
}
pub fn log_weight_upper_bound(&self) -> f64 {
self.0.ln()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Sampling {
runs: u64,
seed: u64,
effective_runs: f64,
}
impl Sampling {
pub fn runs(&self) -> u64 {
self.runs
}
pub fn seed(&self) -> u64 {
self.seed
}
pub fn effective_runs(&self) -> f64 {
self.effective_runs
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Stats {
peak_worlds: usize,
world_steps: u64,
calls: u64,
reused_calls: u64,
solved_loops: u64,
chain_states: u64,
solved_calls: u64,
call_rounds: u64,
exact_updates: Vec<ExactUpdates>,
}
impl Stats {
fn new(outcome: &probl_engine::Outcome, file: &SourceFile) -> Stats {
let s = &outcome.stats;
Stats {
peak_worlds: s.peak_worlds,
world_steps: s.world_steps,
calls: s.calls,
reused_calls: s.memo_hits,
solved_loops: s.solved_loops,
chain_states: s.chain_states,
solved_calls: s.solved_calls,
call_rounds: s.call_rounds,
exact_updates: outcome
.updates
.iter()
.map(|(variable, u)| ExactUpdates {
variable: variable.name.clone(),
line: file.line_col(variable.span.lo).0,
delayed: u.delayed,
observations: u.exact,
drawn: u.drawn,
})
.collect(),
}
}
pub fn peak_worlds(&self) -> usize {
self.peak_worlds
}
pub fn world_steps(&self) -> u64 {
self.world_steps
}
pub fn calls(&self) -> u64 {
self.calls
}
pub fn reused_calls(&self) -> u64 {
self.reused_calls
}
pub fn solved_loops(&self) -> u64 {
self.solved_loops
}
pub fn chain_states(&self) -> u64 {
self.chain_states
}
pub fn solved_calls(&self) -> u64 {
self.solved_calls
}
pub fn call_rounds(&self) -> u64 {
self.call_rounds
}
pub fn exact_updates(&self) -> &[ExactUpdates] {
&self.exact_updates
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExactUpdates {
variable: String,
line: usize,
delayed: u64,
observations: u64,
drawn: u64,
}
impl ExactUpdates {
pub fn variable(&self) -> &str {
&self.variable
}
pub fn line(&self) -> usize {
self.line
}
pub fn delayed(&self) -> u64 {
self.delayed
}
pub fn observations(&self) -> u64 {
self.observations
}
pub fn drawn(&self) -> u64 {
self.drawn
}
}