use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
#[cfg(feature = "radar_examples")]
use pyo3::types::PyComplex;
use pyo3::types::{PyDict, PyList};
use std::collections::HashMap;
use std::sync::Arc;
use crate::builder::Graph;
use crate::cache::{
CacheBackend, CacheDepth, CacheMissReason, CacheOptions, MemoryCacheBackend, MemoryCacheConfig,
};
use crate::dag::{Dag, ExecutionResult, NodeCacheStatus, PredictTarget};
use crate::distribution::{DistContext, Distribution};
use crate::graph_data::GraphData;
use crate::stat_result::StatResult;
#[pyclass(name = "Distribution")]
#[derive(Clone)]
struct PyDistribution {
inner: Distribution,
}
#[pymethods]
impl PyDistribution {
#[getter]
fn mean(&self) -> f64 {
self.inner.mean()
}
#[getter]
fn std(&self) -> f64 {
self.inner.std()
}
#[getter]
fn variance(&self) -> f64 {
self.inner.variance()
}
#[getter]
fn p5(&self) -> f64 {
self.inner.percentile(0.05)
}
#[getter]
fn p50(&self) -> f64 {
self.inner.percentile(0.50)
}
#[getter]
fn p95(&self) -> f64 {
self.inner.percentile(0.95)
}
fn percentile(&self, p: f64) -> f64 {
self.inner.percentile(p)
}
#[getter]
fn samples(&self, py: Python) -> PyObject {
match &self.inner {
Distribution::Empirical { samples } => {
let v: Vec<f64> = samples.as_ref().clone();
v.to_object(py)
}
_ => py.None(),
}
}
fn sample_n(&self, n: usize) -> Vec<f64> {
self.inner.sample_n(n)
}
fn summary(&self) -> String {
format!("{}", self.inner.summary())
}
fn __repr__(&self) -> String {
format!("{}", self.inner)
}
}
#[pyclass(name = "StatResult")]
struct PyStatResult {
inner: StatResult,
}
#[pymethods]
impl PyStatResult {
fn __getitem__(&self, py: Python, key: &str) -> PyResult<PyObject> {
match self.inner.get(key) {
Some(dist) => Ok(PyDistribution {
inner: dist.clone(),
}
.into_py(py)),
None => Err(PyValueError::new_err(format!(
"Variable '{}' not found in StatResult",
key
))),
}
}
fn get(&self, py: Python, key: &str) -> PyObject {
match self.inner.get(key) {
Some(dist) => PyDistribution {
inner: dist.clone(),
}
.into_py(py),
None => py.None(),
}
}
fn for_branch(&self, py: Python, branch_id: usize) -> PyObject {
dist_context_to_py_dict(py, self.inner.for_branch(branch_id))
}
fn for_variant(&self, py: Python, variant_idx: usize) -> PyObject {
dist_context_to_py_dict(py, self.inner.for_variant(variant_idx))
}
fn keys(&self, py: Python) -> PyObject {
let mut ks: Vec<&str> = self
.inner
.dist_context
.keys()
.filter(|k| !k.starts_with("__branch_"))
.map(|k| k.as_str())
.collect();
ks.sort();
ks.to_object(py)
}
fn print_summary(&self) {
self.inner.print_summary();
}
fn __repr__(&self) -> String {
let keys: Vec<&str> = self
.inner
.dist_context
.keys()
.filter(|k| !k.starts_with("__branch_"))
.map(|k| k.as_str())
.collect();
format!("StatResult(vars={:?})", keys)
}
#[getter]
fn particles(&self, py: Python) -> PyObject {
match &self.inner.particles {
None => py.None(),
Some(parts) => {
let py_list = pyo3::types::PyList::empty(py);
for particle in parts {
let d = PyDict::new(py);
for (k, v) in particle {
let _ = d.set_item(k, v);
}
let _ = py_list.append(d);
}
py_list.to_object(py)
}
}
}
}
fn dist_context_to_py_dict(py: Python, ctx: Option<&DistContext>) -> PyObject {
let dict = PyDict::new(py);
if let Some(c) = ctx {
for (k, v) in c {
let _ = dict.set_item(k, PyDistribution { inner: v.clone() }.into_py(py));
}
}
dict.to_object(py)
}
#[pyclass(name = "MemoryCache")]
#[derive(Clone)]
struct PyMemoryCache {
backend: Arc<MemoryCacheBackend>,
namespace: String,
}
impl PyMemoryCache {
fn as_backend(&self) -> Arc<dyn CacheBackend> {
self.backend.clone()
}
fn namespace_or_default(&self) -> String {
self.namespace.clone()
}
}
#[pymethods]
impl PyMemoryCache {
#[new]
#[pyo3(signature = (max_entries=None, ttl_seconds=None, namespace="default"))]
fn new(max_entries: Option<usize>, ttl_seconds: Option<u64>, namespace: &str) -> Self {
let config = MemoryCacheConfig {
max_entries: max_entries.unwrap_or(1_024),
ttl: ttl_seconds.map(std::time::Duration::from_secs),
};
Self {
backend: Arc::new(MemoryCacheBackend::new(config)),
namespace: namespace.to_string(),
}
}
#[getter]
fn namespace(&self) -> String {
self.namespace.clone()
}
#[setter]
fn set_namespace(&mut self, namespace: String) {
self.namespace = namespace;
}
fn clear(&self) {
self.backend.clear_all();
}
fn clear_namespace(&self, namespace: String) {
self.backend.clear_namespace(&namespace);
}
#[pyo3(signature = (node_id, version=None, namespace=None))]
fn clear_node(&self, node_id: usize, version: Option<String>, namespace: Option<String>) {
let namespace = namespace.unwrap_or_else(|| self.namespace_or_default());
self.backend
.clear_node(&namespace, node_id, version.as_deref());
}
fn stats(&self, py: Python) -> PyResult<PyObject> {
let stats = self.backend.stats();
let dict = PyDict::new(py);
dict.set_item("entries", stats.entries)?;
dict.set_item("max_entries", stats.max_entries)?;
dict.set_item("hits", stats.hits)?;
dict.set_item("misses", stats.misses)?;
dict.set_item("evictions", stats.evictions)?;
dict.set_item("expirations", stats.expirations)?;
Ok(dict.to_object(py))
}
}
#[pyclass(name = "Graph")]
struct PyGraph {
graph: Option<Graph>,
cache_namespace_hint: Option<String>,
}
#[pymethods]
impl PyGraph {
#[new]
#[pyo3(signature = (cache_backend=None))]
fn new(cache_backend: Option<PyRef<PyMemoryCache>>) -> Self {
let mut graph = Graph::new();
let cache_namespace_hint = cache_backend
.as_ref()
.map(|backend| backend.namespace_or_default());
if let Some(cache_backend) = cache_backend {
graph.with_cache_backend(cache_backend.as_backend());
}
PyGraph {
graph: Some(graph),
cache_namespace_hint,
}
}
#[pyo3(signature = (function=None, label=None, inputs=None, outputs=None))]
fn add(
&mut self,
function: Option<PyObject>,
label: Option<String>,
inputs: Option<&PyAny>,
outputs: Option<&PyAny>,
) -> PyResult<()> {
let graph = self
.graph
.as_mut()
.ok_or_else(|| PyValueError::new_err("Graph has already been built or consumed"))?;
let input_vec = if let Some(inp) = inputs {
parse_mapping(inp)?
} else {
Vec::new()
};
let output_vec = if let Some(out) = outputs {
parse_mapping(out)?
} else {
Vec::new()
};
let input_refs: Vec<(&str, &str)> = input_vec
.iter()
.map(|(a, b)| (a.as_str(), b.as_str()))
.collect();
let output_refs: Vec<(&str, &str)> = output_vec
.iter()
.map(|(a, b)| (a.as_str(), b.as_str()))
.collect();
if let Some(py_func) = function {
let rust_function = create_python_node_function(py_func);
graph.add(
rust_function,
label.as_deref(),
if input_refs.is_empty() {
None
} else {
Some(input_refs)
},
if output_refs.is_empty() {
None
} else {
Some(output_refs)
},
);
} else {
let noop = |_: &HashMap<String, GraphData>| HashMap::new();
graph.add(
noop,
label.as_deref(),
if input_refs.is_empty() {
None
} else {
Some(input_refs)
},
if output_refs.is_empty() {
None
} else {
Some(output_refs)
},
);
}
Ok(())
}
fn branch(&mut self, mut subgraph: PyRefMut<PyGraph>) -> PyResult<usize> {
let graph = self
.graph
.as_mut()
.ok_or_else(|| PyValueError::new_err("Graph has already been built or consumed"))?;
let subgraph_inner = subgraph
.graph
.take()
.ok_or_else(|| PyValueError::new_err("Subgraph has already been built or consumed"))?;
Ok(graph.branch(subgraph_inner))
}
#[pyo3(signature = (functions, label=None, inputs=None, outputs=None))]
fn variants(
&mut self,
functions: Vec<PyObject>,
label: Option<String>,
inputs: Option<&PyAny>,
outputs: Option<&PyAny>,
) -> PyResult<()> {
let graph = self
.graph
.as_mut()
.ok_or_else(|| PyValueError::new_err("Graph has already been built or consumed"))?;
let input_vec = if let Some(inp) = inputs {
parse_mapping(inp)?
} else {
Vec::new()
};
let output_vec = if let Some(out) = outputs {
parse_mapping(out)?
} else {
Vec::new()
};
let input_refs: Vec<(&str, &str)> = input_vec
.iter()
.map(|(a, b)| (a.as_str(), b.as_str()))
.collect();
let output_refs: Vec<(&str, &str)> = output_vec
.iter()
.map(|(a, b)| (a.as_str(), b.as_str()))
.collect();
let rust_functions: Vec<_> = functions
.iter()
.map(|func| create_python_node_function(func.clone()))
.collect();
graph.variants(
rust_functions,
label.as_deref(),
if input_refs.is_empty() {
None
} else {
Some(input_refs)
},
if output_refs.is_empty() {
None
} else {
Some(output_refs)
},
);
Ok(())
}
#[pyo3(signature = (cache_backend=None))]
fn build(&mut self, cache_backend: Option<PyRef<PyMemoryCache>>) -> PyResult<PyDag> {
let graph = self
.graph
.take()
.ok_or_else(|| PyValueError::new_err("Graph has already been built"))?;
let (dag, default_cache_namespace) = if let Some(cache_backend) = cache_backend {
(
graph.build_with_cache_backend(cache_backend.as_backend()),
Some(cache_backend.namespace_or_default()),
)
} else {
(graph.build(), self.cache_namespace_hint.clone())
};
Ok(PyDag {
dag,
default_cache_namespace,
})
}
#[pyo3(signature = (max_entries=None, ttl_seconds=None))]
fn configure_memory_cache(
&mut self,
max_entries: Option<usize>,
ttl_seconds: Option<u64>,
) -> PyResult<()> {
let graph = self
.graph
.as_mut()
.ok_or_else(|| PyValueError::new_err("Graph has already been built or consumed"))?;
graph.with_memory_cache_config(MemoryCacheConfig {
max_entries: max_entries.unwrap_or(1_024),
ttl: ttl_seconds.map(std::time::Duration::from_secs),
});
Ok(())
}
fn set_cache_backend(&mut self, cache_backend: PyRef<PyMemoryCache>) -> PyResult<()> {
let graph = self
.graph
.as_mut()
.ok_or_else(|| PyValueError::new_err("Graph has already been built or consumed"))?;
graph.with_cache_backend(cache_backend.as_backend());
self.cache_namespace_hint = Some(cache_backend.namespace_or_default());
Ok(())
}
fn set_cache_version(&mut self, label: String, version: String) -> PyResult<()> {
let graph = self
.graph
.as_mut()
.ok_or_else(|| PyValueError::new_err("Graph has already been built or consumed"))?;
graph.set_cache_version_for(&label, version);
Ok(())
}
fn set_cacheable(&mut self, label: String, cacheable: bool) -> PyResult<()> {
let graph = self
.graph
.as_mut()
.ok_or_else(|| PyValueError::new_err("Graph has already been built or consumed"))?;
graph.set_cacheable_for(&label, cacheable);
Ok(())
}
fn set_cache_key_inputs(&mut self, label: String, impl_vars: Vec<String>) -> PyResult<()> {
let graph = self
.graph
.as_mut()
.ok_or_else(|| PyValueError::new_err("Graph has already been built or consumed"))?;
let refs: Vec<&str> = impl_vars.iter().map(String::as_str).collect();
graph.set_cache_key_inputs_for(&label, refs);
Ok(())
}
fn set_dist_transfer(&mut self, label: String, transfer_fn: PyObject) -> PyResult<()> {
let graph = self
.graph
.as_mut()
.ok_or_else(|| PyValueError::new_err("Graph has already been built or consumed"))?;
let rust_transfer = create_python_dist_transfer(transfer_fn);
graph.set_dist_transfer_for(&label, Arc::new(rust_transfer));
Ok(())
}
}
#[pyclass(name = "Dag")]
struct PyDag {
dag: Dag,
default_cache_namespace: Option<String>,
}
#[pymethods]
impl PyDag {
#[pyo3(signature = (parallel=false, max_threads=None, cache=true, cache_depth="transitive", cache_namespace=None, cache_backend=None, detailed=false))]
fn execute(
&self,
py: Python,
parallel: bool,
max_threads: Option<usize>,
cache: bool,
cache_depth: &str,
cache_namespace: Option<String>,
cache_backend: Option<PyRef<PyMemoryCache>>,
detailed: bool,
) -> PyResult<PyObject> {
let depth = CacheDepth::parse(cache_depth).ok_or_else(|| {
PyValueError::new_err("cache_depth must be one of: none, shallow, transitive")
})?;
let backend_override = cache_backend.as_ref().map(|backend| backend.as_backend());
let namespace = cache_namespace.unwrap_or_else(|| {
cache_backend
.as_ref()
.map(|backend| backend.namespace_or_default())
.or_else(|| self.default_cache_namespace.clone())
.unwrap_or_else(|| "default".to_string())
});
let cache_options = CacheOptions::default()
.with_enabled(cache)
.with_depth(depth)
.with_namespace(namespace);
let execution = py.allow_threads(|| {
if let Some(cache_backend) = backend_override {
self.dag.execute_detailed_with_backend_options(
parallel,
max_threads,
cache_options,
cache_backend,
)
} else {
self.dag
.execute_detailed_with_options(parallel, max_threads, cache_options)
}
});
if detailed {
return execution_result_to_python(py, &execution);
}
let py_dict = PyDict::new(py);
for (key, value) in execution.context.iter() {
py_dict.set_item(key, graph_data_to_python(py, value))?;
}
Ok(py_dict.to_object(py))
}
fn to_mermaid(&self) -> String {
self.dag.to_mermaid()
}
fn node_count(&self) -> usize {
self.dag.nodes().len()
}
fn cache_stats(&self, py: Python) -> PyResult<PyObject> {
let stats = self.dag.stats().cache;
let dict = PyDict::new(py);
dict.set_item("entries", stats.entries)?;
dict.set_item("max_entries", stats.max_entries)?;
dict.set_item("hits", stats.hits)?;
dict.set_item("misses", stats.misses)?;
dict.set_item("evictions", stats.evictions)?;
dict.set_item("expirations", stats.expirations)?;
Ok(dict.to_object(py))
}
fn clear_cache(&self) {
self.dag.clear_cache();
}
fn clear_cache_namespace(&self, namespace: String) {
self.dag.clear_cache_namespace(&namespace);
}
#[pyo3(signature = (namespace, node_id, version=None))]
fn clear_cache_node(&self, namespace: String, node_id: usize, version: Option<String>) {
self.dag
.clear_cache_node(&namespace, node_id, version.as_deref());
}
fn node_labels(&self, py: Python) -> PyObject {
let mut labels: Vec<&str> = self
.dag
.nodes()
.iter()
.filter_map(|n| n.label.as_deref())
.collect();
labels.sort();
labels.dedup();
labels.to_object(py)
}
fn branch_ids(&self, py: Python) -> PyObject {
let mut ids: Vec<usize> = self
.dag
.nodes()
.iter()
.filter_map(|n| n.branch_id)
.collect();
ids.sort();
ids.dedup();
ids.to_object(py)
}
fn variant_indices(&self, py: Python) -> PyObject {
let mut idxs: Vec<usize> = self
.dag
.nodes()
.iter()
.filter_map(|n| n.variant_index)
.collect();
idxs.sort();
idxs.dedup();
idxs.to_object(py)
}
#[pyo3(signature = (inputs, n_samples=None, at_node=None, at_branch=None, at_variant=None))]
fn predict_at(
&self,
py: Python,
inputs: &PyDict,
n_samples: Option<usize>,
at_node: Option<String>,
at_branch: Option<usize>,
at_variant: Option<usize>,
) -> PyResult<PyStatResult> {
let mut dist_ctx: DistContext = HashMap::new();
for (key, val) in inputs.iter() {
let k: String = key.extract()?;
let cell = val
.downcast::<PyCell<PyDistribution>>()
.map_err(|_| PyValueError::new_err(format!(
"Value for key '{}' must be a Distribution (use dagex.normal(), dagex.gamma(), etc.)",
k
)))?;
dist_ctx.insert(k, cell.borrow().inner.clone());
}
let target: Option<PredictTarget> = if let Some(label) = at_node {
Some(PredictTarget::NodeLabel(label))
} else if let Some(bid) = at_branch {
Some(PredictTarget::BranchId(bid))
} else if let Some(vi) = at_variant {
Some(PredictTarget::VariantIndex(vi))
} else {
None
};
let stat = py.allow_threads(|| self.dag.predict_at(dist_ctx, n_samples, target.as_ref()));
Ok(PyStatResult { inner: stat })
}
#[pyo3(signature = (inputs, n_samples=1000))]
fn predict(&self, py: Python, inputs: &PyDict, n_samples: usize) -> PyResult<PyStatResult> {
let mut dist_ctx: DistContext = HashMap::new();
for (key, val) in inputs.iter() {
let k: String = key.extract()?;
let cell = val.downcast::<PyCell<PyDistribution>>().map_err(|_| {
PyValueError::new_err(format!("Value for key '{}' must be a Distribution", k))
})?;
dist_ctx.insert(k, cell.borrow().inner.clone());
}
let stat = py.allow_threads(|| self.dag.predict(dist_ctx, n_samples));
Ok(PyStatResult { inner: stat })
}
}
fn node_cache_category(status: &NodeCacheStatus) -> &'static str {
if status.hit {
"hit"
} else {
match status.reason {
Some(
CacheMissReason::CodeChanged
| CacheMissReason::InputChanged
| CacheMissReason::DependencyChanged
| CacheMissReason::Invalidated,
) => "invalidation",
Some(
CacheMissReason::MissingVersion
| CacheMissReason::NonCacheable
| CacheMissReason::UnsupportedInput,
) => "incompatibility",
_ => "miss",
}
}
}
fn execution_result_to_python(py: Python, result: &ExecutionResult) -> PyResult<PyObject> {
let out = PyDict::new(py);
let context = PyDict::new(py);
for (key, value) in result.context.iter() {
context.set_item(key, graph_data_to_python(py, value))?;
}
out.set_item("context", context)?;
let cache_stats = PyDict::new(py);
cache_stats.set_item("hits", result.cache_stats.hits)?;
cache_stats.set_item("misses", result.cache_stats.misses)?;
cache_stats.set_item("stores", result.cache_stats.stores)?;
let reason_counts = PyDict::new(py);
for (reason, count) in &result.cache_stats.reason_counts {
reason_counts.set_item(reason.to_string(), count)?;
}
cache_stats.set_item("reason_counts", reason_counts)?;
out.set_item("cache_stats", cache_stats)?;
let node_cache = PyDict::new(py);
for (node_id, status) in &result.node_cache_status {
let status_dict = PyDict::new(py);
status_dict.set_item("hit", status.hit)?;
status_dict.set_item("stored", status.stored)?;
status_dict.set_item("reason", status.reason.map(|reason| reason.to_string()))?;
status_dict.set_item("category", node_cache_category(status))?;
status_dict.set_item("cache_key", status.cache_key.clone())?;
node_cache.set_item(*node_id, status_dict)?;
}
out.set_item("node_cache", node_cache)?;
Ok(out.to_object(py))
}
fn parse_mapping(obj: &PyAny) -> PyResult<Vec<(String, String)>> {
if let Ok(dict) = obj.downcast::<PyDict>() {
let mut result = Vec::new();
for (key, value) in dict.iter() {
let k: String = key.extract()?;
let v: String = value.extract()?;
result.push((k, v));
}
Ok(result)
} else if let Ok(list) = obj.downcast::<PyList>() {
let mut result = Vec::new();
for item in list.iter() {
let tuple: (String, String) = item.extract()?;
result.push(tuple);
}
Ok(result)
} else {
Err(PyValueError::new_err(
"inputs/outputs must be a dict or list of tuples",
))
}
}
fn create_python_node_function(
py_func: PyObject,
) -> impl Fn(&HashMap<String, GraphData>) -> HashMap<String, GraphData> + Send + Sync + 'static {
let py_func = Arc::new(py_func);
move |inputs: &HashMap<String, GraphData>| {
Python::with_gil(|py| {
let py_inputs = PyDict::new(py);
for (key, value) in inputs.iter() {
if let Err(e) = py_inputs.set_item(key, graph_data_to_python(py, value)) {
let _ = py
.import("sys")
.and_then(|sys| sys.getattr("stderr"))
.and_then(|stderr| {
stderr.call_method1(
"write",
(format!("Error setting input '{}': {}\n", key, e),),
)
});
return HashMap::new();
}
}
let result = py_func.call1(py, (py_inputs,));
match result {
Ok(py_result) => {
if let Ok(result_dict) = py_result.downcast::<PyDict>(py) {
let mut output = HashMap::new();
for (key, value) in result_dict.iter() {
if let Ok(k) = key.extract::<String>() {
output.insert(k, python_to_graph_data(value));
}
}
output
} else {
let _ = py
.import("sys")
.and_then(|sys| sys.getattr("stderr"))
.and_then(|stderr| {
stderr.call_method1(
"write",
("Error: Python function did not return a dict\n",),
)
});
HashMap::new()
}
}
Err(e) => {
e.print(py);
HashMap::new()
}
}
})
}
}
fn graph_data_to_python(py: Python, data: &GraphData) -> PyObject {
match data {
GraphData::Int(v) => v.to_object(py),
GraphData::Float(v) => v.to_object(py),
GraphData::String(s) => s.to_object(py),
GraphData::FloatVec(v) => v.to_object(py),
GraphData::IntVec(v) => v.to_object(py),
GraphData::Map(m) => {
let mut is_complex_array = true;
let mut max_idx = 0;
for (k, v) in m.iter() {
if let Ok(idx) = k.parse::<usize>() {
if idx > max_idx {
max_idx = idx;
}
if let Some(inner_map) = v.as_map() {
if !inner_map.contains_key("re") || !inner_map.contains_key("im") {
is_complex_array = false;
break;
}
} else {
is_complex_array = false;
break;
}
} else {
is_complex_array = false;
break;
}
}
if is_complex_array && !m.is_empty() && m.len() == max_idx + 1 {
let list = PyList::empty(py);
for i in 0..m.len() {
if let Some(v) = m.get(&i.to_string()) {
if let Some(inner_map) = v.as_map() {
let re = inner_map
.get("re")
.and_then(|d| d.as_float())
.unwrap_or(0.0);
let im = inner_map
.get("im")
.and_then(|d| d.as_float())
.unwrap_or(0.0);
let _ = list.append((re, im).to_object(py));
}
}
}
return list.to_object(py);
}
let mut is_list = true;
let mut max_idx = 0;
for k in m.keys() {
if let Ok(idx) = k.parse::<usize>() {
if idx > max_idx {
max_idx = idx;
}
} else {
is_list = false;
break;
}
}
if is_list && !m.is_empty() && m.len() == max_idx + 1 {
let list = PyList::empty(py);
for i in 0..m.len() {
if let Some(v) = m.get(&i.to_string()) {
let _ = list.append(graph_data_to_python(py, v));
}
}
list.to_object(py)
} else {
let dict = PyDict::new(py);
for (k, v) in m.iter() {
let _ = dict.set_item(k, graph_data_to_python(py, v));
}
dict.to_object(py)
}
}
GraphData::None => py.None(),
#[cfg(feature = "python")]
GraphData::PyObject(obj) => {
obj.clone_ref(py)
}
#[cfg(feature = "radar_examples")]
GraphData::Complex(c) => {
PyComplex::from_doubles(py, c.re, c.im).to_object(py)
}
#[cfg(feature = "radar_examples")]
GraphData::FloatArray(a) => {
a.to_vec().to_object(py)
}
#[cfg(feature = "radar_examples")]
GraphData::ComplexArray(a) => {
let list = PyList::empty(py);
for c in a.iter() {
let py_complex = PyComplex::from_doubles(py, c.re, c.im);
let _ = list.append(py_complex);
}
list.to_object(py)
}
}
}
fn python_to_graph_data(obj: &PyAny) -> GraphData {
if let Ok(f) = obj.extract::<f64>() {
return GraphData::Float(f);
}
if let Ok(i) = obj.extract::<i64>() {
return GraphData::Int(i);
}
if let Ok(s) = obj.extract::<String>() {
return GraphData::String(s);
}
if let Ok(list) = obj.extract::<Vec<f64>>() {
return GraphData::FloatVec(std::sync::Arc::new(list));
}
if let Ok(list) = obj.extract::<Vec<i64>>() {
return GraphData::IntVec(std::sync::Arc::new(list));
}
GraphData::PyObject(obj.to_object(obj.py()))
}
fn create_python_dist_transfer(
py_func: PyObject,
) -> impl Fn(&DistContext) -> Option<DistContext> + Send + Sync + 'static {
let py_func = Arc::new(py_func);
move |input_dists: &DistContext| -> Option<DistContext> {
Python::with_gil(|py| {
let py_dict = PyDict::new(py);
for (key, dist) in input_dists {
let d = PyDistribution {
inner: dist.clone(),
};
py_dict.set_item(key, d.into_py(py)).ok()?;
}
let result = py_func.call1(py, (py_dict,)).ok()?;
if result.is_none(py) {
return None;
}
let result_dict = result.downcast::<PyDict>(py).ok()?;
let mut output: DistContext = HashMap::new();
for (key, val) in result_dict.iter() {
let k: String = key.extract().ok()?;
if let Ok(cell) = val.downcast::<PyCell<PyDistribution>>() {
output.insert(k, cell.borrow().inner.clone());
}
}
if output.is_empty() {
None
} else {
Some(output)
}
})
}
}
#[pyfunction]
#[pyo3(signature = (mean, std))]
fn normal(mean: f64, std: f64) -> PyDistribution {
PyDistribution {
inner: Distribution::normal(mean, std),
}
}
#[pyfunction]
#[pyo3(signature = (low, high))]
fn uniform(low: f64, high: f64) -> PyDistribution {
PyDistribution {
inner: Distribution::uniform(low, high),
}
}
#[pyfunction]
#[pyo3(signature = (alpha, beta))]
fn beta(alpha: f64, beta: f64) -> PyDistribution {
PyDistribution {
inner: Distribution::beta(alpha, beta),
}
}
#[pyfunction]
#[pyo3(signature = (shape, rate))]
fn gamma(shape: f64, rate: f64) -> PyDistribution {
PyDistribution {
inner: Distribution::gamma(shape, rate),
}
}
#[pyfunction]
#[pyo3(signature = (mu, sigma))]
fn lognormal(mu: f64, sigma: f64) -> PyDistribution {
PyDistribution {
inner: Distribution::lognormal(mu, sigma),
}
}
#[pyfunction]
#[pyo3(signature = (value))]
fn deterministic(value: f64) -> PyDistribution {
PyDistribution {
inner: Distribution::deterministic(value),
}
}
#[pyfunction]
#[pyo3(signature = (samples))]
fn empirical(samples: Vec<f64>) -> PyDistribution {
PyDistribution {
inner: Distribution::empirical(samples),
}
}
#[pymodule]
fn dagex(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<PyMemoryCache>()?;
m.add_class::<PyGraph>()?;
m.add_class::<PyDag>()?;
m.add_class::<PyDistribution>()?;
m.add_class::<PyStatResult>()?;
m.add_function(wrap_pyfunction!(normal, m)?)?;
m.add_function(wrap_pyfunction!(uniform, m)?)?;
m.add_function(wrap_pyfunction!(beta, m)?)?;
m.add_function(wrap_pyfunction!(gamma, m)?)?;
m.add_function(wrap_pyfunction!(lognormal, m)?)?;
m.add_function(wrap_pyfunction!(deterministic, m)?)?;
m.add_function(wrap_pyfunction!(empirical, m)?)?;
Ok(())
}