use antecedent_estimate::BayesianBackendKind;
use crate::error::CausalError;
use crate::inference::InferenceMode;
use crate::planner::GraphInput;
use super::builder::RefuteSuite;
pub const INTERACTIVE_N_DRAWS: usize = 64;
pub const STANDARD_N_DRAWS: usize = 1000;
pub const REPORT_N_DRAWS: usize = 4000;
pub const INTERACTIVE_BOOTSTRAP: u32 = 0;
pub const STANDARD_BOOTSTRAP: u32 = 50;
pub const REPORT_BOOTSTRAP: u32 = 200;
pub const INTERACTIVE_MAX_ENVELOPE_GRAPHS: usize = 16;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum LatencyMode {
Interactive,
Standard,
Report,
}
impl LatencyMode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Interactive => "interactive",
Self::Standard => "standard",
Self::Report => "report",
}
}
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"interactive" => Some(Self::Interactive),
"standard" => Some(Self::Standard),
"report" => Some(Self::Report),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
pub struct ComputeBudget {
pub wall_ms: Option<u64>,
pub bootstrap: Option<u32>,
pub n_draws: Option<usize>,
pub validators: Option<RefuteSuite>,
}
impl ComputeBudget {
#[must_use]
pub const fn new() -> Self {
Self { wall_ms: None, bootstrap: None, n_draws: None, validators: None }
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ResolvedLatencyBudget {
pub mode: LatencyMode,
pub bootstrap: u32,
pub refute: RefuteSuite,
pub n_draws: usize,
pub wall_ms: Option<u64>,
}
impl ResolvedLatencyBudget {
#[must_use]
pub const fn from_mode(mode: LatencyMode) -> Self {
match mode {
LatencyMode::Interactive => Self {
mode,
bootstrap: INTERACTIVE_BOOTSTRAP,
refute: RefuteSuite::Cheap,
n_draws: INTERACTIVE_N_DRAWS,
wall_ms: None,
},
LatencyMode::Standard => Self {
mode,
bootstrap: STANDARD_BOOTSTRAP,
refute: RefuteSuite::PlaceboAndRcc,
n_draws: STANDARD_N_DRAWS,
wall_ms: None,
},
LatencyMode::Report => Self {
mode,
bootstrap: REPORT_BOOTSTRAP,
refute: RefuteSuite::Full,
n_draws: REPORT_N_DRAWS,
wall_ms: None,
},
}
}
#[must_use]
pub const fn with_overrides(mut self, budget: ComputeBudget) -> Self {
if let Some(b) = budget.bootstrap {
self.bootstrap = b;
}
if let Some(n) = budget.n_draws {
self.n_draws = n;
}
if let Some(v) = budget.validators {
self.refute = v;
}
if budget.wall_ms.is_some() {
self.wall_ms = budget.wall_ms;
}
self
}
}
pub fn refuse_non_report_hmc(
mode: LatencyMode,
inference: &InferenceMode,
) -> Result<(), CausalError> {
if mode == LatencyMode::Report {
return Ok(());
}
let InferenceMode::Bayesian(cfg) = inference else {
return Ok(());
};
if matches!(cfg.backend, BayesianBackendKind::Hmc) {
return Err(CausalError::Unsupported {
message: "HMC requires LatencyMode::Report; use Laplace/conjugate for Interactive/Standard",
});
}
Ok(())
}
pub fn refuse_discovery_under_interactive(
mode: LatencyMode,
graph: &GraphInput,
) -> Result<(), CausalError> {
if mode == LatencyMode::Interactive && graph.is_discovery() {
return Err(CausalError::Unsupported {
message: "discovery graphs are not on the Interactive estimate path; \
discover once, accept the graph, then supply GraphInput::Static/Cpdag/Pag \
(or prepare) under LatencyMode::Interactive",
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inference::BayesianConfig;
#[test]
fn mode_labels_roundtrip() {
for mode in [LatencyMode::Interactive, LatencyMode::Standard, LatencyMode::Report] {
assert_eq!(LatencyMode::parse(mode.as_str()), Some(mode));
}
assert_eq!(LatencyMode::parse("nope"), None);
}
#[test]
fn explicit_budget_overrides_mode() {
let resolved = ResolvedLatencyBudget::from_mode(LatencyMode::Interactive).with_overrides(
ComputeBudget {
wall_ms: Some(250),
bootstrap: Some(10),
n_draws: Some(128),
validators: Some(RefuteSuite::None),
},
);
assert_eq!(resolved.bootstrap, 10);
assert_eq!(resolved.n_draws, 128);
assert_eq!(resolved.refute, RefuteSuite::None);
assert_eq!(resolved.wall_ms, Some(250));
assert_eq!(resolved.mode, LatencyMode::Interactive);
}
#[test]
fn non_report_refuses_hmc() {
let cfg = BayesianConfig::hmc();
let err =
refuse_non_report_hmc(LatencyMode::Interactive, &InferenceMode::Bayesian(cfg.clone()))
.unwrap_err();
assert!(matches!(err, CausalError::Unsupported { .. }));
let err_std = refuse_non_report_hmc(LatencyMode::Standard, &InferenceMode::Bayesian(cfg))
.unwrap_err();
assert!(matches!(err_std, CausalError::Unsupported { .. }));
assert!(
refuse_non_report_hmc(
LatencyMode::Interactive,
&InferenceMode::Bayesian(BayesianConfig::laplace()),
)
.is_ok()
);
assert!(
refuse_non_report_hmc(
LatencyMode::Report,
&InferenceMode::Bayesian(BayesianConfig::hmc()),
)
.is_ok()
);
}
#[test]
fn interactive_refuses_discovery_graph() {
let discover = GraphInput::DiscoverPc {
alpha: 0.05,
max_cond_size: 3,
fdr: None,
accept_discovered: true,
};
let err =
refuse_discovery_under_interactive(LatencyMode::Interactive, &discover).unwrap_err();
assert!(
matches!(err, CausalError::Unsupported { message } if message.contains("Interactive"))
);
assert!(refuse_discovery_under_interactive(LatencyMode::Standard, &discover).is_ok());
assert!(refuse_discovery_under_interactive(LatencyMode::Report, &discover).is_ok());
let supplied = GraphInput::Static(antecedent_graph::Dag::with_variables(1));
assert!(refuse_discovery_under_interactive(LatencyMode::Interactive, &supplied).is_ok());
}
}