use crate::{DataSource, Date};
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>,
}
impl Outcome {
pub(crate) fn new(outcome: probl_engine::Outcome, program: &Arc<ir::Program>, file: &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 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),
text: outcome.output,
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
}
}
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
}
}