use std::collections::HashMap;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use crate::asv::AsvExplainer;
use crate::error::CausasvError;
use crate::graph::{Dag as RustDag, NodeId};
use crate::sampler::SamplingConfig;
#[pyclass(name = "CausalDAG")]
pub struct PyCausalDAG {
pub(crate) inner: RustDag,
}
#[pymethods]
impl PyCausalDAG {
#[new]
fn new() -> Self {
Self {
inner: RustDag::new(),
}
}
fn add_edge(&mut self, from_name: &str, to_name: &str) -> PyResult<()> {
let from = self.inner.add_node(from_name);
let to = self.inner.add_node(to_name);
self.inner.add_edge(from, to).map_err(py_err)
}
fn validate(&self) -> PyResult<()> {
self.inner.validate().map_err(py_err)
}
}
#[pyclass(name = "ASVExplainer")]
pub struct PyASVExplainer {
inner: AsvExplainer,
names: Vec<String>,
}
#[pymethods]
impl PyASVExplainer {
#[new]
fn new(dag: &PyCausalDAG) -> PyResult<Self> {
dag.inner.validate().map_err(py_err)?;
let names: Vec<String> = dag
.inner
.all_nodes()
.map(|id| dag.inner.node_name(id).unwrap().to_string())
.collect();
Ok(Self {
inner: AsvExplainer::new(dag.inner.clone()),
names,
})
}
#[pyo3(signature = (value_fn, method = "approx", n_samples = 10_000, seed = None))]
fn explain(
&self,
py: Python<'_>,
value_fn: PyObject,
method: &str,
n_samples: usize,
seed: Option<u64>,
) -> PyResult<HashMap<String, f64>> {
let names = &self.names;
let rust_fn = |coalition: &[NodeId]| -> Result<f64, CausasvError> {
let name_list: Vec<&str> = coalition
.iter()
.map(|id| names[id.0 as usize].as_str())
.collect();
value_fn
.call1(py, (name_list,))
.and_then(|r| r.extract::<f64>(py))
.map_err(|e| CausasvError::ValueFunctionError(e.to_string()))
};
let result = match method {
"approx" => {
let mut cfg = SamplingConfig::new(n_samples);
if let Some(s) = seed {
cfg = cfg.with_seed(s);
}
self.inner.approximate(rust_fn, cfg)
}
"exact" => self.inner.exact(rust_fn),
"exact_tree" => self.inner.exact_tree(rust_fn),
_ => {
return Err(PyValueError::new_err(format!(
"unknown method '{method}': use 'approx', 'exact', or 'exact_tree'"
)))
}
}
.map_err(py_err)?;
Ok(result
.values
.iter()
.map(|(id, &v)| (names[id.0 as usize].clone(), v))
.collect())
}
}
#[pymodule]
fn causasv(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyCausalDAG>()?;
m.add_class::<PyASVExplainer>()?;
Ok(())
}
fn py_err(e: CausasvError) -> PyErr {
PyValueError::new_err(e.to_string())
}