From d56a41902def4b0823fb5b3ea547aa218b156b17 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Fri, 20 Jun 2025 00:08:27 +0530 Subject: [PATCH 01/62] rust initial commit --- .gitignore | 1 + Cargo.toml | 23 +++++ src/dag.rs | 285 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 12 +++ src/utils.rs | 16 +++ 5 files changed, 337 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/dag.rs create mode 100644 src/lib.rs create mode 100644 src/utils.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a6aea0f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "pgmpy-rust" +version = "0.1.0" +edition = "2024" + +[lib] +name = "pgmpy_rust" +crate-type = ["cdylib"] + +[dependencies] +rustworkx-core = "0.14" +pyo3 = { version = "0.20", features = ["extension-module"] } +petgraph = "0.6" +indexmap = "2.0" +ahash = "0.8" + +[dependencies.rustworkx] +version = "0.14" +default-features = false +features = ["maturin"] + +[build-dependencies] +pyo3-build-config = "0.20" \ No newline at end of file diff --git a/src/dag.rs b/src/dag.rs new file mode 100644 index 0000000..55b2f17 --- /dev/null +++ b/src/dag.rs @@ -0,0 +1,285 @@ +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyList, PySet, PyTuple}; +use rustworkx_core::petgraph::graph::{DiGraph, NodeIndex}; +use std::collections::{HashMap, HashSet, VecDeque}; + +type NodeIndex = petgraph::graph::NodeIndex; +type EdgeIndex = petgraph::graph::EdgeIndex; + +#[pyclass] +pub struct RustDAG { + graph: Graph, + node_map: HashMap, + reverse_node_map: HashMap, + latents: HashSet, +} + +#[pymethods] +impl RustDAG { + #[new] + pub fn new() -> Self { + RustDAG { + graph: Graph::new(), + node_map: HashMap::new(), + reverse_node_map: HashMap::new(), + latents: HashSet::new(), + } + } + + /// Add a single node to the graph + pub fn add_node(&mut self, node: String, latent: Option) -> PyResult<()> { + if !self.node_map.contains_key(&node) { + let idx = self.graph.add_node(node.clone()); + self.node_map.insert(node.clone(), idx); + self.reverse_node_map.insert(idx, node.clone()); + + if latent.unwrap_or(false) { + self.latents.insert(node); + } + } + Ok(()) + } + + /// Add multiple nodes to the graph + pub fn add_nodes_from(&mut self, nodes: Vec, latent: Option>) -> PyResult<()> { + let latent_flags = latent.unwrap_or_else(|| vec![false; nodes.len()]); + + if nodes.len() != latent_flags.len() { + return Err(pyo3::exceptions::PyValueError::new_err( + "Length of nodes and latent flags must match" + )); + } + + for (node, is_latent) in nodes.iter().zip(latent_flags.iter()) { + self.add_node(node.clone(), Some(*is_latent))?; + } + Ok(()) + } + + + /// Add an edge between two nodes + pub fn add_edge(&mut self, u: String, v: String, weight: Option) -> PyResult<()> { + // Add nodes if they don't exist + self.add_node(u.clone(), None)?; + self.add_node(v.clone(), None)?; + + let u_idx = self.node_map[&u]; + let v_idx = self.node_map[&v]; + + self.graph.add_edge(u_idx, v_idx, weight.unwrap_or(1.0)); + Ok(()) + } + + //** Stop here for now **/ + + + /// Get parents of a node + pub fn get_parents(&self, node: String) -> PyResult> { + let node_idx = self.node_map.get(&node) + .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("Node {} not found", node)))?; + + let parents: Vec = self.graph + .neighbors_directed(*node_idx, Direction::Incoming) + .map(|idx| self.reverse_node_map[&idx].clone()) + .collect(); + + Ok(parents) + } + + /// Get children of a node + pub fn get_children(&self, node: String) -> PyResult> { + let node_idx = self.node_map.get(&node) + .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("Node {} not found", node)))?; + + let children: Vec = self.graph + .neighbors_directed(*node_idx, Direction::Outgoing) + .map(|idx| self.reverse_node_map[&idx].clone()) + .collect(); + + Ok(children) + } + + /// Get all ancestors of given nodes (optimized Rust implementation) + pub fn get_ancestors_of(&self, nodes: Vec) -> PyResult> { + let mut ancestors = AHashSet::new(); + let mut queue = VecDeque::new(); + + // Initialize queue with input nodes + for node in &nodes { + if let Some(&node_idx) = self.node_map.get(node) { + queue.push_back(node_idx); + ancestors.insert(node.clone()); + } else { + return Err(pyo3::exceptions::PyValueError::new_err( + format!("Node {} not in graph", node) + )); + } + } + + // BFS to find all ancestors + while let Some(current_idx) = queue.pop_front() { + for parent_idx in self.graph.neighbors_directed(current_idx, Direction::Incoming) { + let parent_name = &self.reverse_node_map[&parent_idx]; + if ancestors.insert(parent_name.clone()) { + queue.push_back(parent_idx); + } + } + } + + Ok(ancestors.into_iter().collect()) + } + + /// Fast implementation of minimal d-separator + pub fn minimal_dseparator(&self, start: String, end: String, include_latents: Option) -> PyResult>> { + let include_latents = include_latents.unwrap_or(false); + + // Check if nodes are adjacent + if self.are_adjacent(&start, &end)? { + return Err(pyo3::exceptions::PyValueError::new_err( + "No possible separators because start and end are adjacent" + )); + } + + // Get initial separator candidates (parents of both nodes) + let start_parents: HashSet = self.get_parents(start.clone())?.into_iter().collect(); + let end_parents: HashSet = self.get_parents(end.clone())?.into_iter().collect(); + + let mut separator: HashSet = start_parents.union(&end_parents).cloned().collect(); + + // Handle latents if not included + if !include_latents { + separator = self.resolve_latents(separator)?; + } + + // Remove start and end nodes from separator + separator.remove(&start); + separator.remove(&end); + + // Check if initial set can d-separate + if self.is_dconnected(&start, &end, Some(separator.iter().cloned().collect()))? { + return Ok(None); + } + + // Find minimal separator by removing unnecessary nodes + let mut minimal_separator = separator.clone(); + + for node in &separator { + let mut test_separator = minimal_separator.clone(); + test_separator.remove(node); + + if !self.is_dconnected(&start, &end, Some(test_separator.iter().cloned().collect()))? { + minimal_separator.remove(node); + } + } + + Ok(Some(minimal_separator)) + } + + /// Check if two nodes are adjacent + fn are_adjacent(&self, node1: &str, node2: &str) -> PyResult { + let idx1 = self.node_map.get(node1) + .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("Node {} not found", node1)))?; + let idx2 = self.node_map.get(node2) + .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("Node {} not found", node2)))?; + + Ok(self.graph.find_edge(*idx1, *idx2).is_some() || + self.graph.find_edge(*idx2, *idx1).is_some()) + } + + /// Resolve latents by replacing them with their parents + fn resolve_latents(&self, mut separator: HashSet) -> PyResult> { + while separator.iter().any(|node| self.latents.contains(node)) { + let mut new_separator = HashSet::new(); + for node in &separator { + if self.latents.contains(node) { + // Replace latent with its parents + let parents = self.get_parents(node.clone())?; + new_separator.extend(parents); + } else { + new_separator.insert(node.clone()); + } + } + separator = new_separator; + } + Ok(separator) + } + + /// Fast d-connection check + pub fn is_dconnected(&self, start: &str, end: &str, observed: Option>) -> PyResult { + let observed_set: HashSet = observed.unwrap_or_default().into_iter().collect(); + let ancestors = self.get_ancestors_of(observed_set.iter().cloned().collect())?; + + let mut visited = HashSet::new(); + let mut queue = VecDeque::new(); + + // Start BFS from start node + queue.push_back((start.to_string(), "up".to_string())); + + while let Some((node, direction)) = queue.pop_front() { + let state = format!("{}_{}", node, direction); + if visited.contains(&state) { + continue; + } + visited.insert(state); + + if node == end { + return Ok(true); + } + + if direction == "up" && !observed_set.contains(&node) { + // Can go up to parents + for parent in self.get_parents(node.clone())? { + queue.push_back((parent, "up".to_string())); + } + // Can go down to children + for child in self.get_children(node.clone())? { + queue.push_back((child, "down".to_string())); + } + } else if direction == "down" { + if !observed_set.contains(&node) { + // Can continue down to children + for child in self.get_children(node.clone())? { + queue.push_back((child, "down".to_string())); + } + } + if ancestors.contains(&node) { + // Can go up to parents (collider) + for parent in self.get_parents(node.clone())? { + queue.push_back((parent, "up".to_string())); + } + } + } + } + + Ok(false) + } + + /// Get all nodes in the graph + pub fn nodes(&self) -> Vec { + self.node_map.keys().cloned().collect() + } + + /// Get all edges in the graph + pub fn edges(&self) -> Vec<(String, String)> { + self.graph + .edge_indices() + .map(|edge_idx| { + let (source, target) = self.graph.edge_endpoints(edge_idx).unwrap(); + ( + self.reverse_node_map[&source].clone(), + self.reverse_node_map[&target].clone(), + ) + }) + .collect() + } + + /// Get number of nodes + pub fn node_count(&self) -> usize { + self.graph.node_count() + } + + /// Get number of edges + pub fn edge_count(&self) -> usize { + self.graph.edge_count() + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c4fda99 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,12 @@ +use pyo3::prelude::*; + +mod dag; +mod utils; + +use dag::RustDAG; + +#[pymodule] +fn pgmpy_rust(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..a576a41 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,16 @@ +// Utility functions for graph operations +use std::collections::HashSet; + +pub fn intersection( + set1: &HashSet, + set2: &HashSet +) -> HashSet { + set1.intersection(set2).cloned().collect() +} + +pub fn union( + set1: &HashSet, + set2: &HashSet +) -> HashSet { + set1.union(set2).cloned().collect() +} \ No newline at end of file From 08426b856935a8b56d1f975c6fdbf4f7936e5fa6 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Fri, 20 Jun 2025 00:46:31 +0530 Subject: [PATCH 02/62] update some rust --- .gitignore | 2 + Cargo.toml | 18 ++-- pyproject.toml | 31 +++++++ src/dag.rs | 220 +------------------------------------------------ 4 files changed, 45 insertions(+), 226 deletions(-) create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore index ea8c4bf..9dabf7d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +venv/ +.venv/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index a6aea0f..0c322c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,23 +1,23 @@ [package] -name = "pgmpy-rust" +name = "pgmpy_rust" version = "0.1.0" -edition = "2024" +edition = "2021" [lib] name = "pgmpy_rust" crate-type = ["cdylib"] [dependencies] +# Core graph library from rustworkx (pure Rust, on crates.io) rustworkx-core = "0.14" -pyo3 = { version = "0.20", features = ["extension-module"] } + +# You already imported petgraph and ahash for utilities: petgraph = "0.6" +ahash = "0.8" indexmap = "2.0" -ahash = "0.8" -[dependencies.rustworkx] -version = "0.14" -default-features = false -features = ["maturin"] +# PyO3 for the Python binding +pyo3 = { version = "0.20", features = ["extension-module"] } [build-dependencies] -pyo3-build-config = "0.20" \ No newline at end of file +pyo3-build-config = "0.20" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..01c52c4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[project] +name = "pgmpy-rust" +version = "0.1.0" +description = "High-performance graph operations for PGMPy using Rust" +authors = [ + {name = "Your Name", email = "your.email@example.com"}, +] +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [ + "rustworkx>=0.14.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=6.0", + "numpy", + "networkx", +] + +[tool.maturin] +features = ["pyo3/extension-module"] +python-source = "python" \ No newline at end of file diff --git a/src/dag.rs b/src/dag.rs index 55b2f17..7d1e047 100644 --- a/src/dag.rs +++ b/src/dag.rs @@ -1,14 +1,10 @@ use pyo3::prelude::*; -use pyo3::types::{PyDict, PyList, PySet, PyTuple}; use rustworkx_core::petgraph::graph::{DiGraph, NodeIndex}; -use std::collections::{HashMap, HashSet, VecDeque}; - -type NodeIndex = petgraph::graph::NodeIndex; -type EdgeIndex = petgraph::graph::EdgeIndex; +use std::collections::{HashMap, HashSet}; #[pyclass] pub struct RustDAG { - graph: Graph, + graph: DiGraph, node_map: HashMap, reverse_node_map: HashMap, latents: HashSet, @@ -19,7 +15,7 @@ impl RustDAG { #[new] pub fn new() -> Self { RustDAG { - graph: Graph::new(), + graph: DiGraph::new(), node_map: HashMap::new(), reverse_node_map: HashMap::new(), latents: HashSet::new(), @@ -72,214 +68,4 @@ impl RustDAG { //** Stop here for now **/ - - /// Get parents of a node - pub fn get_parents(&self, node: String) -> PyResult> { - let node_idx = self.node_map.get(&node) - .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("Node {} not found", node)))?; - - let parents: Vec = self.graph - .neighbors_directed(*node_idx, Direction::Incoming) - .map(|idx| self.reverse_node_map[&idx].clone()) - .collect(); - - Ok(parents) - } - - /// Get children of a node - pub fn get_children(&self, node: String) -> PyResult> { - let node_idx = self.node_map.get(&node) - .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("Node {} not found", node)))?; - - let children: Vec = self.graph - .neighbors_directed(*node_idx, Direction::Outgoing) - .map(|idx| self.reverse_node_map[&idx].clone()) - .collect(); - - Ok(children) - } - - /// Get all ancestors of given nodes (optimized Rust implementation) - pub fn get_ancestors_of(&self, nodes: Vec) -> PyResult> { - let mut ancestors = AHashSet::new(); - let mut queue = VecDeque::new(); - - // Initialize queue with input nodes - for node in &nodes { - if let Some(&node_idx) = self.node_map.get(node) { - queue.push_back(node_idx); - ancestors.insert(node.clone()); - } else { - return Err(pyo3::exceptions::PyValueError::new_err( - format!("Node {} not in graph", node) - )); - } - } - - // BFS to find all ancestors - while let Some(current_idx) = queue.pop_front() { - for parent_idx in self.graph.neighbors_directed(current_idx, Direction::Incoming) { - let parent_name = &self.reverse_node_map[&parent_idx]; - if ancestors.insert(parent_name.clone()) { - queue.push_back(parent_idx); - } - } - } - - Ok(ancestors.into_iter().collect()) - } - - /// Fast implementation of minimal d-separator - pub fn minimal_dseparator(&self, start: String, end: String, include_latents: Option) -> PyResult>> { - let include_latents = include_latents.unwrap_or(false); - - // Check if nodes are adjacent - if self.are_adjacent(&start, &end)? { - return Err(pyo3::exceptions::PyValueError::new_err( - "No possible separators because start and end are adjacent" - )); - } - - // Get initial separator candidates (parents of both nodes) - let start_parents: HashSet = self.get_parents(start.clone())?.into_iter().collect(); - let end_parents: HashSet = self.get_parents(end.clone())?.into_iter().collect(); - - let mut separator: HashSet = start_parents.union(&end_parents).cloned().collect(); - - // Handle latents if not included - if !include_latents { - separator = self.resolve_latents(separator)?; - } - - // Remove start and end nodes from separator - separator.remove(&start); - separator.remove(&end); - - // Check if initial set can d-separate - if self.is_dconnected(&start, &end, Some(separator.iter().cloned().collect()))? { - return Ok(None); - } - - // Find minimal separator by removing unnecessary nodes - let mut minimal_separator = separator.clone(); - - for node in &separator { - let mut test_separator = minimal_separator.clone(); - test_separator.remove(node); - - if !self.is_dconnected(&start, &end, Some(test_separator.iter().cloned().collect()))? { - minimal_separator.remove(node); - } - } - - Ok(Some(minimal_separator)) - } - - /// Check if two nodes are adjacent - fn are_adjacent(&self, node1: &str, node2: &str) -> PyResult { - let idx1 = self.node_map.get(node1) - .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("Node {} not found", node1)))?; - let idx2 = self.node_map.get(node2) - .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("Node {} not found", node2)))?; - - Ok(self.graph.find_edge(*idx1, *idx2).is_some() || - self.graph.find_edge(*idx2, *idx1).is_some()) - } - - /// Resolve latents by replacing them with their parents - fn resolve_latents(&self, mut separator: HashSet) -> PyResult> { - while separator.iter().any(|node| self.latents.contains(node)) { - let mut new_separator = HashSet::new(); - for node in &separator { - if self.latents.contains(node) { - // Replace latent with its parents - let parents = self.get_parents(node.clone())?; - new_separator.extend(parents); - } else { - new_separator.insert(node.clone()); - } - } - separator = new_separator; - } - Ok(separator) - } - - /// Fast d-connection check - pub fn is_dconnected(&self, start: &str, end: &str, observed: Option>) -> PyResult { - let observed_set: HashSet = observed.unwrap_or_default().into_iter().collect(); - let ancestors = self.get_ancestors_of(observed_set.iter().cloned().collect())?; - - let mut visited = HashSet::new(); - let mut queue = VecDeque::new(); - - // Start BFS from start node - queue.push_back((start.to_string(), "up".to_string())); - - while let Some((node, direction)) = queue.pop_front() { - let state = format!("{}_{}", node, direction); - if visited.contains(&state) { - continue; - } - visited.insert(state); - - if node == end { - return Ok(true); - } - - if direction == "up" && !observed_set.contains(&node) { - // Can go up to parents - for parent in self.get_parents(node.clone())? { - queue.push_back((parent, "up".to_string())); - } - // Can go down to children - for child in self.get_children(node.clone())? { - queue.push_back((child, "down".to_string())); - } - } else if direction == "down" { - if !observed_set.contains(&node) { - // Can continue down to children - for child in self.get_children(node.clone())? { - queue.push_back((child, "down".to_string())); - } - } - if ancestors.contains(&node) { - // Can go up to parents (collider) - for parent in self.get_parents(node.clone())? { - queue.push_back((parent, "up".to_string())); - } - } - } - } - - Ok(false) - } - - /// Get all nodes in the graph - pub fn nodes(&self) -> Vec { - self.node_map.keys().cloned().collect() - } - - /// Get all edges in the graph - pub fn edges(&self) -> Vec<(String, String)> { - self.graph - .edge_indices() - .map(|edge_idx| { - let (source, target) = self.graph.edge_endpoints(edge_idx).unwrap(); - ( - self.reverse_node_map[&source].clone(), - self.reverse_node_map[&target].clone(), - ) - }) - .collect() - } - - /// Get number of nodes - pub fn node_count(&self) -> usize { - self.graph.node_count() - } - - /// Get number of edges - pub fn edge_count(&self) -> usize { - self.graph.edge_count() - } } \ No newline at end of file From 0b78985fe7843796d760f1ec4196597637ac5c94 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Fri, 20 Jun 2025 00:46:49 +0530 Subject: [PATCH 03/62] add python sdk --- Cargo.lock | 621 ++++++++++++++++++++++++++++++++++ python/pgmpy_rust/__init__.py | 7 + python/pgmpy_rust/dag.py | 87 +++++ python/pgmpy_rust/test.py | 9 + 4 files changed, 724 insertions(+) create mode 100644 Cargo.lock create mode 100644 python/pgmpy_rust/__init__.py create mode 100644 python/pgmpy_rust/dag.py create mode 100644 python/pgmpy_rust/test.py diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0ff9d7b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,621 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", + "rayon", +] + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.4", + "rayon", +] + +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.9.0", +] + +[[package]] +name = "pgmpy_rust" +version = "0.1.0" +dependencies = [ + "ahash", + "indexmap 2.9.0", + "petgraph", + "pyo3", + "pyo3-build-config", + "rustworkx-core", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "priority-queue" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bda9164fe05bc9225752d54aae413343c36f684380005398a6a8fde95fe785" +dependencies = [ + "autocfg", + "indexmap 1.9.3", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_pcg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-cond" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059f538b55efd2309c9794130bc149c6a553db90e9d99c2030785c82f0bd7df9" +dependencies = [ + "either", + "itertools", + "rayon", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustworkx-core" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529027dfaa8125aa61bb7736ae9484f41e8544f448af96918c8da6b1def7f57b" +dependencies = [ + "ahash", + "fixedbitset", + "hashbrown 0.14.5", + "indexmap 2.9.0", + "num-traits", + "petgraph", + "priority-queue", + "rand", + "rand_pcg", + "rayon", + "rayon-cond", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/python/pgmpy_rust/__init__.py b/python/pgmpy_rust/__init__.py new file mode 100644 index 0000000..43c3db6 --- /dev/null +++ b/python/pgmpy_rust/__init__.py @@ -0,0 +1,7 @@ +""" +High-performance graph operations for PGMPy using Rust +""" +from .dag import RustDAG + +__version__ = "0.1.0" +__all__ = ["RustDAG"] \ No newline at end of file diff --git a/python/pgmpy_rust/dag.py b/python/pgmpy_rust/dag.py new file mode 100644 index 0000000..28d1600 --- /dev/null +++ b/python/pgmpy_rust/dag.py @@ -0,0 +1,87 @@ +""" +Python wrapper for Rust DAG implementation with pgmpy compatibility +""" +from typing import List, Set, Optional, Union, Hashable +from .pgmpy_rust import RustDAG as _RustDAG + + +class RustDAG: + """ + A high-performance DAG implementation using Rust backend. + Compatible with pgmpy's DAG interface. + """ + + def __init__(self, ebunch=None, latents=None): + self._dag = _RustDAG() + self.latents = set(latents or []) + + if ebunch: + for u, v in ebunch: + self.add_edge(str(u), str(v)) + + def add_node(self, node: Hashable, latent: bool = False): + """Add a single node to the graph.""" + self._dag.add_node(str(node), latent) + if latent: + self.latents.add(str(node)) + + def add_nodes_from(self, nodes: List[Hashable], latent: Union[bool, List[bool]] = False): + """Add multiple nodes to the graph.""" + nodes_str = [str(n) for n in nodes] + + if isinstance(latent, bool): + latent_flags = [latent] * len(nodes) + else: + latent_flags = latent + + self._dag.add_nodes_from(nodes_str, latent_flags) + + for node, is_latent in zip(nodes_str, latent_flags): + if is_latent: + self.latents.add(node) + + def add_edge(self, u: Hashable, v: Hashable, weight: Optional[float] = None): + """Add an edge between two nodes.""" + self._dag.add_edge(str(u), str(v), weight) + + def get_parents(self, node: Hashable) -> List[str]: + """Get parents of a node.""" + return self._dag.get_parents(str(node)) + + def get_children(self, node: Hashable) -> List[str]: + """Get children of a node.""" + return self._dag.get_children(str(node)) + + def _get_ancestors_of(self, nodes: Union[str, List[Hashable]]) -> Set[str]: + """Get ancestors of given nodes (optimized Rust implementation).""" + if isinstance(nodes, str): + nodes = [nodes] + nodes_str = [str(n) for n in nodes] + return self._dag.get_ancestors_of(nodes_str) + + def minimal_dseparator(self, start: Hashable, end: Hashable, + include_latents: bool = False) -> Optional[Set[str]]: + """Find minimal d-separating set (optimized Rust implementation).""" + return self._dag.minimal_dseparator(str(start), str(end), include_latents) + + def is_dconnected(self, start: Hashable, end: Hashable, + observed: Optional[List[Hashable]] = None) -> bool: + """Check if two nodes are d-connected.""" + observed_str = [str(n) for n in observed] if observed else None + return self._dag.is_dconnected(str(start), str(end), observed_str) + + def nodes(self) -> List[str]: + """Get all nodes.""" + return self._dag.nodes() + + def edges(self) -> List[tuple]: + """Get all edges.""" + return self._dag.edges() + + def __len__(self) -> int: + """Return number of nodes.""" + return self._dag.node_count() + + def number_of_edges(self) -> int: + """Return number of edges.""" + return self._dag.edge_count() \ No newline at end of file diff --git a/python/pgmpy_rust/test.py b/python/pgmpy_rust/test.py new file mode 100644 index 0000000..8efc580 --- /dev/null +++ b/python/pgmpy_rust/test.py @@ -0,0 +1,9 @@ +from pgmpy_rust import RustDAG + +# Drop-in replacement for performance-critical operations +dag = RustDAG([('A', 'B'), ('B', 'C'), ('D', 'C')]) + +# These operations are now much faster +ancestors = dag._get_ancestors_of(['C']) +separator = dag.minimal_dseparator('A', 'D') +is_connected = dag.is_dconnected('A', 'D', ['B']) \ No newline at end of file From a7b97a6cfc62a2a448aa59ba2d0d9ebd2044ac7a Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Fri, 20 Jun 2025 00:51:43 +0530 Subject: [PATCH 04/62] added nodes and edges fetch in rust --- python/pgmpy_rust/test.py | 9 ++++++--- src/dag.rs | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/python/pgmpy_rust/test.py b/python/pgmpy_rust/test.py index 8efc580..73486c1 100644 --- a/python/pgmpy_rust/test.py +++ b/python/pgmpy_rust/test.py @@ -4,6 +4,9 @@ dag = RustDAG([('A', 'B'), ('B', 'C'), ('D', 'C')]) # These operations are now much faster -ancestors = dag._get_ancestors_of(['C']) -separator = dag.minimal_dseparator('A', 'D') -is_connected = dag.is_dconnected('A', 'D', ['B']) \ No newline at end of file +# ancestors = dag._get_ancestors_of(['C']) +# separator = dag.minimal_dseparator('A', 'D') +# is_connected = dag.is_dconnected('A', 'D', ['B']) + +print("dag:", dag.nodes()) +print("edges: ", dag.edges()) \ No newline at end of file diff --git a/src/dag.rs b/src/dag.rs index 7d1e047..152bd0a 100644 --- a/src/dag.rs +++ b/src/dag.rs @@ -66,6 +66,26 @@ impl RustDAG { Ok(()) } + /// Get all nodes in the graph + pub fn nodes(&self) -> Vec { + self.node_map.keys().cloned().collect() + } + + /// Get all edges in the graph + pub fn edges(&self) -> Vec<(String, String)> { + self.graph + .edge_indices() + .map(|edge_idx| { + let (source, target) = self.graph.edge_endpoints(edge_idx).unwrap(); + ( + self.reverse_node_map[&source].clone(), + self.reverse_node_map[&target].clone(), + ) + }) + .collect() + } + + //** Stop here for now **/ } \ No newline at end of file From 1e5e31434da27605e1d7adf074439bbfb790c9ea Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 22 Jun 2025 11:41:57 +0530 Subject: [PATCH 05/62] add additional rust implementations --- python/pgmpy_rust/dag.py | 18 ++++----- src/dag.rs | 80 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/python/pgmpy_rust/dag.py b/python/pgmpy_rust/dag.py index 28d1600..cd28ecd 100644 --- a/python/pgmpy_rust/dag.py +++ b/python/pgmpy_rust/dag.py @@ -59,16 +59,16 @@ def _get_ancestors_of(self, nodes: Union[str, List[Hashable]]) -> Set[str]: nodes_str = [str(n) for n in nodes] return self._dag.get_ancestors_of(nodes_str) - def minimal_dseparator(self, start: Hashable, end: Hashable, - include_latents: bool = False) -> Optional[Set[str]]: - """Find minimal d-separating set (optimized Rust implementation).""" - return self._dag.minimal_dseparator(str(start), str(end), include_latents) + # def minimal_dseparator(self, start: Hashable, end: Hashable, + # include_latents: bool = False) -> Optional[Set[str]]: + # """Find minimal d-separating set (optimized Rust implementation).""" + # return self._dag.minimal_dseparator(str(start), str(end), include_latents) - def is_dconnected(self, start: Hashable, end: Hashable, - observed: Optional[List[Hashable]] = None) -> bool: - """Check if two nodes are d-connected.""" - observed_str = [str(n) for n in observed] if observed else None - return self._dag.is_dconnected(str(start), str(end), observed_str) + # def is_dconnected(self, start: Hashable, end: Hashable, + # observed: Optional[List[Hashable]] = None) -> bool: + # """Check if two nodes are d-connected.""" + # observed_str = [str(n) for n in observed] if observed else None + # return self._dag.is_dconnected(str(start), str(end), observed_str) def nodes(self) -> List[str]: """Get all nodes.""" diff --git a/src/dag.rs b/src/dag.rs index 152bd0a..0699ae9 100644 --- a/src/dag.rs +++ b/src/dag.rs @@ -1,6 +1,8 @@ +use ahash::AHashSet; +use petgraph::Direction; use pyo3::prelude::*; use rustworkx_core::petgraph::graph::{DiGraph, NodeIndex}; -use std::collections::{HashMap, HashSet}; +use std::collections::{HashMap, HashSet, VecDeque}; #[pyclass] pub struct RustDAG { @@ -25,7 +27,7 @@ impl RustDAG { /// Add a single node to the graph pub fn add_node(&mut self, node: String, latent: Option) -> PyResult<()> { if !self.node_map.contains_key(&node) { - let idx = self.graph.add_node(node.clone()); + let idx: NodeIndex = self.graph.add_node(node.clone()); self.node_map.insert(node.clone(), idx); self.reverse_node_map.insert(idx, node.clone()); @@ -38,7 +40,7 @@ impl RustDAG { /// Add multiple nodes to the graph pub fn add_nodes_from(&mut self, nodes: Vec, latent: Option>) -> PyResult<()> { - let latent_flags = latent.unwrap_or_else(|| vec![false; nodes.len()]); + let latent_flags: Vec = latent.unwrap_or_else(|| vec![false; nodes.len()]); if nodes.len() != latent_flags.len() { return Err(pyo3::exceptions::PyValueError::new_err( @@ -59,13 +61,69 @@ impl RustDAG { self.add_node(u.clone(), None)?; self.add_node(v.clone(), None)?; - let u_idx = self.node_map[&u]; - let v_idx = self.node_map[&v]; + let u_idx: NodeIndex = self.node_map[&u]; + let v_idx: NodeIndex = self.node_map[&v]; self.graph.add_edge(u_idx, v_idx, weight.unwrap_or(1.0)); Ok(()) } + /// Get parents of a node + pub fn get_parents(&self, node: String) -> PyResult> { + let node_idx = self.node_map.get(&node) + .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("Node {} not found", node)))?; + + let parents: Vec = self.graph + .neighbors_directed(*node_idx, Direction::Incoming) + .map(|idx| self.reverse_node_map[&idx].clone()) + .collect(); + + Ok(parents) + } + + /// Get children of a node + pub fn get_children(&self, node: String) -> PyResult> { + let node_idx = self.node_map.get(&node) + .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("Node {} not found", node)))?; + + let children: Vec = self.graph + .neighbors_directed(*node_idx, Direction::Outgoing) + .map(|idx: NodeIndex| self.reverse_node_map[&idx].clone()) + .collect(); + + Ok(children) + } + + /// Get all ancestors of given nodes (optimized Rust implementation) + pub fn get_ancestors_of(&self, nodes: Vec) -> PyResult> { + let mut ancestors: AHashSet = AHashSet::new(); + let mut queue: VecDeque = VecDeque::new(); + + // Initialize queue with input nodes + for node in &nodes { + if let Some(&node_idx) = self.node_map.get(node) { + queue.push_back(node_idx); + ancestors.insert(node.clone()); + } else { + return Err(pyo3::exceptions::PyValueError::new_err( + format!("Node {} not in graph", node) + )); + } + } + + // BFS to find all ancestors + while let Some(current_idx) = queue.pop_front() { + for parent_idx in self.graph.neighbors_directed(current_idx, Direction::Incoming) { + let parent_name = &self.reverse_node_map[&parent_idx]; + if ancestors.insert(parent_name.clone()) { + queue.push_back(parent_idx); + } + } + } + + Ok(ancestors.into_iter().collect()) + } + /// Get all nodes in the graph pub fn nodes(&self) -> Vec { self.node_map.keys().cloned().collect() @@ -85,7 +143,15 @@ impl RustDAG { .collect() } - - //** Stop here for now **/ + /// Get number of nodes + pub fn node_count(&self) -> usize { + self.graph.node_count() + } + + /// Get number of edges + pub fn edge_count(&self) -> usize { + self.graph.edge_count() + } + } \ No newline at end of file From b4ebea76e59e1abefb1d8742f54d6c8c92723ca7 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Mon, 23 Jun 2025 19:13:46 +0530 Subject: [PATCH 06/62] Rename crate to causalgraphs and wire up Python bindings --- Cargo.lock | 24 +++++++++---------- Cargo.toml | 4 ++-- pyproject.toml | 2 +- .../{pgmpy_rust => causalgraphs}/__init__.py | 0 python/{pgmpy_rust => causalgraphs}/dag.py | 2 +- python/causalgraphs/test.py | 15 ++++++++++++ python/pgmpy_rust/test.py | 12 ---------- src/lib.rs | 2 +- 8 files changed, 32 insertions(+), 29 deletions(-) rename python/{pgmpy_rust => causalgraphs}/__init__.py (100%) rename python/{pgmpy_rust => causalgraphs}/dag.py (98%) create mode 100644 python/causalgraphs/test.py delete mode 100644 python/pgmpy_rust/test.py diff --git a/Cargo.lock b/Cargo.lock index 0ff9d7b..ede296a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,18 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "causalgraphs" +version = "0.1.0" +dependencies = [ + "ahash", + "indexmap 2.9.0", + "petgraph", + "pyo3", + "pyo3-build-config", + "rustworkx-core", +] + [[package]] name = "cfg-if" version = "1.0.1" @@ -243,18 +255,6 @@ dependencies = [ "indexmap 2.9.0", ] -[[package]] -name = "pgmpy_rust" -version = "0.1.0" -dependencies = [ - "ahash", - "indexmap 2.9.0", - "petgraph", - "pyo3", - "pyo3-build-config", - "rustworkx-core", -] - [[package]] name = "portable-atomic" version = "1.11.1" diff --git a/Cargo.toml b/Cargo.toml index 0c322c9..7f00fe3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "pgmpy_rust" +name = "causalgraphs" version = "0.1.0" edition = "2021" [lib] -name = "pgmpy_rust" +name = "causalgraphs" crate-type = ["cdylib"] [dependencies] diff --git a/pyproject.toml b/pyproject.toml index 01c52c4..1588170 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["maturin>=1.0,<2.0"] build-backend = "maturin" [project] -name = "pgmpy-rust" +name = "causalgraphs" version = "0.1.0" description = "High-performance graph operations for PGMPy using Rust" authors = [ diff --git a/python/pgmpy_rust/__init__.py b/python/causalgraphs/__init__.py similarity index 100% rename from python/pgmpy_rust/__init__.py rename to python/causalgraphs/__init__.py diff --git a/python/pgmpy_rust/dag.py b/python/causalgraphs/dag.py similarity index 98% rename from python/pgmpy_rust/dag.py rename to python/causalgraphs/dag.py index cd28ecd..bcf3bf1 100644 --- a/python/pgmpy_rust/dag.py +++ b/python/causalgraphs/dag.py @@ -2,7 +2,7 @@ Python wrapper for Rust DAG implementation with pgmpy compatibility """ from typing import List, Set, Optional, Union, Hashable -from .pgmpy_rust import RustDAG as _RustDAG +from .causalgraphs import RustDAG as _RustDAG class RustDAG: diff --git a/python/causalgraphs/test.py b/python/causalgraphs/test.py new file mode 100644 index 0000000..77539c5 --- /dev/null +++ b/python/causalgraphs/test.py @@ -0,0 +1,15 @@ +from causalgraphs import RustDAG + +# Drop-in replacement for performance-critical operations +dag = RustDAG([('A', 'B'), ('B', 'C'), ('D', 'C')]) + +print("dag:", dag.nodes()) +print("edges: ", dag.edges()) + + +# test get_children and parents +dag.add_node('E') +dag.add_edge('E', 'A') +print("Children of A:", dag.get_children('A')) +print("Parents of C:", dag.get_parents('C')) +print("Ancestors of C:", dag._get_ancestors_of('C')) \ No newline at end of file diff --git a/python/pgmpy_rust/test.py b/python/pgmpy_rust/test.py deleted file mode 100644 index 73486c1..0000000 --- a/python/pgmpy_rust/test.py +++ /dev/null @@ -1,12 +0,0 @@ -from pgmpy_rust import RustDAG - -# Drop-in replacement for performance-critical operations -dag = RustDAG([('A', 'B'), ('B', 'C'), ('D', 'C')]) - -# These operations are now much faster -# ancestors = dag._get_ancestors_of(['C']) -# separator = dag.minimal_dseparator('A', 'D') -# is_connected = dag.is_dconnected('A', 'D', ['B']) - -print("dag:", dag.nodes()) -print("edges: ", dag.edges()) \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index c4fda99..a6ba24c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ mod utils; use dag::RustDAG; #[pymodule] -fn pgmpy_rust(_py: Python, m: &PyModule) -> PyResult<()> { +fn causalgraphs(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; Ok(()) } \ No newline at end of file From e1d3898551155feb7a3c9574684e2569b6273751 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Mon, 23 Jun 2025 19:38:31 +0530 Subject: [PATCH 07/62] Add initial implementation of causalgraphs with RustDAG and Python bindings --- .gitignore | 2 + Cargo.lock | 621 ++++++++++++++++++++++++++++++++ Cargo.toml | 23 ++ pyproject.toml | 31 ++ python/causalgraphs/__init__.py | 7 + python/causalgraphs/dag.py | 87 +++++ python/causalgraphs/test.py | 15 + src/dag.rs | 157 ++++++++ src/lib.rs | 11 + 9 files changed, 954 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 pyproject.toml create mode 100644 python/causalgraphs/__init__.py create mode 100644 python/causalgraphs/dag.py create mode 100644 python/causalgraphs/test.py create mode 100644 src/dag.rs create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore index cfbeb4f..db06c0f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ target/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +venv/ +.venv/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ede296a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,621 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "causalgraphs" +version = "0.1.0" +dependencies = [ + "ahash", + "indexmap 2.9.0", + "petgraph", + "pyo3", + "pyo3-build-config", + "rustworkx-core", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", + "rayon", +] + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown 0.15.4", + "rayon", +] + +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.9.0", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "priority-queue" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bda9164fe05bc9225752d54aae413343c36f684380005398a6a8fde95fe785" +dependencies = [ + "autocfg", + "indexmap 1.9.3", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_pcg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-cond" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059f538b55efd2309c9794130bc149c6a553db90e9d99c2030785c82f0bd7df9" +dependencies = [ + "either", + "itertools", + "rayon", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustworkx-core" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529027dfaa8125aa61bb7736ae9484f41e8544f448af96918c8da6b1def7f57b" +dependencies = [ + "ahash", + "fixedbitset", + "hashbrown 0.14.5", + "indexmap 2.9.0", + "num-traits", + "petgraph", + "priority-queue", + "rand", + "rand_pcg", + "rayon", + "rayon-cond", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "2.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7f00fe3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "causalgraphs" +version = "0.1.0" +edition = "2021" + +[lib] +name = "causalgraphs" +crate-type = ["cdylib"] + +[dependencies] +# Core graph library from rustworkx (pure Rust, on crates.io) +rustworkx-core = "0.14" + +# You already imported petgraph and ahash for utilities: +petgraph = "0.6" +ahash = "0.8" +indexmap = "2.0" + +# PyO3 for the Python binding +pyo3 = { version = "0.20", features = ["extension-module"] } + +[build-dependencies] +pyo3-build-config = "0.20" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1588170 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[project] +name = "causalgraphs" +version = "0.1.0" +description = "High-performance graph operations for PGMPy using Rust" +authors = [ + {name = "Your Name", email = "your.email@example.com"}, +] +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [ + "rustworkx>=0.14.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=6.0", + "numpy", + "networkx", +] + +[tool.maturin] +features = ["pyo3/extension-module"] +python-source = "python" \ No newline at end of file diff --git a/python/causalgraphs/__init__.py b/python/causalgraphs/__init__.py new file mode 100644 index 0000000..43c3db6 --- /dev/null +++ b/python/causalgraphs/__init__.py @@ -0,0 +1,7 @@ +""" +High-performance graph operations for PGMPy using Rust +""" +from .dag import RustDAG + +__version__ = "0.1.0" +__all__ = ["RustDAG"] \ No newline at end of file diff --git a/python/causalgraphs/dag.py b/python/causalgraphs/dag.py new file mode 100644 index 0000000..bcf3bf1 --- /dev/null +++ b/python/causalgraphs/dag.py @@ -0,0 +1,87 @@ +""" +Python wrapper for Rust DAG implementation with pgmpy compatibility +""" +from typing import List, Set, Optional, Union, Hashable +from .causalgraphs import RustDAG as _RustDAG + + +class RustDAG: + """ + A high-performance DAG implementation using Rust backend. + Compatible with pgmpy's DAG interface. + """ + + def __init__(self, ebunch=None, latents=None): + self._dag = _RustDAG() + self.latents = set(latents or []) + + if ebunch: + for u, v in ebunch: + self.add_edge(str(u), str(v)) + + def add_node(self, node: Hashable, latent: bool = False): + """Add a single node to the graph.""" + self._dag.add_node(str(node), latent) + if latent: + self.latents.add(str(node)) + + def add_nodes_from(self, nodes: List[Hashable], latent: Union[bool, List[bool]] = False): + """Add multiple nodes to the graph.""" + nodes_str = [str(n) for n in nodes] + + if isinstance(latent, bool): + latent_flags = [latent] * len(nodes) + else: + latent_flags = latent + + self._dag.add_nodes_from(nodes_str, latent_flags) + + for node, is_latent in zip(nodes_str, latent_flags): + if is_latent: + self.latents.add(node) + + def add_edge(self, u: Hashable, v: Hashable, weight: Optional[float] = None): + """Add an edge between two nodes.""" + self._dag.add_edge(str(u), str(v), weight) + + def get_parents(self, node: Hashable) -> List[str]: + """Get parents of a node.""" + return self._dag.get_parents(str(node)) + + def get_children(self, node: Hashable) -> List[str]: + """Get children of a node.""" + return self._dag.get_children(str(node)) + + def _get_ancestors_of(self, nodes: Union[str, List[Hashable]]) -> Set[str]: + """Get ancestors of given nodes (optimized Rust implementation).""" + if isinstance(nodes, str): + nodes = [nodes] + nodes_str = [str(n) for n in nodes] + return self._dag.get_ancestors_of(nodes_str) + + # def minimal_dseparator(self, start: Hashable, end: Hashable, + # include_latents: bool = False) -> Optional[Set[str]]: + # """Find minimal d-separating set (optimized Rust implementation).""" + # return self._dag.minimal_dseparator(str(start), str(end), include_latents) + + # def is_dconnected(self, start: Hashable, end: Hashable, + # observed: Optional[List[Hashable]] = None) -> bool: + # """Check if two nodes are d-connected.""" + # observed_str = [str(n) for n in observed] if observed else None + # return self._dag.is_dconnected(str(start), str(end), observed_str) + + def nodes(self) -> List[str]: + """Get all nodes.""" + return self._dag.nodes() + + def edges(self) -> List[tuple]: + """Get all edges.""" + return self._dag.edges() + + def __len__(self) -> int: + """Return number of nodes.""" + return self._dag.node_count() + + def number_of_edges(self) -> int: + """Return number of edges.""" + return self._dag.edge_count() \ No newline at end of file diff --git a/python/causalgraphs/test.py b/python/causalgraphs/test.py new file mode 100644 index 0000000..77539c5 --- /dev/null +++ b/python/causalgraphs/test.py @@ -0,0 +1,15 @@ +from causalgraphs import RustDAG + +# Drop-in replacement for performance-critical operations +dag = RustDAG([('A', 'B'), ('B', 'C'), ('D', 'C')]) + +print("dag:", dag.nodes()) +print("edges: ", dag.edges()) + + +# test get_children and parents +dag.add_node('E') +dag.add_edge('E', 'A') +print("Children of A:", dag.get_children('A')) +print("Parents of C:", dag.get_parents('C')) +print("Ancestors of C:", dag._get_ancestors_of('C')) \ No newline at end of file diff --git a/src/dag.rs b/src/dag.rs new file mode 100644 index 0000000..0699ae9 --- /dev/null +++ b/src/dag.rs @@ -0,0 +1,157 @@ +use ahash::AHashSet; +use petgraph::Direction; +use pyo3::prelude::*; +use rustworkx_core::petgraph::graph::{DiGraph, NodeIndex}; +use std::collections::{HashMap, HashSet, VecDeque}; + +#[pyclass] +pub struct RustDAG { + graph: DiGraph, + node_map: HashMap, + reverse_node_map: HashMap, + latents: HashSet, +} + +#[pymethods] +impl RustDAG { + #[new] + pub fn new() -> Self { + RustDAG { + graph: DiGraph::new(), + node_map: HashMap::new(), + reverse_node_map: HashMap::new(), + latents: HashSet::new(), + } + } + + /// Add a single node to the graph + pub fn add_node(&mut self, node: String, latent: Option) -> PyResult<()> { + if !self.node_map.contains_key(&node) { + let idx: NodeIndex = self.graph.add_node(node.clone()); + self.node_map.insert(node.clone(), idx); + self.reverse_node_map.insert(idx, node.clone()); + + if latent.unwrap_or(false) { + self.latents.insert(node); + } + } + Ok(()) + } + + /// Add multiple nodes to the graph + pub fn add_nodes_from(&mut self, nodes: Vec, latent: Option>) -> PyResult<()> { + let latent_flags: Vec = latent.unwrap_or_else(|| vec![false; nodes.len()]); + + if nodes.len() != latent_flags.len() { + return Err(pyo3::exceptions::PyValueError::new_err( + "Length of nodes and latent flags must match" + )); + } + + for (node, is_latent) in nodes.iter().zip(latent_flags.iter()) { + self.add_node(node.clone(), Some(*is_latent))?; + } + Ok(()) + } + + + /// Add an edge between two nodes + pub fn add_edge(&mut self, u: String, v: String, weight: Option) -> PyResult<()> { + // Add nodes if they don't exist + self.add_node(u.clone(), None)?; + self.add_node(v.clone(), None)?; + + let u_idx: NodeIndex = self.node_map[&u]; + let v_idx: NodeIndex = self.node_map[&v]; + + self.graph.add_edge(u_idx, v_idx, weight.unwrap_or(1.0)); + Ok(()) + } + + /// Get parents of a node + pub fn get_parents(&self, node: String) -> PyResult> { + let node_idx = self.node_map.get(&node) + .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("Node {} not found", node)))?; + + let parents: Vec = self.graph + .neighbors_directed(*node_idx, Direction::Incoming) + .map(|idx| self.reverse_node_map[&idx].clone()) + .collect(); + + Ok(parents) + } + + /// Get children of a node + pub fn get_children(&self, node: String) -> PyResult> { + let node_idx = self.node_map.get(&node) + .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("Node {} not found", node)))?; + + let children: Vec = self.graph + .neighbors_directed(*node_idx, Direction::Outgoing) + .map(|idx: NodeIndex| self.reverse_node_map[&idx].clone()) + .collect(); + + Ok(children) + } + + /// Get all ancestors of given nodes (optimized Rust implementation) + pub fn get_ancestors_of(&self, nodes: Vec) -> PyResult> { + let mut ancestors: AHashSet = AHashSet::new(); + let mut queue: VecDeque = VecDeque::new(); + + // Initialize queue with input nodes + for node in &nodes { + if let Some(&node_idx) = self.node_map.get(node) { + queue.push_back(node_idx); + ancestors.insert(node.clone()); + } else { + return Err(pyo3::exceptions::PyValueError::new_err( + format!("Node {} not in graph", node) + )); + } + } + + // BFS to find all ancestors + while let Some(current_idx) = queue.pop_front() { + for parent_idx in self.graph.neighbors_directed(current_idx, Direction::Incoming) { + let parent_name = &self.reverse_node_map[&parent_idx]; + if ancestors.insert(parent_name.clone()) { + queue.push_back(parent_idx); + } + } + } + + Ok(ancestors.into_iter().collect()) + } + + /// Get all nodes in the graph + pub fn nodes(&self) -> Vec { + self.node_map.keys().cloned().collect() + } + + /// Get all edges in the graph + pub fn edges(&self) -> Vec<(String, String)> { + self.graph + .edge_indices() + .map(|edge_idx| { + let (source, target) = self.graph.edge_endpoints(edge_idx).unwrap(); + ( + self.reverse_node_map[&source].clone(), + self.reverse_node_map[&target].clone(), + ) + }) + .collect() + } + + /// Get number of nodes + pub fn node_count(&self) -> usize { + self.graph.node_count() + } + + /// Get number of edges + pub fn edge_count(&self) -> usize { + self.graph.edge_count() + } + + +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..aed7e8d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,11 @@ +use pyo3::prelude::*; + +mod dag; + +use dag::RustDAG; + +#[pymodule] +fn causalgraphs(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} \ No newline at end of file From 30426816594251619c7f9176de23e21222c36770 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Mon, 23 Jun 2025 20:19:37 +0530 Subject: [PATCH 08/62] add requirements.txt --- .gitignore | 4 +++- python/requirements.txt | 4 ++++ python/causalgraphs/test.py => test.py | 0 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 python/requirements.txt rename python/causalgraphs/test.py => test.py (100%) diff --git a/.gitignore b/.gitignore index db06c0f..9258eff 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,6 @@ target/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ venv/ -.venv/ \ No newline at end of file +.venv/ + +*pyc* \ No newline at end of file diff --git a/python/requirements.txt b/python/requirements.txt new file mode 100644 index 0000000..22c35bb --- /dev/null +++ b/python/requirements.txt @@ -0,0 +1,4 @@ +maturin==1.8.7 +numpy==2.2.6 +rustworkx==0.16.0 +tomli==2.2.1 diff --git a/python/causalgraphs/test.py b/test.py similarity index 100% rename from python/causalgraphs/test.py rename to test.py From bc39e80e75539248d315f2fcec8bfc8a2568df22 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Thu, 26 Jun 2025 22:30:02 +0530 Subject: [PATCH 09/62] update README.md --- Cargo.lock | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + README.md | 54 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index ede296a..e772c67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,12 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bumpalo" +version = "3.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" + [[package]] name = "causalgraphs" version = "0.1.0" @@ -43,6 +49,7 @@ dependencies = [ "pyo3", "pyo3-build-config", "rustworkx-core", + "wasm-bindgen", ] [[package]] @@ -198,6 +205,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + [[package]] name = "memoffset" version = "0.9.1" @@ -446,6 +459,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + [[package]] name = "rustworkx-core" version = "0.14.2" @@ -527,6 +546,64 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 7f00fe3..3dd5a95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["cdylib"] [dependencies] # Core graph library from rustworkx (pure Rust, on crates.io) rustworkx-core = "0.14" +wasm-bindgen = "0.2" # You already imported petgraph and ahash for utilities: petgraph = "0.6" diff --git a/README.md b/README.md index 1160279..5021de3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,56 @@ # causalgraphs Rust implemention of causal graphs + + +## Building & Installing the Python Extension + +First, make sure you have Rust installed on your system. If you haven't already done so, try following the instructions [here](https://www.rust-lang.org/tools/install). + + +This project uses [PyO3](https://pyo3.rs/v0.25.1/) to create Python bindings for the Rust code. +We use [Maturin](https://github.com/PyO3/maturin) build tool to compile(.so/.dll) our Rust core into a native Python module(.whl) and install it into a local venv. + +1. **Clone the repo** + ```bash + git clone https://github.com/pgmpy/causalgraphs.git + cd causalgraphs + ``` + +2. **Create & activate a venv** + ```bash + cd python + python3 -m venv .venv + source .venv/bin/activate + ``` +3. **Install Python requirements** + ```bash + pip install -r requirements.txt + ``` +4. **Build & install the Rust extension** + From the project root (where Cargo.toml and pyproject.toml live): + ```bash + maturin build --release + ``` + This will compile the Rust code and generate a .whl file in the target/wheels/ directory. You can then install it: + + ```bash + pip install target/wheels/causalgraphs-0.1.0-*.whl + ``` + + (Replace your_package_name-0.1.0-cp3x-cp3x-your_platform.whl with the actual filename.) + + ```bash + Note: The `causalgraphs` package is not yet published on PyPI. You must install it from the locally built wheel as shown above while the project is in active development. + ``` +--- + +5. **🚀 Usage** + + Once installed, you can import the `DAG` from `causalgraphs` and use it just like a regular Python class. + ```bash + >>> from causalgraphs import RustDAG + >>> dag = RustDAG() + >>> dag.add_node("A") + >>> dag.nodes() + ["A"] + ``` From 677ed4fbe3027ca59d48ddbd42e08045a00dff4f Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 28 Jun 2025 16:56:57 +0530 Subject: [PATCH 10/62] conditional compilation: create python, js cargo features --- src/dag.rs | 215 +++++++++++++++++++++++++++++++++++++++++++---------- src/lib.rs | 20 ++++- 2 files changed, 196 insertions(+), 39 deletions(-) diff --git a/src/dag.rs b/src/dag.rs index 0699ae9..038ed43 100644 --- a/src/dag.rs +++ b/src/dag.rs @@ -1,20 +1,32 @@ -use ahash::AHashSet; use petgraph::Direction; -use pyo3::prelude::*; use rustworkx_core::petgraph::graph::{DiGraph, NodeIndex}; use std::collections::{HashMap, HashSet, VecDeque}; -#[pyclass] -pub struct RustDAG { +// Conditional imports based on features +#[cfg(feature = "python")] +use pyo3::prelude::*; + +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +#[cfg(feature = "wasm")] +use serde::{Deserialize, Serialize}; + +// Core DAG structure - shared between Python and WASM +#[derive(Clone)] +#[cfg_attr(feature = "python", pyclass)] +#[cfg_attr(feature = "wasm", wasm_bindgen)] +#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))] +pub struct RustDAG { graph: DiGraph, node_map: HashMap, reverse_node_map: HashMap, latents: HashSet, } -#[pymethods] -impl RustDAG { - #[new] +// Core implementation (shared) +impl RustDAG { + /// Create a new empty DAG pub fn new() -> Self { RustDAG { graph: DiGraph::new(), @@ -24,8 +36,8 @@ impl RustDAG { } } - /// Add a single node to the graph - pub fn add_node(&mut self, node: String, latent: Option) -> PyResult<()> { + /// Add a single node to the graph (internal implementation) + fn add_node_internal(&mut self, node: String, latent: Option) -> Result<(), String> { if !self.node_map.contains_key(&node) { let idx: NodeIndex = self.graph.add_node(node.clone()); self.node_map.insert(node.clone(), idx); @@ -38,28 +50,25 @@ impl RustDAG { Ok(()) } - /// Add multiple nodes to the graph - pub fn add_nodes_from(&mut self, nodes: Vec, latent: Option>) -> PyResult<()> { + /// Add multiple nodes to the graph (internal implementation) + fn add_nodes_from_internal(&mut self, nodes: Vec, latent: Option>) -> Result<(), String> { let latent_flags: Vec = latent.unwrap_or_else(|| vec![false; nodes.len()]); if nodes.len() != latent_flags.len() { - return Err(pyo3::exceptions::PyValueError::new_err( - "Length of nodes and latent flags must match" - )); + return Err("Length of nodes and latent flags must match".to_string()); } for (node, is_latent) in nodes.iter().zip(latent_flags.iter()) { - self.add_node(node.clone(), Some(*is_latent))?; + self.add_node_internal(node.clone(), Some(*is_latent))?; } Ok(()) } - - /// Add an edge between two nodes - pub fn add_edge(&mut self, u: String, v: String, weight: Option) -> PyResult<()> { + /// Add an edge between two nodes (internal implementation) + fn add_edge_internal(&mut self, u: String, v: String, weight: Option) -> Result<(), String> { // Add nodes if they don't exist - self.add_node(u.clone(), None)?; - self.add_node(v.clone(), None)?; + self.add_node_internal(u.clone(), None)?; + self.add_node_internal(v.clone(), None)?; let u_idx: NodeIndex = self.node_map[&u]; let v_idx: NodeIndex = self.node_map[&v]; @@ -68,10 +77,10 @@ impl RustDAG { Ok(()) } - /// Get parents of a node - pub fn get_parents(&self, node: String) -> PyResult> { - let node_idx = self.node_map.get(&node) - .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("Node {} not found", node)))?; + /// Get parents of a node (internal implementation) + fn get_parents_internal(&self, node: &str) -> Result, String> { + let node_idx = self.node_map.get(node) + .ok_or_else(|| format!("Node {} not found", node))?; let parents: Vec = self.graph .neighbors_directed(*node_idx, Direction::Incoming) @@ -81,10 +90,10 @@ impl RustDAG { Ok(parents) } - /// Get children of a node - pub fn get_children(&self, node: String) -> PyResult> { - let node_idx = self.node_map.get(&node) - .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("Node {} not found", node)))?; + /// Get children of a node (internal implementation) + fn get_children_internal(&self, node: &str) -> Result, String> { + let node_idx = self.node_map.get(node) + .ok_or_else(|| format!("Node {} not found", node))?; let children: Vec = self.graph .neighbors_directed(*node_idx, Direction::Outgoing) @@ -94,9 +103,9 @@ impl RustDAG { Ok(children) } - /// Get all ancestors of given nodes (optimized Rust implementation) - pub fn get_ancestors_of(&self, nodes: Vec) -> PyResult> { - let mut ancestors: AHashSet = AHashSet::new(); + /// Get all ancestors of given nodes (internal implementation) + fn get_ancestors_of_internal(&self, nodes: Vec) -> Result, String> { + let mut ancestors: HashSet = HashSet::new(); let mut queue: VecDeque = VecDeque::new(); // Initialize queue with input nodes @@ -105,9 +114,7 @@ impl RustDAG { queue.push_back(node_idx); ancestors.insert(node.clone()); } else { - return Err(pyo3::exceptions::PyValueError::new_err( - format!("Node {} not in graph", node) - )); + return Err(format!("Node {} not in graph", node)); } } @@ -125,12 +132,12 @@ impl RustDAG { } /// Get all nodes in the graph - pub fn nodes(&self) -> Vec { + fn nodes_internal(&self) -> Vec { self.node_map.keys().cloned().collect() } /// Get all edges in the graph - pub fn edges(&self) -> Vec<(String, String)> { + fn edges_internal(&self) -> Vec<(String, String)> { self.graph .edge_indices() .map(|edge_idx| { @@ -144,14 +151,146 @@ impl RustDAG { } /// Get number of nodes - pub fn node_count(&self) -> usize { + fn node_count_internal(&self) -> usize { self.graph.node_count() } /// Get number of edges - pub fn edge_count(&self) -> usize { + fn edge_count_internal(&self) -> usize { self.graph.edge_count() } +} + +// Python-specific methods +#[cfg(feature = "python")] +#[pymethods] +impl RustDAG { + #[new] + pub fn py_new() -> Self { + Self::new() + } + + /// Add a single node to the graph + pub fn add_node(&mut self, node: String, latent: Option) -> PyResult<()> { + self.add_node_internal(node, latent) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e)) + } + + /// Add multiple nodes to the graph + pub fn add_nodes_from(&mut self, nodes: Vec, latent: Option>) -> PyResult<()> { + self.add_nodes_from_internal(nodes, latent) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e)) + } + /// Add an edge between two nodes + pub fn add_edge(&mut self, u: String, v: String, weight: Option) -> PyResult<()> { + self.add_edge_internal(u, v, weight) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e)) + } + /// Get parents of a node + pub fn get_parents(&self, node: String) -> PyResult> { + self.get_parents_internal(&node) + .map_err(|e| pyo3::exceptions::PyKeyError::new_err(e)) + } + + /// Get children of a node + pub fn get_children(&self, node: String) -> PyResult> { + self.get_children_internal(&node) + .map_err(|e| pyo3::exceptions::PyKeyError::new_err(e)) + } + + /// Get all ancestors of given nodes + pub fn get_ancestors_of(&self, nodes: Vec) -> PyResult> { + self.get_ancestors_of_internal(nodes) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e)) + } + + /// Get all nodes in the graph + pub fn nodes(&self) -> Vec { + self.nodes_internal() + } + + /// Get all edges in the graph + pub fn edges(&self) -> Vec<(String, String)> { + self.edges_internal() + } + + /// Get number of nodes + pub fn node_count(&self) -> usize { + self.node_count_internal() + } + + /// Get number of edges + pub fn edge_count(&self) -> usize { + self.edge_count_internal() + } +} + +// WASM-specific methods +#[cfg(feature = "wasm")] +#[wasm_bindgen] +impl RustDAG { + #[wasm_bindgen(constructor)] + pub fn js_new() -> RustDAG { + Self::new() + } + + #[wasm_bindgen(js_name = addNode)] + pub fn js_add_node(&mut self, node: String, latent: Option) -> Result<(), JsValue> { + self.add_node_internal(node, latent) + .map_err(|e| JsValue::from_str(&e)) + } + + #[wasm_bindgen(js_name = addNodesFrom)] + pub fn js_add_nodes_from(&mut self, nodes: Vec, latent: Option>) -> Result<(), JsValue> { + self.add_nodes_from_internal(nodes, latent) + .map_err(|e| JsValue::from_str(&e)) + } + + #[wasm_bindgen(js_name = addEdge)] + pub fn js_add_edge(&mut self, u: String, v: String, weight: Option) -> Result<(), JsValue> { + self.add_edge_internal(u, v, weight) + .map_err(|e| JsValue::from_str(&e)) + } + + #[wasm_bindgen(js_name = getParents, catch)] + pub fn js_get_parents(&self, node: String) -> Result, JsValue> { + self.get_parents_internal(&node) + .map_err(|e| JsValue::from_str(&e)) + } + + #[wasm_bindgen(js_name = getChildren, catch)] + pub fn js_get_children(&self, node: String) -> Result, JsValue> { + self.get_children_internal(&node) + .map_err(|e| JsValue::from_str(&e)) + } + + #[wasm_bindgen(js_name = getAncestorsOf, catch)] + pub fn js_get_ancestors_of(&mut self, nodes: Vec) -> Result, JsValue> { + let ancestors = self.get_ancestors_of_internal(nodes) + .map_err(|e| JsValue::from_str(&e))?; + Ok(ancestors.into_iter().collect()) + } + + #[wasm_bindgen(js_name = nodes)] + pub fn js_nodes(&self) -> Vec { + self.nodes_internal() + } + + #[wasm_bindgen(js_name = edges, getter)] + pub fn js_edges(&self) -> JsValue { + let edges = self.edges_internal(); + serde_wasm_bindgen::to_value(&edges).unwrap() + } + + #[wasm_bindgen(js_name = nodeCount, getter)] + pub fn js_node_count(&self) -> usize { + self.node_count_internal() + } + + #[wasm_bindgen(js_name = edgeCount, getter)] + pub fn js_edge_count(&self) -> usize { + self.edge_count_internal() + } } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index aed7e8d..a381089 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,29 @@ +// Python bindings +#[cfg(feature = "python")] use pyo3::prelude::*; +// WASM bindings +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + mod dag; -use dag::RustDAG; +// Export the DAG for different targets +pub use dag::RustDAG; +// Python module +#[cfg(feature = "python")] #[pymodule] fn causalgraphs(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; Ok(()) +} + +// WASM initialization +#[cfg(feature = "wasm")] +#[wasm_bindgen(start)] +pub fn main() { + // Optional: Set up panic hook for better error messages in browser + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); } \ No newline at end of file From 15efc47e60542b15df089888cf4c7714e95a420e Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 28 Jun 2025 16:57:41 +0530 Subject: [PATCH 11/62] fix get random wasm compilation --- .cargo/config.toml | 2 + Cargo.lock | 102 ++++++++++++++++++++++++++++++++++++++++----- Cargo.toml | 38 +++++++++++++---- 3 files changed, 123 insertions(+), 19 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..cb9ecf9 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.wasm32-unknown-unknown] +rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e772c67..b206ae3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,21 +35,27 @@ checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "causalgraphs" version = "0.1.0" dependencies = [ - "ahash", - "indexmap 2.9.0", + "getrandom 0.2.16", + "getrandom 0.3.3", + "indexmap 2.10.0", + "js-sys", "petgraph", "pyo3", "pyo3-build-config", "rustworkx-core", + "serde", + "serde-wasm-bindgen", + "uuid", "wasm-bindgen", + "web-sys", ] [[package]] @@ -108,8 +114,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -119,9 +127,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -165,9 +175,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown 0.15.4", @@ -189,6 +199,16 @@ dependencies = [ "either", ] +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.174" @@ -265,7 +285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.9.0", + "indexmap 2.10.0", ] [[package]] @@ -474,7 +494,7 @@ dependencies = [ "ahash", "fixedbitset", "hashbrown 0.14.5", - "indexmap 2.9.0", + "indexmap 2.10.0", "num-traits", "petgraph", "priority-queue", @@ -490,6 +510,37 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -498,9 +549,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "syn" -version = "2.0.103" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -525,6 +576,27 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "uuid-rng-internal", + "wasm-bindgen", +] + +[[package]] +name = "uuid-rng-internal" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11c277e43528edc5dd4660d28b2e61d70dff7f4f91502fe6a9a917eb61e427e9" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "version_check" version = "0.9.5" @@ -604,6 +676,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 3dd5a95..7f518fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,20 +5,40 @@ edition = "2021" [lib] name = "causalgraphs" -crate-type = ["cdylib"] +# Dynamic crate-type based on features +crate-type = ["cdylib", "rlib"] + +[features] +default = ["python"] +python = ["dep:pyo3", "dep:pyo3-build-config"] +wasm = ["dep:wasm-bindgen", "dep:js-sys", "dep:web-sys", "dep:serde", "dep:serde-wasm-bindgen"] [dependencies] -# Core graph library from rustworkx (pure Rust, on crates.io) +# Core dependencies (always included) rustworkx-core = "0.14" -wasm-bindgen = "0.2" - -# You already imported petgraph and ahash for utilities: petgraph = "0.6" -ahash = "0.8" indexmap = "2.0" -# PyO3 for the Python binding -pyo3 = { version = "0.20", features = ["extension-module"] } +# Python-specific dependencies (only for python feature) +pyo3 = { version = "0.20", features = ["extension-module"], optional = true } + +# WASM-specific dependencies (only for wasm feature) +wasm-bindgen = { version = "0.2", optional = true } +js-sys = { version = "0.3", optional = true } +web-sys = { version = "0.3", optional = true } +serde = { version = "1.0", features = ["derive"], optional = true } +serde-wasm-bindgen = { version = "0.6", optional = true } + +# UUID and getrandom dependencies (for both features) +uuid = { version = "1.13", features = ["v4", "rng-getrandom"] } +getrandom_v03 = { package = "getrandom", version = "0.3", features = ["wasm_js"] } +getrandom_v02 = { package = "getrandom", version = "0.2", features = ["js"] } [build-dependencies] -pyo3-build-config = "0.20" +pyo3-build-config = { version = "0.20", optional = true } + +# WASM-specific profile +[profile.release] +# Optimize for size when building for WASM +opt-level = "s" +lto = true \ No newline at end of file From 4539126e2424cd0a5be10f5ef29fd296e661cacc Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 28 Jun 2025 20:34:04 +0530 Subject: [PATCH 12/62] fix wasm compilation issue - removed the faulty default python flag - does not compile python code during wasm compilation now --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7f518fe..cfdcfb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ name = "causalgraphs" crate-type = ["cdylib", "rlib"] [features] -default = ["python"] +default = [] python = ["dep:pyo3", "dep:pyo3-build-config"] wasm = ["dep:wasm-bindgen", "dep:js-sys", "dep:web-sys", "dep:serde", "dep:serde-wasm-bindgen"] From e595d8700d0760eb713581d7fab4237b2eff92ec Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 28 Jun 2025 21:15:53 +0530 Subject: [PATCH 13/62] js runtime tests --- js/demo/test-wasm.html | 176 +++++++++++++++++++++++++++++++++++++++++ js/package.json | 29 +++++++ js/test/test-wasm.js | 85 ++++++++++++++++++++ src/dag.rs | 5 +- 4 files changed, 292 insertions(+), 3 deletions(-) create mode 100644 js/demo/test-wasm.html create mode 100644 js/package.json create mode 100644 js/test/test-wasm.js diff --git a/js/demo/test-wasm.html b/js/demo/test-wasm.html new file mode 100644 index 0000000..f4c6398 --- /dev/null +++ b/js/demo/test-wasm.html @@ -0,0 +1,176 @@ + + + + + + CausalGraphs WASM Test + + + +
+

🚀 CausalGraphs WASM Test

+

Testing the compiled WASM functions from Rust.

+ + + + +
Click a button to run tests...
+
+ + + + \ No newline at end of file diff --git a/js/package.json b/js/package.json new file mode 100644 index 0000000..e8d51f4 --- /dev/null +++ b/js/package.json @@ -0,0 +1,29 @@ +{ + "name": "causalgraphs-js", + "version": "0.1.0", + "description": "JavaScript/WASM bindings for CausalGraphs", + "type": "module", + "main": "causalgraphs.js", + "scripts": { + "test": "node test/test-wasm.js", + "demo": "python3 -m http.server 8001", + "build": "cd .. && wasm-pack build --target web --out-dir js/pkg-web --features wasm", + "build:node": "cd .. && wasm-pack build --target nodejs --out-dir js/pkg-node --features wasm", + "clean": "rm -rf pkg-web pkg-node" + }, + "keywords": [ + "causal-graphs", + "dag", + "wasm", + "rust", + "graph-algorithms" + ], + "author": "Your Name", + "license": "MIT", + "files": [ + "pkg-web/", + "pkg-node/", + "demo/", + "test/" + ] +} \ No newline at end of file diff --git a/js/test/test-wasm.js b/js/test/test-wasm.js new file mode 100644 index 0000000..0f64223 --- /dev/null +++ b/js/test/test-wasm.js @@ -0,0 +1,85 @@ +/** + * Node.js test for CausalGraphs WASM + */ + +import * as causalgraphs from '../pkg-node/causalgraphs.js'; + +async function testWasm() { + console.log('🚀 Testing CausalGraphs WASM...\n'); + + try { + // No need to call init for node target! + // Create a new DAG + const dag = new causalgraphs.RustDAG(); + console.log('✅ DAG created successfully!\n'); + + // Add some nodes + dag.addNode('A'); + dag.addNode('B'); + dag.addNode('C'); + console.log('Added nodes: A, B, C'); + + // Add edges + dag.addEdge('A', 'B'); + dag.addEdge('B', 'C'); + console.log('Added edges: A→B, B→C\n'); + + // Get graph information + const nodes = dag.nodes(); + const edges = dag.edges; + const nodeCount = dag.nodeCount; + const edgeCount = dag.edgeCount; + + console.log(`Nodes: ${nodes.join(', ')}`); + console.log(`Edges: ${JSON.stringify(edges)}`); + console.log(`Node count: ${nodeCount}`); + console.log(`Edge count: ${edgeCount}\n`); + + // Test graph traversal + const parentsOfC = dag.getParents('C'); + const childrenOfA = dag.getChildren('A'); + const ancestorsOfC = dag.getAncestorsOf(['C']); + + console.log(`Parents of C: ${parentsOfC.join(', ')}`); + console.log(`Children of A: ${childrenOfA.join(', ')}`); + console.log(`Ancestors of C: ${ancestorsOfC.join(', ')}\n`); + + // Test with latent variables + dag.addNode('L', true); // Add latent node + dag.addEdge('L', 'A'); + console.log('Added latent node L → A'); + + // Test more complex graph + const dag2 = new causalgraphs.RustDAG(); + const nodeNames = ['X', 'Y', 'Z', 'W', 'V']; + dag2.addNodesFrom(nodeNames); + + dag2.addEdge('X', 'Y'); + dag2.addEdge('Y', 'Z'); + dag2.addEdge('X', 'W'); + dag2.addEdge('W', 'Z'); + dag2.addEdge('V', 'X'); + + console.log('\nCreated complex graph:'); + console.log('V → X → Y → Z'); + console.log(' ↓ ↑'); + console.log(' W → Z\n'); + + const ancestorsOfZ = dag2.getAncestorsOf(['Z']); + const parentsOfZ = dag2.getParents('Z'); + const childrenOfX = dag2.getChildren('X'); + + console.log(`Ancestors of Z: ${ancestorsOfZ.join(', ')}`); + console.log(`Parents of Z: ${parentsOfZ.join(', ')}`); + console.log(`Children of X: ${childrenOfX.join(', ')}\n`); + + console.log('🎉 All tests passed!'); + + } catch (error) { + console.error('❌ Error:', error); + process.exit(1); + } +} + +// Run the test +testWasm(); \ No newline at end of file diff --git a/src/dag.rs b/src/dag.rs index 038ed43..dd089a1 100644 --- a/src/dag.rs +++ b/src/dag.rs @@ -16,7 +16,6 @@ use serde::{Deserialize, Serialize}; #[derive(Clone)] #[cfg_attr(feature = "python", pyclass)] #[cfg_attr(feature = "wasm", wasm_bindgen)] -#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))] pub struct RustDAG { graph: DiGraph, node_map: HashMap, @@ -243,8 +242,8 @@ impl RustDAG { } #[wasm_bindgen(js_name = addNodesFrom)] - pub fn js_add_nodes_from(&mut self, nodes: Vec, latent: Option>) -> Result<(), JsValue> { - self.add_nodes_from_internal(nodes, latent) + pub fn js_add_nodes_from(&mut self, nodes: Vec) -> Result<(), JsValue> { + self.add_nodes_from_internal(nodes, None) .map_err(|e| JsValue::from_str(&e)) } From 92ce6e598500735b31784d690d2129d9e269eb14 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Wed, 2 Jul 2025 21:41:44 +0530 Subject: [PATCH 14/62] Refactor project structure: separate Rust core and Python bindings, remove unused files, and update dependencies --- Cargo.lock | 89 ++--------- Cargo.toml | 29 +--- pyproject.toml | 31 ---- python/causalgraphs/__init__.py | 7 - python/causalgraphs/dag.py | 87 ----------- python/requirements.txt | 4 - python_bindings/.github/workflows/CI.yml | 181 +++++++++++++++++++++++ python_bindings/.gitignore | 72 +++++++++ python_bindings/Cargo.toml | 13 ++ python_bindings/pyproject.toml | 15 ++ python_bindings/requirements.txt | 2 + python_bindings/src/lib.rs | 71 +++++++++ rust_core/Cargo.toml | 14 ++ {src => rust_core/src}/dag.rs | 72 +++++---- rust_core/src/lib.rs | 5 + {src => rust_core/src}/utils.rs | 0 src/lib.rs | 11 -- 17 files changed, 421 insertions(+), 282 deletions(-) delete mode 100644 pyproject.toml delete mode 100644 python/causalgraphs/__init__.py delete mode 100644 python/causalgraphs/dag.py delete mode 100644 python/requirements.txt create mode 100644 python_bindings/.github/workflows/CI.yml create mode 100644 python_bindings/.gitignore create mode 100644 python_bindings/Cargo.toml create mode 100644 python_bindings/pyproject.toml create mode 100644 python_bindings/requirements.txt create mode 100644 python_bindings/src/lib.rs create mode 100644 rust_core/Cargo.toml rename {src => rust_core/src}/dag.rs (68%) create mode 100644 rust_core/src/lib.rs rename {src => rust_core/src}/utils.rs (100%) delete mode 100644 src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index e772c67..c2f8ac7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,23 +33,12 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" -[[package]] -name = "bumpalo" -version = "3.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" - [[package]] name = "causalgraphs" version = "0.1.0" dependencies = [ - "ahash", - "indexmap 2.9.0", - "petgraph", "pyo3", - "pyo3-build-config", - "rustworkx-core", - "wasm-bindgen", + "rust_core", ] [[package]] @@ -205,12 +194,6 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - [[package]] name = "memoffset" version = "0.9.1" @@ -460,10 +443,14 @@ dependencies = [ ] [[package]] -name = "rustversion" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +name = "rust_core" +version = "0.1.0" +dependencies = [ + "ahash", + "indexmap 2.9.0", + "petgraph", + "rustworkx-core", +] [[package]] name = "rustworkx-core" @@ -546,64 +533,6 @@ dependencies = [ "wit-bindgen-rt", ] -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 3dd5a95..c8a8829 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,24 +1,7 @@ -[package] -name = "causalgraphs" -version = "0.1.0" -edition = "2021" +[workspace] +members = [ + "rust_core", + "python_bindings", +] -[lib] -name = "causalgraphs" -crate-type = ["cdylib"] - -[dependencies] -# Core graph library from rustworkx (pure Rust, on crates.io) -rustworkx-core = "0.14" -wasm-bindgen = "0.2" - -# You already imported petgraph and ahash for utilities: -petgraph = "0.6" -ahash = "0.8" -indexmap = "2.0" - -# PyO3 for the Python binding -pyo3 = { version = "0.20", features = ["extension-module"] } - -[build-dependencies] -pyo3-build-config = "0.20" +resolver = "2" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 1588170..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,31 +0,0 @@ -[build-system] -requires = ["maturin>=1.0,<2.0"] -build-backend = "maturin" - -[project] -name = "causalgraphs" -version = "0.1.0" -description = "High-performance graph operations for PGMPy using Rust" -authors = [ - {name = "Your Name", email = "your.email@example.com"}, -] -requires-python = ">=3.8" -classifiers = [ - "Programming Language :: Rust", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", -] -dependencies = [ - "rustworkx>=0.14.0", -] - -[project.optional-dependencies] -dev = [ - "pytest>=6.0", - "numpy", - "networkx", -] - -[tool.maturin] -features = ["pyo3/extension-module"] -python-source = "python" \ No newline at end of file diff --git a/python/causalgraphs/__init__.py b/python/causalgraphs/__init__.py deleted file mode 100644 index 43c3db6..0000000 --- a/python/causalgraphs/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -High-performance graph operations for PGMPy using Rust -""" -from .dag import RustDAG - -__version__ = "0.1.0" -__all__ = ["RustDAG"] \ No newline at end of file diff --git a/python/causalgraphs/dag.py b/python/causalgraphs/dag.py deleted file mode 100644 index bcf3bf1..0000000 --- a/python/causalgraphs/dag.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Python wrapper for Rust DAG implementation with pgmpy compatibility -""" -from typing import List, Set, Optional, Union, Hashable -from .causalgraphs import RustDAG as _RustDAG - - -class RustDAG: - """ - A high-performance DAG implementation using Rust backend. - Compatible with pgmpy's DAG interface. - """ - - def __init__(self, ebunch=None, latents=None): - self._dag = _RustDAG() - self.latents = set(latents or []) - - if ebunch: - for u, v in ebunch: - self.add_edge(str(u), str(v)) - - def add_node(self, node: Hashable, latent: bool = False): - """Add a single node to the graph.""" - self._dag.add_node(str(node), latent) - if latent: - self.latents.add(str(node)) - - def add_nodes_from(self, nodes: List[Hashable], latent: Union[bool, List[bool]] = False): - """Add multiple nodes to the graph.""" - nodes_str = [str(n) for n in nodes] - - if isinstance(latent, bool): - latent_flags = [latent] * len(nodes) - else: - latent_flags = latent - - self._dag.add_nodes_from(nodes_str, latent_flags) - - for node, is_latent in zip(nodes_str, latent_flags): - if is_latent: - self.latents.add(node) - - def add_edge(self, u: Hashable, v: Hashable, weight: Optional[float] = None): - """Add an edge between two nodes.""" - self._dag.add_edge(str(u), str(v), weight) - - def get_parents(self, node: Hashable) -> List[str]: - """Get parents of a node.""" - return self._dag.get_parents(str(node)) - - def get_children(self, node: Hashable) -> List[str]: - """Get children of a node.""" - return self._dag.get_children(str(node)) - - def _get_ancestors_of(self, nodes: Union[str, List[Hashable]]) -> Set[str]: - """Get ancestors of given nodes (optimized Rust implementation).""" - if isinstance(nodes, str): - nodes = [nodes] - nodes_str = [str(n) for n in nodes] - return self._dag.get_ancestors_of(nodes_str) - - # def minimal_dseparator(self, start: Hashable, end: Hashable, - # include_latents: bool = False) -> Optional[Set[str]]: - # """Find minimal d-separating set (optimized Rust implementation).""" - # return self._dag.minimal_dseparator(str(start), str(end), include_latents) - - # def is_dconnected(self, start: Hashable, end: Hashable, - # observed: Optional[List[Hashable]] = None) -> bool: - # """Check if two nodes are d-connected.""" - # observed_str = [str(n) for n in observed] if observed else None - # return self._dag.is_dconnected(str(start), str(end), observed_str) - - def nodes(self) -> List[str]: - """Get all nodes.""" - return self._dag.nodes() - - def edges(self) -> List[tuple]: - """Get all edges.""" - return self._dag.edges() - - def __len__(self) -> int: - """Return number of nodes.""" - return self._dag.node_count() - - def number_of_edges(self) -> int: - """Return number of edges.""" - return self._dag.edge_count() \ No newline at end of file diff --git a/python/requirements.txt b/python/requirements.txt deleted file mode 100644 index 22c35bb..0000000 --- a/python/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -maturin==1.8.7 -numpy==2.2.6 -rustworkx==0.16.0 -tomli==2.2.1 diff --git a/python_bindings/.github/workflows/CI.yml b/python_bindings/.github/workflows/CI.yml new file mode 100644 index 0000000..138846e --- /dev/null +++ b/python_bindings/.github/workflows/CI.yml @@ -0,0 +1,181 @@ +# This file is autogenerated by maturin v1.8.7 +# To update, run +# +# maturin generate-ci github +# +name: CI + +on: + push: + branches: + - main + - master + tags: + - '*' + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: ubuntu-22.04 + target: x86_64 + - runner: ubuntu-22.04 + target: x86 + - runner: ubuntu-22.04 + target: aarch64 + - runner: ubuntu-22.04 + target: armv7 + - runner: ubuntu-22.04 + target: s390x + - runner: ubuntu-22.04 + target: ppc64le + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: auto + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-linux-${{ matrix.platform.target }} + path: dist + + musllinux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: ubuntu-22.04 + target: x86_64 + - runner: ubuntu-22.04 + target: x86 + - runner: ubuntu-22.04 + target: aarch64 + - runner: ubuntu-22.04 + target: armv7 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: musllinux_1_2 + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-musllinux-${{ matrix.platform.target }} + path: dist + + windows: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: windows-latest + target: x64 + - runner: windows-latest + target: x86 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + architecture: ${{ matrix.platform.target }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-windows-${{ matrix.platform.target }} + path: dist + + macos: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: macos-13 + target: x86_64 + - runner: macos-14 + target: aarch64 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-macos-${{ matrix.platform.target }} + path: dist + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + - name: Upload sdist + uses: actions/upload-artifact@v4 + with: + name: wheels-sdist + path: dist + + release: + name: Release + runs-on: ubuntu-latest + if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} + needs: [linux, musllinux, windows, macos, sdist] + permissions: + # Use to sign the release artifacts + id-token: write + # Used to upload release artifacts + contents: write + # Used to generate artifact attestation + attestations: write + steps: + - uses: actions/download-artifact@v4 + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v2 + with: + subject-path: 'wheels-*/*' + - name: Publish to PyPI + if: ${{ startsWith(github.ref, 'refs/tags/') }} + uses: PyO3/maturin-action@v1 + env: + MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + with: + command: upload + args: --non-interactive --skip-existing wheels-*/* diff --git a/python_bindings/.gitignore b/python_bindings/.gitignore new file mode 100644 index 0000000..c8f0442 --- /dev/null +++ b/python_bindings/.gitignore @@ -0,0 +1,72 @@ +/target + +# Byte-compiled / optimized / DLL files +__pycache__/ +.pytest_cache/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +.venv/ +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +include/ +man/ +venv/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +pip-selfcheck.json + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +.DS_Store + +# Sphinx documentation +docs/_build/ + +# PyCharm +.idea/ + +# VSCode +.vscode/ + +# Pyenv +.python-version diff --git a/python_bindings/Cargo.toml b/python_bindings/Cargo.toml new file mode 100644 index 0000000..256ce10 --- /dev/null +++ b/python_bindings/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "causalgraphs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "causalgraphs" +crate-type = ["cdylib"] + +[dependencies] +rust_core = { path = "../rust_core" } +pyo3 = { version = "0.20", features = ["extension-module"] } diff --git a/python_bindings/pyproject.toml b/python_bindings/pyproject.toml new file mode 100644 index 0000000..e78a293 --- /dev/null +++ b/python_bindings/pyproject.toml @@ -0,0 +1,15 @@ +[build-system] +requires = ["maturin>=1.8,<2.0"] +build-backend = "maturin" + +[project] +name = "causalgraphs" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dynamic = ["version"] +[tool.maturin] +features = ["pyo3/extension-module"] diff --git a/python_bindings/requirements.txt b/python_bindings/requirements.txt new file mode 100644 index 0000000..120eaa2 --- /dev/null +++ b/python_bindings/requirements.txt @@ -0,0 +1,2 @@ +maturin==1.9.0 +tomli==2.2.1 diff --git a/python_bindings/src/lib.rs b/python_bindings/src/lib.rs new file mode 100644 index 0000000..7d37b81 --- /dev/null +++ b/python_bindings/src/lib.rs @@ -0,0 +1,71 @@ +use pyo3::prelude::*; +use pyo3::exceptions::{PyKeyError, PyValueError}; +use rust_core::RustDAG; +use std::collections::HashSet; + +#[pyclass] +#[derive(Clone)] +pub struct PyRustDAG { + inner: RustDAG, +} + +#[pymethods] +impl PyRustDAG { + #[new] + pub fn new() -> Self { + PyRustDAG { inner: RustDAG::new() } + } + + pub fn add_node(&mut self, node: String, latent: Option) -> PyResult<()> { + self.inner.add_node(node, latent.unwrap_or(false)) + .map_err(PyValueError::new_err) + } + + pub fn add_nodes_from(&mut self, nodes: Vec, latent: Option>) -> PyResult<()> { + self.inner.add_nodes_from(nodes, latent) + .map_err(PyValueError::new_err) + } + + pub fn add_edge(&mut self, u: String, v: String, weight: Option) -> PyResult<()> { + self.inner.add_edge(u, v, weight) + .map_err(PyValueError::new_err) + } + + pub fn get_parents(&self, node: String) -> PyResult> { + self.inner.get_parents(&node) + .map_err(PyKeyError::new_err) + } + + pub fn get_children(&self, node: String) -> PyResult> { + self.inner.get_children(&node) + .map_err(PyKeyError::new_err) + } + + pub fn get_ancestors_of(&self, nodes: Vec) -> PyResult> { + self.inner.get_ancestors_of(nodes) + .map_err(PyValueError::new_err) + } + + pub fn nodes(&self) -> Vec { + self.inner.nodes() + } + + pub fn edges(&self) -> Vec<(String, String)> { + self.inner.edges() + } + + pub fn node_count(&self) -> usize { + self.inner.node_count() + } + + pub fn edge_count(&self) -> usize { + self.inner.edge_count() + } +} + +#[pymodule] +fn causalgraphs(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} + diff --git a/rust_core/Cargo.toml b/rust_core/Cargo.toml new file mode 100644 index 0000000..a498f6e --- /dev/null +++ b/rust_core/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "rust_core" +version = "0.1.0" +edition = "2021" + +[lib] +name = "rust_core" +path = "src/lib.rs" + +[dependencies] +petgraph = "0.6" +ahash = "0.8" +indexmap = "2.0" +rustworkx-core = "0.14" \ No newline at end of file diff --git a/src/dag.rs b/rust_core/src/dag.rs similarity index 68% rename from src/dag.rs rename to rust_core/src/dag.rs index 0699ae9..35405fd 100644 --- a/src/dag.rs +++ b/rust_core/src/dag.rs @@ -1,20 +1,20 @@ use ahash::AHashSet; use petgraph::Direction; -use pyo3::prelude::*; use rustworkx_core::petgraph::graph::{DiGraph, NodeIndex}; use std::collections::{HashMap, HashSet, VecDeque}; -#[pyclass] -pub struct RustDAG { - graph: DiGraph, - node_map: HashMap, - reverse_node_map: HashMap, - latents: HashSet, +// Remove #[pyclass] here. This is a pure Rust struct. +#[derive(Debug, Clone)] // Add Debug for easier printing in Rust tests +pub struct RustDAG { + pub graph: DiGraph, // Make fields public if bindings need direct access, + pub node_map: HashMap, // or provide internal methods. + pub reverse_node_map: HashMap, + pub latents: HashSet, } -#[pymethods] -impl RustDAG { - #[new] +// All methods here should be public, but not necessarily #[pymethods] +// They are the *internal* implementations that the bindings will call. +impl RustDAG { pub fn new() -> Self { RustDAG { graph: DiGraph::new(), @@ -25,13 +25,13 @@ impl RustDAG { } /// Add a single node to the graph - pub fn add_node(&mut self, node: String, latent: Option) -> PyResult<()> { + pub fn add_node(&mut self, node: String, latent: bool) -> Result<(), String> { if !self.node_map.contains_key(&node) { let idx: NodeIndex = self.graph.add_node(node.clone()); self.node_map.insert(node.clone(), idx); self.reverse_node_map.insert(idx, node.clone()); - - if latent.unwrap_or(false) { + + if latent { self.latents.insert(node); } } @@ -39,39 +39,37 @@ impl RustDAG { } /// Add multiple nodes to the graph - pub fn add_nodes_from(&mut self, nodes: Vec, latent: Option>) -> PyResult<()> { + pub fn add_nodes_from(&mut self, nodes: Vec, latent: Option>) -> Result<(), String> { let latent_flags: Vec = latent.unwrap_or_else(|| vec![false; nodes.len()]); - + if nodes.len() != latent_flags.len() { - return Err(pyo3::exceptions::PyValueError::new_err( - "Length of nodes and latent flags must match" - )); + return Err("Length of nodes and latent flags must match".to_string()); } for (node, is_latent) in nodes.iter().zip(latent_flags.iter()) { - self.add_node(node.clone(), Some(*is_latent))?; + // Note: Call self.add_node directly now, not self.add_node_internal + self.add_node(node.clone(), *is_latent)?; } Ok(()) } - /// Add an edge between two nodes - pub fn add_edge(&mut self, u: String, v: String, weight: Option) -> PyResult<()> { - // Add nodes if they don't exist - self.add_node(u.clone(), None)?; - self.add_node(v.clone(), None)?; + pub fn add_edge(&mut self, u: String, v: String, weight: Option) -> Result<(), String> { + // Add nodes if they don't exist. Pass false for latent by default. + self.add_node(u.clone(), false)?; + self.add_node(v.clone(), false)?; let u_idx: NodeIndex = self.node_map[&u]; let v_idx: NodeIndex = self.node_map[&v]; - + self.graph.add_edge(u_idx, v_idx, weight.unwrap_or(1.0)); Ok(()) } /// Get parents of a node - pub fn get_parents(&self, node: String) -> PyResult> { - let node_idx = self.node_map.get(&node) - .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("Node {} not found", node)))?; + pub fn get_parents(&self, node: &str) -> Result, String> { + let node_idx = self.node_map.get(node) + .ok_or_else(|| format!("Node {} not found", node))?; let parents: Vec = self.graph .neighbors_directed(*node_idx, Direction::Incoming) @@ -82,9 +80,9 @@ impl RustDAG { } /// Get children of a node - pub fn get_children(&self, node: String) -> PyResult> { - let node_idx = self.node_map.get(&node) - .ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(format!("Node {} not found", node)))?; + pub fn get_children(&self, node: &str) -> Result, String> { + let node_idx = self.node_map.get(node) + .ok_or_else(|| format!("Node {} not found", node))?; let children: Vec = self.graph .neighbors_directed(*node_idx, Direction::Outgoing) @@ -95,7 +93,7 @@ impl RustDAG { } /// Get all ancestors of given nodes (optimized Rust implementation) - pub fn get_ancestors_of(&self, nodes: Vec) -> PyResult> { + pub fn get_ancestors_of(&self, nodes: Vec) -> Result, String> { let mut ancestors: AHashSet = AHashSet::new(); let mut queue: VecDeque = VecDeque::new(); @@ -105,9 +103,7 @@ impl RustDAG { queue.push_back(node_idx); ancestors.insert(node.clone()); } else { - return Err(pyo3::exceptions::PyValueError::new_err( - format!("Node {} not in graph", node) - )); + return Err(format!("Node {} not in graph", node)); } } @@ -120,7 +116,7 @@ impl RustDAG { } } } - + Ok(ancestors.into_iter().collect()) } @@ -148,10 +144,8 @@ impl RustDAG { self.graph.node_count() } - /// Get number of edges + /// Get number of edges pub fn edge_count(&self) -> usize { self.graph.edge_count() } - - } \ No newline at end of file diff --git a/rust_core/src/lib.rs b/rust_core/src/lib.rs new file mode 100644 index 0000000..cf99111 --- /dev/null +++ b/rust_core/src/lib.rs @@ -0,0 +1,5 @@ +// Re-export modules/structs from your core logic +pub mod dag; +// pub mod pdag; // Add PDAG.rs later if needed + +pub use dag::RustDAG; \ No newline at end of file diff --git a/src/utils.rs b/rust_core/src/utils.rs similarity index 100% rename from src/utils.rs rename to rust_core/src/utils.rs diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index aed7e8d..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -use pyo3::prelude::*; - -mod dag; - -use dag::RustDAG; - -#[pymodule] -fn causalgraphs(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_class::()?; - Ok(()) -} \ No newline at end of file From 5bfe3bb67992d521e6386ab7075836ec757c8821 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Wed, 2 Jul 2025 22:37:00 +0530 Subject: [PATCH 15/62] clean up --- python_bindings/Cargo.toml | 1 - python_bindings/src/lib.rs | 2 +- python_bindings/test.py | 29 +++++++++++++++++++++++++++++ test.py | 15 --------------- 4 files changed, 30 insertions(+), 17 deletions(-) create mode 100644 python_bindings/test.py delete mode 100644 test.py diff --git a/python_bindings/Cargo.toml b/python_bindings/Cargo.toml index 256ce10..42ae774 100644 --- a/python_bindings/Cargo.toml +++ b/python_bindings/Cargo.toml @@ -3,7 +3,6 @@ name = "causalgraphs" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] name = "causalgraphs" crate-type = ["cdylib"] diff --git a/python_bindings/src/lib.rs b/python_bindings/src/lib.rs index 7d37b81..52db9ea 100644 --- a/python_bindings/src/lib.rs +++ b/python_bindings/src/lib.rs @@ -3,7 +3,7 @@ use pyo3::exceptions::{PyKeyError, PyValueError}; use rust_core::RustDAG; use std::collections::HashSet; -#[pyclass] +#[pyclass(name = "RustDAG")] #[derive(Clone)] pub struct PyRustDAG { inner: RustDAG, diff --git a/python_bindings/test.py b/python_bindings/test.py new file mode 100644 index 0000000..a23ea7d --- /dev/null +++ b/python_bindings/test.py @@ -0,0 +1,29 @@ +import causalgraphs + +# Instantiate your RustDAG object +dag = causalgraphs.RustDAG() + +# Test adding nodes +dag.add_node("A") +dag.add_node("B", latent=True) +dag.add_nodes_from(["C", "D"], latent=[False, True]) +print(f"Nodes: {dag.nodes()}") +# Expected output: Nodes: ['A', 'B', 'C', 'D'] (order may vary) + +# Test adding edges +dag.add_edge("A", "B") +dag.add_edge("B", "C", weight=0.5) +print(f"Edges: {dag.edges()}") +# Expected output: Edges: [('A', 'B'), ('B', 'C')] (order may vary) + +# Test graph properties +print(f"Node count: {dag.node_count()}") # Expected: 4 +print(f"Edge count: {dag.edge_count()}") # Expected: 2 + +# Test methods +print(f"Parents of C: {dag.get_parents('C')}") # Expected: ['B'] +print(f"Children of B: {dag.get_children('B')}") # Expected: ['C'] + +# Test ancestors (Rust-backed logic) +ancestors_of_C = dag.get_ancestors_of(["C"]) +print(f"Ancestors of C: {ancestors_of_C}") # Expected: {'A', 'B', 'C'} (order may vary, depends on your ancestor definition) diff --git a/test.py b/test.py deleted file mode 100644 index 77539c5..0000000 --- a/test.py +++ /dev/null @@ -1,15 +0,0 @@ -from causalgraphs import RustDAG - -# Drop-in replacement for performance-critical operations -dag = RustDAG([('A', 'B'), ('B', 'C'), ('D', 'C')]) - -print("dag:", dag.nodes()) -print("edges: ", dag.edges()) - - -# test get_children and parents -dag.add_node('E') -dag.add_edge('E', 'A') -print("Children of A:", dag.get_children('A')) -print("Parents of C:", dag.get_parents('C')) -print("Ancestors of C:", dag._get_ancestors_of('C')) \ No newline at end of file From 46bce997b94bf9d207db728c354f2ebc8ccac29f Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Thu, 3 Jul 2025 22:22:11 +0530 Subject: [PATCH 16/62] Restructure wasm_bindings module with initial setup and dependencies --- .cargo/config.toml | 2 - Cargo.lock | 214 ++++++++++++++++++++++++++- Cargo.toml | 2 +- rust_core/src/dag.rs | 7 +- wasm_bindings/.cargo/config.toml | 5 + wasm_bindings/.github/dependabot.yml | 8 + wasm_bindings/.gitignore | 6 + wasm_bindings/Cargo.toml | 43 ++++++ wasm_bindings/LICENSE_MIT | 21 +++ wasm_bindings/README.md | 84 +++++++++++ wasm_bindings/src/lib.rs | 124 ++++++++++++++++ wasm_bindings/src/utils.rs | 10 ++ wasm_bindings/tests/web.rs | 13 ++ 13 files changed, 532 insertions(+), 7 deletions(-) delete mode 100644 .cargo/config.toml create mode 100644 wasm_bindings/.cargo/config.toml create mode 100644 wasm_bindings/.github/dependabot.yml create mode 100644 wasm_bindings/.gitignore create mode 100644 wasm_bindings/Cargo.toml create mode 100644 wasm_bindings/LICENSE_MIT create mode 100644 wasm_bindings/README.md create mode 100644 wasm_bindings/src/lib.rs create mode 100644 wasm_bindings/src/utils.rs create mode 100644 wasm_bindings/tests/web.rs diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index cb9ecf9..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[target.wasm32-unknown-unknown] -rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 2a36bd5..9080616 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,12 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + [[package]] name = "causalgraphs" version = "0.1.0" @@ -41,12 +47,48 @@ dependencies = [ "rust_core", ] +[[package]] +name = "causalgraphs-wasm" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "getrandom 0.2.16", + "getrandom 0.3.3", + "js-sys", + "rust_core", + "serde", + "serde-wasm-bindgen", + "uuid", + "wasm-bindgen", + "wasm-bindgen-test", + "web-sys", +] + +[[package]] +name = "cc" +version = "1.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -208,6 +250,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + [[package]] name = "memoffset" version = "0.9.1" @@ -217,6 +265,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "minicov" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -461,11 +519,17 @@ name = "rust_core" version = "0.1.0" dependencies = [ "ahash", - "indexmap 2.9.0", + "indexmap 2.10.0", "petgraph", "rustworkx-core", ] +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + [[package]] name = "rustworkx-core" version = "0.14.2" @@ -485,6 +549,15 @@ dependencies = [ "rayon-cond", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -522,6 +595,12 @@ dependencies = [ "syn", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "smallvec" version = "1.15.1" @@ -584,6 +663,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -599,6 +688,129 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3" +dependencies = [ + "js-sys", + "minicov", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index c8a8829..fb4c2dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,6 @@ members = [ "rust_core", "python_bindings", + "wasm_bindings" ] - resolver = "2" \ No newline at end of file diff --git a/rust_core/src/dag.rs b/rust_core/src/dag.rs index 22239fc..35405fd 100644 --- a/rust_core/src/dag.rs +++ b/rust_core/src/dag.rs @@ -1,3 +1,4 @@ +use ahash::AHashSet; use petgraph::Direction; use rustworkx_core::petgraph::graph::{DiGraph, NodeIndex}; use std::collections::{HashMap, HashSet, VecDeque}; @@ -120,12 +121,12 @@ impl RustDAG { } /// Get all nodes in the graph - fn nodes_internal(&self) -> Vec { + pub fn nodes(&self) -> Vec { self.node_map.keys().cloned().collect() } /// Get all edges in the graph - fn edges_internal(&self) -> Vec<(String, String)> { + pub fn edges(&self) -> Vec<(String, String)> { self.graph .edge_indices() .map(|edge_idx| { @@ -139,7 +140,7 @@ impl RustDAG { } /// Get number of nodes - fn node_count_internal(&self) -> usize { + pub fn node_count(&self) -> usize { self.graph.node_count() } diff --git a/wasm_bindings/.cargo/config.toml b/wasm_bindings/.cargo/config.toml new file mode 100644 index 0000000..7da31e1 --- /dev/null +++ b/wasm_bindings/.cargo/config.toml @@ -0,0 +1,5 @@ +# Ongoing Issue See: https://github.com/uuid-rs/uuid/issues/792 & https://docs.rs/getrandom/latest/getrandom/#webassembly-support for more details. +# This configuration ensures that the getrandom crate uses the correct backend for WebAssembly. +# It is required for crates like uuid and rand to work on wasm32-unknown-unknown. +[target.wasm32-unknown-unknown] +rustflags = ['--cfg', 'getrandom_backend="wasm_js"'] \ No newline at end of file diff --git a/wasm_bindings/.github/dependabot.yml b/wasm_bindings/.github/dependabot.yml new file mode 100644 index 0000000..7377d37 --- /dev/null +++ b/wasm_bindings/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: +- package-ecosystem: cargo + directory: "/" + schedule: + interval: daily + time: "08:00" + open-pull-requests-limit: 10 diff --git a/wasm_bindings/.gitignore b/wasm_bindings/.gitignore new file mode 100644 index 0000000..4e30131 --- /dev/null +++ b/wasm_bindings/.gitignore @@ -0,0 +1,6 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log diff --git a/wasm_bindings/Cargo.toml b/wasm_bindings/Cargo.toml new file mode 100644 index 0000000..7934c1e --- /dev/null +++ b/wasm_bindings/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "wasm_bindings" +version = "0.1.0" +authors = ["Mohammed Razak "] +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +rust_core = { path = "../rust_core" } + +# WASM binding dependencies +wasm-bindgen = "0.2.84" +js-sys = "0.3" +web-sys = { version = "0.3", features = ["console"] } # 'console' is useful for logging +serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = "0.6" + +# UUID and getrandom dependencies +# Ongoing Issue See: https://github.com/uuid-rs/uuid/issues/792 & https://docs.rs/getrandom/latest/getrandom/#webassembly-support for more details. +# New version of getrandom doesnt utilize js backend for random generation, which is why we need to specify the features explicitly and +# add a .cargo/config.toml file to set the flag to use js backend explicitly during compile time +uuid = { version = "1.13", features = ["v4", "rng-getrandom"] } +getrandom_v03 = { package = "getrandom", version = "0.3", features = ["wasm_js"] } +getrandom_v02 = { package = "getrandom", version = "0.2", features = ["js"] } + + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.7", optional = true } + +[dev-dependencies] +wasm-bindgen-test = "0.3.34" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" diff --git a/wasm_bindings/LICENSE_MIT b/wasm_bindings/LICENSE_MIT new file mode 100644 index 0000000..3ff47a2 --- /dev/null +++ b/wasm_bindings/LICENSE_MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 pgmpy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/wasm_bindings/README.md b/wasm_bindings/README.md new file mode 100644 index 0000000..6b68408 --- /dev/null +++ b/wasm_bindings/README.md @@ -0,0 +1,84 @@ +
+ +

wasm-pack-template

+ + A template for kick starting a Rust and WebAssembly project using wasm-pack. + +

+ Build Status +

+ +

+ Tutorial + | + Chat +

+ + Built with 🦀🕸 by The Rust and WebAssembly Working Group +
+ +## About + +[**📚 Read this template tutorial! 📚**][template-docs] + +This template is designed for compiling Rust libraries into WebAssembly and +publishing the resulting package to NPM. + +Be sure to check out [other `wasm-pack` tutorials online][tutorials] for other +templates and usages of `wasm-pack`. + +[tutorials]: https://rustwasm.github.io/docs/wasm-pack/tutorials/index.html +[template-docs]: https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html + +## 🚴 Usage + +### 🐑 Use `cargo generate` to Clone this Template + +[Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate) + +``` +cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project +cd my-project +``` + +### 🛠️ Build with `wasm-pack build` + +``` +wasm-pack build +``` + +### 🔬 Test in Headless Browsers with `wasm-pack test` + +``` +wasm-pack test --headless --firefox +``` + +### 🎁 Publish to NPM with `wasm-pack publish` + +``` +wasm-pack publish +``` + +## 🔋 Batteries Included + +* [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating + between WebAssembly and JavaScript. +* [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook) + for logging panic messages to the developer console. +* `LICENSE-APACHE` and `LICENSE-MIT`: most Rust projects are licensed this way, so these are included for you + +## License + +Licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. diff --git a/wasm_bindings/src/lib.rs b/wasm_bindings/src/lib.rs new file mode 100644 index 0000000..7f33f60 --- /dev/null +++ b/wasm_bindings/src/lib.rs @@ -0,0 +1,124 @@ +use wasm_bindgen::prelude::*; +use serde::{Deserialize, Serialize}; // For serializing/deserializing data to JS +use std::collections::HashSet; // For returning HashSet from Rust to Vec in JS + +// Import RustDAG from your core library +use rust_core::RustDAG; + +// Add a public struct to wrap RustDAG for WASM. +// It can directly be `RustDAG` if you make it `#[wasm_bindgen]` +// but sometimes a wrapper is cleaner for WASM for more control over JS API. +// Let's directly expose RustDAG as #[wasm_bindgen] for simplicity, similar to Python. +// However, RustDAG must be Clone, and its internal fields must be serializable if exposed. + +// You will likely want to make your RustDAG cloneable and serialize/deserialize for WASM, +// allowing it to be passed between Rust and JS contexts. +// Make sure `RustDAG` in `rust_core/src/dag.rs` has `#[derive(Clone, Serialize, Deserialize)]` if needed. +// IMPORTANT: `DiGraph` from `petgraph` does NOT implement `Serialize` or `Deserialize` directly. +// You'll need to either: +// 1. Manually serialize/deserialize `RustDAG` (complex). +// 2. Expose methods that operate on the graph but don't pass the graph *object* itself. +// 3. Use a different graph library if it provides Serde support. +// +// For simplicity in this example, let's assume `RustDAG` will have methods, +// but the struct itself won't be directly serialized/deserialized to JS object, +// unless you add custom Serde implementations. +// If you only pass primitives and call methods, `#[wasm_bindgen]` can apply directly. + +// Applying `#[wasm_bindgen]` to `RustDAG` directly. +// Note: If `RustDAG` in `rust_core` needs to be `Serialize`/`Deserialize` +// for use with `serde_wasm_bindgen`, you'll need to add `#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))]` +// to the `RustDAG` struct *in `rust_core/src/dag.rs`*. This requires `serde` to be +// an optional dependency in `rust_core`'s `Cargo.toml` with a `wasm` feature. +// This is getting more complex, so let's stick to methods for now. + +#[wasm_bindgen] +#[derive(Clone)] // Make sure RustDAG in rust_core also derives Clone +pub struct WasmDAG { // Use a wrapper struct named WasmDAG for clarity + inner: RustDAG, +} + +#[wasm_bindgen] +impl WasmDAG { + #[wasm_bindgen(constructor)] + pub fn new() -> WasmDAG { + WasmDAG { inner: RustDAG::new() } + } + + #[wasm_bindgen(js_name = addNode, catch)] + pub fn add_node(&mut self, node: String, latent: Option) -> Result<(), JsValue> { + self.inner.add_node(node, latent.unwrap_or(false)) + .map_err(|e| JsValue::from_str(&e)) + } + + #[wasm_bindgen] + pub fn add_nodes_from(&mut self, nodes: Vec, latent: Option>) -> Result<(), JsValue> { + let latent_bools = latent.map(|v| v.into_iter().map(|x| x != 0).collect()); + self.inner.add_nodes_from(nodes, latent_bools).map_err(|e| JsValue::from_str(&e.to_string())) + } + + #[wasm_bindgen(js_name = addEdge, catch)] + pub fn add_edge(&mut self, u: String, v: String, weight: Option) -> Result<(), JsValue> { + self.inner.add_edge(u, v, weight) + .map_err(|e| JsValue::from_str(&e)) + } + + #[wasm_bindgen(js_name = getParents, catch)] + pub fn get_parents(&self, node: String) -> Result, JsValue> { + self.inner.get_parents(&node) + .map_err(|e| JsValue::from_str(&e)) + } + + #[wasm_bindgen(js_name = getChildren, catch)] + pub fn get_children(&self, node: String) -> Result, JsValue> { + self.inner.get_children(&node) + .map_err(|e| JsValue::from_str(&e)) + } + + // For `HashSet` return, WASM-bindgen prefers `Vec` or serializable. + #[wasm_bindgen(js_name = getAncestorsOf, catch)] + pub fn get_ancestors_of(&self, nodes: Vec) -> Result, JsValue> { + self.inner.get_ancestors_of(nodes) + .map(|set| set.into_iter().collect()) // Convert HashSet to Vec + .map_err(|e| JsValue::from_str(&e)) + } + + #[wasm_bindgen(js_name = nodes)] + pub fn nodes(&self) -> Vec { + self.inner.nodes() + } + + #[wasm_bindgen(js_name = edges)] + pub fn edges(&self) -> JsValue { // Return JsValue for complex types like Vec<(String, String)> + serde_wasm_bindgen::to_value(&self.inner.edges()).unwrap_or_else(|_| JsValue::from_str("Failed to serialize edges")) + } + + #[wasm_bindgen(js_name = nodeCount, getter)] + pub fn node_count(&self) -> usize { + self.inner.node_count() + } + + #[wasm_bindgen(js_name = edgeCount, getter)] + pub fn edge_count(&self) -> usize { + self.inner.edge_count() + } + + // Expose latents + #[wasm_bindgen(js_name = latents, getter)] + pub fn latents(&self) -> JsValue { + serde_wasm_bindgen::to_value(&self.inner.latents).unwrap_or_else(|_| JsValue::from_str("Failed to serialize latents")) + } +} + +// Optional: Add a start function for debugging or initialization +#[wasm_bindgen(start)] +pub fn main_js() -> Result<(), JsValue> { + // This is optional, but useful for setting up panic hooks in browser + #[cfg(debug_assertions)] + console_error_panic_hook::set_once(); + + // You can do some initialization here if needed + web_sys::console::log_1(&"WasmDAG loaded!".into()); + + Ok(()) +} \ No newline at end of file diff --git a/wasm_bindings/src/utils.rs b/wasm_bindings/src/utils.rs new file mode 100644 index 0000000..b1d7929 --- /dev/null +++ b/wasm_bindings/src/utils.rs @@ -0,0 +1,10 @@ +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} diff --git a/wasm_bindings/tests/web.rs b/wasm_bindings/tests/web.rs new file mode 100644 index 0000000..de5c1da --- /dev/null +++ b/wasm_bindings/tests/web.rs @@ -0,0 +1,13 @@ +//! Test suite for the Web and headless browsers. + +#![cfg(target_arch = "wasm32")] + +extern crate wasm_bindgen_test; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn pass() { + assert_eq!(1 + 1, 2); +} From 9bfc7d662dc66f48f9bb27c9da06a553ec3b1f5b Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Thu, 3 Jul 2025 23:22:41 +0530 Subject: [PATCH 17/62] Refactors: * rename WasmDAG -> RustDAG * Added a simple node test file(needs to be fixed) --- wasm_bindings/Cargo.toml | 4 +- wasm_bindings/js/tests/test-wasm.js | 85 +++++++++++++++++++++++++++++ wasm_bindings/package.json | 29 ++++++++++ wasm_bindings/src/lib.rs | 42 ++------------ wasm_bindings/tests/web.rs | 13 ----- 5 files changed, 122 insertions(+), 51 deletions(-) create mode 100644 wasm_bindings/js/tests/test-wasm.js create mode 100644 wasm_bindings/package.json delete mode 100644 wasm_bindings/tests/web.rs diff --git a/wasm_bindings/Cargo.toml b/wasm_bindings/Cargo.toml index 7934c1e..1f4e5bb 100644 --- a/wasm_bindings/Cargo.toml +++ b/wasm_bindings/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "wasm_bindings" +name = "causalgraphs-wasm" version = "0.1.0" authors = ["Mohammed Razak "] -edition = "2018" +edition = "2021" [lib] crate-type = ["cdylib", "rlib"] diff --git a/wasm_bindings/js/tests/test-wasm.js b/wasm_bindings/js/tests/test-wasm.js new file mode 100644 index 0000000..def32d6 --- /dev/null +++ b/wasm_bindings/js/tests/test-wasm.js @@ -0,0 +1,85 @@ +/** + * Node.js test for CausalGraphs WASM + */ + +import * as causalgraphs from '../pkg-node/causalgraphs_wasm.js'; + +async function testWasm() { + console.log('🚀 Testing CausalGraphs WASM...\n'); + + try { + // No need to call init for node target! + // Create a new DAG + const dag = new causalgraphs.RustDAG(); + console.log('✅ DAG created successfully!\n'); + + // Add some nodes + dag.addNode('A'); + dag.addNode('B'); + dag.addNode('C'); + console.log('Added nodes: A, B, C'); + + // Add edges + dag.addEdge('A', 'B'); + dag.addEdge('B', 'C'); + console.log('Added edges: A→B, B→C\n'); + + // Get graph information + const nodes = dag.nodes(); + const edges = dag.edges(); + const nodeCount = dag.nodeCount; + const edgeCount = dag.edgeCount; + + console.log(`Nodes: ${nodes.join(', ')}`); + console.log(`Edges: ${JSON.stringify(edges)}`); + console.log(`Node count: ${nodeCount}`); + console.log(`Edge count: ${edgeCount}\n`); + + // Test graph traversal + const parentsOfC = dag.getParents('C'); + const childrenOfA = dag.getChildren('A'); + // const ancestorsOfC = dag.getAncestorsOf(['C']); + + console.log(`Parents of C: ${parentsOfC.join(', ')}`); + console.log(`Children of A: ${childrenOfA.join(', ')}`); + // console.log(`Ancestors of C: ${ancestorsOfC.join(', ')}\n`); + + // Test with latent variables + dag.addNode('L', true); // Add latent node + dag.addEdge('L', 'A'); + console.log('Added latent node L → A'); + + // Test more complex graph + const dag2 = new causalgraphs.RustDAG(); + const nodeNames = ['X', 'Y', 'Z', 'W', 'V']; + dag2.addNodesFrom(nodeNames); + + dag2.addEdge('X', 'Y'); + dag2.addEdge('Y', 'Z'); + dag2.addEdge('X', 'W'); + dag2.addEdge('W', 'Z'); + dag2.addEdge('V', 'X'); + + console.log('\nCreated complex graph:'); + console.log('V → X → Y → Z'); + console.log(' ↓ ↑'); + console.log(' W → Z\n'); + + const ancestorsOfZ = dag2.getAncestorsOf(['Z']); + const parentsOfZ = dag2.getParents('Z'); + const childrenOfX = dag2.getChildren('X'); + + console.log(`Ancestors of Z: ${ancestorsOfZ.join(', ')}`); + console.log(`Parents of Z: ${parentsOfZ.join(', ')}`); + console.log(`Children of X: ${childrenOfX.join(', ')}\n`); + + console.log('🎉 All tests passed!'); + + } catch (error) { + console.error('❌ Error:', error); + process.exit(1); + } +} + +// Run the test +testWasm(); \ No newline at end of file diff --git a/wasm_bindings/package.json b/wasm_bindings/package.json new file mode 100644 index 0000000..faf9c9a --- /dev/null +++ b/wasm_bindings/package.json @@ -0,0 +1,29 @@ +{ + "name": "causalgraphs-js", + "version": "0.1.0", + "description": "JavaScript/WASM bindings for CausalGraphs", + "type": "module", + "main": "causalgraphs.js", + "scripts": { + "test": "node js/tests/test-wasm.js", + "demo": "python3 -m http.server 8001", + "build": "wasm-pack build --target web --out-dir js/pkg-web", + "build:node": "wasm-pack build --target nodejs --out-dir js/pkg-node", + "clean": "rm -rf pkg-web pkg-node" + }, + "keywords": [ + "causal-graphs", + "dag", + "wasm", + "rust", + "graph-algorithms" + ], + "author": "Your Name", + "license": "MIT", + "files": [ + "pkg-web/", + "pkg-node/", + "demo/", + "test/" + ] +} \ No newline at end of file diff --git a/wasm_bindings/src/lib.rs b/wasm_bindings/src/lib.rs index 7f33f60..53021ed 100644 --- a/wasm_bindings/src/lib.rs +++ b/wasm_bindings/src/lib.rs @@ -2,47 +2,17 @@ use wasm_bindgen::prelude::*; use serde::{Deserialize, Serialize}; // For serializing/deserializing data to JS use std::collections::HashSet; // For returning HashSet from Rust to Vec in JS -// Import RustDAG from your core library -use rust_core::RustDAG; - -// Add a public struct to wrap RustDAG for WASM. -// It can directly be `RustDAG` if you make it `#[wasm_bindgen]` -// but sometimes a wrapper is cleaner for WASM for more control over JS API. -// Let's directly expose RustDAG as #[wasm_bindgen] for simplicity, similar to Python. -// However, RustDAG must be Clone, and its internal fields must be serializable if exposed. - -// You will likely want to make your RustDAG cloneable and serialize/deserialize for WASM, -// allowing it to be passed between Rust and JS contexts. -// Make sure `RustDAG` in `rust_core/src/dag.rs` has `#[derive(Clone, Serialize, Deserialize)]` if needed. -// IMPORTANT: `DiGraph` from `petgraph` does NOT implement `Serialize` or `Deserialize` directly. -// You'll need to either: -// 1. Manually serialize/deserialize `RustDAG` (complex). -// 2. Expose methods that operate on the graph but don't pass the graph *object* itself. -// 3. Use a different graph library if it provides Serde support. -// -// For simplicity in this example, let's assume `RustDAG` will have methods, -// but the struct itself won't be directly serialized/deserialized to JS object, -// unless you add custom Serde implementations. -// If you only pass primitives and call methods, `#[wasm_bindgen]` can apply directly. - -// Applying `#[wasm_bindgen]` to `RustDAG` directly. -// Note: If `RustDAG` in `rust_core` needs to be `Serialize`/`Deserialize` -// for use with `serde_wasm_bindgen`, you'll need to add `#[cfg_attr(feature = "wasm", derive(Serialize, Deserialize))]` -// to the `RustDAG` struct *in `rust_core/src/dag.rs`*. This requires `serde` to be -// an optional dependency in `rust_core`'s `Cargo.toml` with a `wasm` feature. -// This is getting more complex, so let's stick to methods for now. - -#[wasm_bindgen] +#[wasm_bindgen(js_name = RustDAG)] #[derive(Clone)] // Make sure RustDAG in rust_core also derives Clone -pub struct WasmDAG { // Use a wrapper struct named WasmDAG for clarity - inner: RustDAG, +pub struct RustDAG { // Use a wrapper struct named WasmDAG for clarity + inner: rust_core::RustDAG, } #[wasm_bindgen] -impl WasmDAG { +impl RustDAG { #[wasm_bindgen(constructor)] - pub fn new() -> WasmDAG { - WasmDAG { inner: RustDAG::new() } + pub fn new() -> RustDAG { + RustDAG { inner: rust_core::RustDAG::new() } } #[wasm_bindgen(js_name = addNode, catch)] diff --git a/wasm_bindings/tests/web.rs b/wasm_bindings/tests/web.rs deleted file mode 100644 index de5c1da..0000000 --- a/wasm_bindings/tests/web.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Test suite for the Web and headless browsers. - -#![cfg(target_arch = "wasm32")] - -extern crate wasm_bindgen_test; -use wasm_bindgen_test::*; - -wasm_bindgen_test_configure!(run_in_browser); - -#[wasm_bindgen_test] -fn pass() { - assert_eq!(1 + 1, 2); -} From 438a47a4e46e9b908dc617a3205cd38b3f8f8d75 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Fri, 4 Jul 2025 17:43:18 +0530 Subject: [PATCH 18/62] Refactor ancestor retrieval in RustDAG --- python_bindings/test.py | 12 ++++++++++++ rust_core/src/dag.rs | 15 +++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/python_bindings/test.py b/python_bindings/test.py index a23ea7d..97dcc2d 100644 --- a/python_bindings/test.py +++ b/python_bindings/test.py @@ -27,3 +27,15 @@ # Test ancestors (Rust-backed logic) ancestors_of_C = dag.get_ancestors_of(["C"]) print(f"Ancestors of C: {ancestors_of_C}") # Expected: {'A', 'B', 'C'} (order may vary, depends on your ancestor definition) + + +# create dag 2 +dag2 = causalgraphs.RustDAG() +dag2.add_nodes_from(["V", "W", "X", "Y", "Z"]) +dag2.add_edge("V", "X") +dag2.add_edge("X", "Y") +dag2.add_edge("X", "W") +dag2.add_edge("W", "Z") +dag2.add_edge("Y", "Z") +ancestorsOfZ = dag2.get_ancestors_of(['Z']) +print(f"Ancestors of Z: {ancestorsOfZ}") # Expected: ['V', 'W', 'X', 'Y', 'Z'] (order may vary) \ No newline at end of file diff --git a/rust_core/src/dag.rs b/rust_core/src/dag.rs index 35405fd..de55162 100644 --- a/rust_core/src/dag.rs +++ b/rust_core/src/dag.rs @@ -94,7 +94,7 @@ impl RustDAG { /// Get all ancestors of given nodes (optimized Rust implementation) pub fn get_ancestors_of(&self, nodes: Vec) -> Result, String> { - let mut ancestors: AHashSet = AHashSet::new(); + let mut ancestors: HashSet = HashSet::new(); let mut queue: VecDeque = VecDeque::new(); // Initialize queue with input nodes @@ -110,14 +110,17 @@ impl RustDAG { // BFS to find all ancestors while let Some(current_idx) = queue.pop_front() { for parent_idx in self.graph.neighbors_directed(current_idx, Direction::Incoming) { - let parent_name = &self.reverse_node_map[&parent_idx]; - if ancestors.insert(parent_name.clone()) { - queue.push_back(parent_idx); + if let Some(parent_name) = self.reverse_node_map.get(&parent_idx) { + if ancestors.insert(parent_name.clone()) { + queue.push_back(parent_idx); + } + } else { + return Err(format!("Node index {:?} not found in reverse map", parent_idx)); } } } - - Ok(ancestors.into_iter().collect()) + + Ok(ancestors) } /// Get all nodes in the graph From 3dbf9dc97ab069615b42e414c1cd7b21b7cdf1fd Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Fri, 4 Jul 2025 22:12:02 +0530 Subject: [PATCH 19/62] refactor `add_nodes_from` & test-wasm.js --- wasm_bindings/js/tests/test-wasm.js | 6 +++--- wasm_bindings/src/lib.rs | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/wasm_bindings/js/tests/test-wasm.js b/wasm_bindings/js/tests/test-wasm.js index def32d6..f985d74 100644 --- a/wasm_bindings/js/tests/test-wasm.js +++ b/wasm_bindings/js/tests/test-wasm.js @@ -38,11 +38,11 @@ async function testWasm() { // Test graph traversal const parentsOfC = dag.getParents('C'); const childrenOfA = dag.getChildren('A'); - // const ancestorsOfC = dag.getAncestorsOf(['C']); + const ancestorsOfC = dag.getAncestorsOf(['C']); console.log(`Parents of C: ${parentsOfC.join(', ')}`); console.log(`Children of A: ${childrenOfA.join(', ')}`); - // console.log(`Ancestors of C: ${ancestorsOfC.join(', ')}\n`); + console.log(`Ancestors of C: ${ancestorsOfC.join(', ')}\n`); // Test with latent variables dag.addNode('L', true); // Add latent node @@ -82,4 +82,4 @@ async function testWasm() { } // Run the test -testWasm(); \ No newline at end of file +testWasm(); \ No newline at end of file diff --git a/wasm_bindings/src/lib.rs b/wasm_bindings/src/lib.rs index 53021ed..e13a6e0 100644 --- a/wasm_bindings/src/lib.rs +++ b/wasm_bindings/src/lib.rs @@ -1,10 +1,10 @@ use wasm_bindgen::prelude::*; -use serde::{Deserialize, Serialize}; // For serializing/deserializing data to JS -use std::collections::HashSet; // For returning HashSet from Rust to Vec in JS +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; #[wasm_bindgen(js_name = RustDAG)] -#[derive(Clone)] // Make sure RustDAG in rust_core also derives Clone -pub struct RustDAG { // Use a wrapper struct named WasmDAG for clarity +#[derive(Clone)] +pub struct RustDAG { inner: rust_core::RustDAG, } @@ -21,7 +21,7 @@ impl RustDAG { .map_err(|e| JsValue::from_str(&e)) } - #[wasm_bindgen] + #[wasm_bindgen(js_name = addNodesFrom)] pub fn add_nodes_from(&mut self, nodes: Vec, latent: Option>) -> Result<(), JsValue> { let latent_bools = latent.map(|v| v.into_iter().map(|x| x != 0).collect()); self.inner.add_nodes_from(nodes, latent_bools).map_err(|e| JsValue::from_str(&e.to_string())) @@ -59,7 +59,7 @@ impl RustDAG { } #[wasm_bindgen(js_name = edges)] - pub fn edges(&self) -> JsValue { // Return JsValue for complex types like Vec<(String, String)> + pub fn edges(&self) -> JsValue { serde_wasm_bindgen::to_value(&self.inner.edges()).unwrap_or_else(|_| JsValue::from_str("Failed to serialize edges")) } @@ -87,8 +87,8 @@ pub fn main_js() -> Result<(), JsValue> { #[cfg(debug_assertions)] console_error_panic_hook::set_once(); - // You can do some initialization here if needed - web_sys::console::log_1(&"WasmDAG loaded!".into()); + // logs + web_sys::console::log_1(&"RustDAG loaded!".into()); Ok(()) } \ No newline at end of file From 46daced9bb7ffc9b6ce03979cae2dd267aa1ffe3 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Fri, 4 Jul 2025 22:20:37 +0530 Subject: [PATCH 20/62] Update pyo3 version to 0.21.2 and clean up imports in RustDAG --- Cargo.lock | 20 ++++++++++---------- python_bindings/Cargo.toml | 2 +- python_bindings/src/lib.rs | 2 +- rust_core/src/dag.rs | 1 - 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2f8ac7..5637812 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -287,9 +287,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.20.3" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" +checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" dependencies = [ "cfg-if", "indoc", @@ -305,9 +305,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.20.3" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" +checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50" dependencies = [ "once_cell", "target-lexicon", @@ -315,9 +315,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.20.3" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" +checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403" dependencies = [ "libc", "pyo3-build-config", @@ -325,9 +325,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.20.3" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" +checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -337,9 +337,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.20.3" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" +checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c" dependencies = [ "heck", "proc-macro2", diff --git a/python_bindings/Cargo.toml b/python_bindings/Cargo.toml index 42ae774..6b8427d 100644 --- a/python_bindings/Cargo.toml +++ b/python_bindings/Cargo.toml @@ -9,4 +9,4 @@ crate-type = ["cdylib"] [dependencies] rust_core = { path = "../rust_core" } -pyo3 = { version = "0.20", features = ["extension-module"] } +pyo3 = { version = "0.21", features = ["extension-module"] } diff --git a/python_bindings/src/lib.rs b/python_bindings/src/lib.rs index 52db9ea..b523d34 100644 --- a/python_bindings/src/lib.rs +++ b/python_bindings/src/lib.rs @@ -64,7 +64,7 @@ impl PyRustDAG { } #[pymodule] -fn causalgraphs(_py: Python, m: &PyModule) -> PyResult<()> { +fn causalgraphs(_py: Python, m: &Bound) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/rust_core/src/dag.rs b/rust_core/src/dag.rs index de55162..183a185 100644 --- a/rust_core/src/dag.rs +++ b/rust_core/src/dag.rs @@ -1,4 +1,3 @@ -use ahash::AHashSet; use petgraph::Direction; use rustworkx_core::petgraph::graph::{DiGraph, NodeIndex}; use std::collections::{HashMap, HashSet, VecDeque}; From cd41a9932258b0e4c92ff629d21082825d144495 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Fri, 4 Jul 2025 22:34:06 +0530 Subject: [PATCH 21/62] remove old js folder --- js/demo/test-wasm.html | 176 ----------------------------------------- js/package.json | 29 ------- js/test/test-wasm.js | 85 -------------------- 3 files changed, 290 deletions(-) delete mode 100644 js/demo/test-wasm.html delete mode 100644 js/package.json delete mode 100644 js/test/test-wasm.js diff --git a/js/demo/test-wasm.html b/js/demo/test-wasm.html deleted file mode 100644 index f4c6398..0000000 --- a/js/demo/test-wasm.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - CausalGraphs WASM Test - - - -
-

🚀 CausalGraphs WASM Test

-

Testing the compiled WASM functions from Rust.

- - - - -
Click a button to run tests...
-
- - - - \ No newline at end of file diff --git a/js/package.json b/js/package.json deleted file mode 100644 index e8d51f4..0000000 --- a/js/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "causalgraphs-js", - "version": "0.1.0", - "description": "JavaScript/WASM bindings for CausalGraphs", - "type": "module", - "main": "causalgraphs.js", - "scripts": { - "test": "node test/test-wasm.js", - "demo": "python3 -m http.server 8001", - "build": "cd .. && wasm-pack build --target web --out-dir js/pkg-web --features wasm", - "build:node": "cd .. && wasm-pack build --target nodejs --out-dir js/pkg-node --features wasm", - "clean": "rm -rf pkg-web pkg-node" - }, - "keywords": [ - "causal-graphs", - "dag", - "wasm", - "rust", - "graph-algorithms" - ], - "author": "Your Name", - "license": "MIT", - "files": [ - "pkg-web/", - "pkg-node/", - "demo/", - "test/" - ] -} \ No newline at end of file diff --git a/js/test/test-wasm.js b/js/test/test-wasm.js deleted file mode 100644 index 0f64223..0000000 --- a/js/test/test-wasm.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Node.js test for CausalGraphs WASM - */ - -import * as causalgraphs from '../pkg-node/causalgraphs.js'; - -async function testWasm() { - console.log('🚀 Testing CausalGraphs WASM...\n'); - - try { - // No need to call init for node target! - // Create a new DAG - const dag = new causalgraphs.RustDAG(); - console.log('✅ DAG created successfully!\n'); - - // Add some nodes - dag.addNode('A'); - dag.addNode('B'); - dag.addNode('C'); - console.log('Added nodes: A, B, C'); - - // Add edges - dag.addEdge('A', 'B'); - dag.addEdge('B', 'C'); - console.log('Added edges: A→B, B→C\n'); - - // Get graph information - const nodes = dag.nodes(); - const edges = dag.edges; - const nodeCount = dag.nodeCount; - const edgeCount = dag.edgeCount; - - console.log(`Nodes: ${nodes.join(', ')}`); - console.log(`Edges: ${JSON.stringify(edges)}`); - console.log(`Node count: ${nodeCount}`); - console.log(`Edge count: ${edgeCount}\n`); - - // Test graph traversal - const parentsOfC = dag.getParents('C'); - const childrenOfA = dag.getChildren('A'); - const ancestorsOfC = dag.getAncestorsOf(['C']); - - console.log(`Parents of C: ${parentsOfC.join(', ')}`); - console.log(`Children of A: ${childrenOfA.join(', ')}`); - console.log(`Ancestors of C: ${ancestorsOfC.join(', ')}\n`); - - // Test with latent variables - dag.addNode('L', true); // Add latent node - dag.addEdge('L', 'A'); - console.log('Added latent node L → A'); - - // Test more complex graph - const dag2 = new causalgraphs.RustDAG(); - const nodeNames = ['X', 'Y', 'Z', 'W', 'V']; - dag2.addNodesFrom(nodeNames); - - dag2.addEdge('X', 'Y'); - dag2.addEdge('Y', 'Z'); - dag2.addEdge('X', 'W'); - dag2.addEdge('W', 'Z'); - dag2.addEdge('V', 'X'); - - console.log('\nCreated complex graph:'); - console.log('V → X → Y → Z'); - console.log(' ↓ ↑'); - console.log(' W → Z\n'); - - const ancestorsOfZ = dag2.getAncestorsOf(['Z']); - const parentsOfZ = dag2.getParents('Z'); - const childrenOfX = dag2.getChildren('X'); - - console.log(`Ancestors of Z: ${ancestorsOfZ.join(', ')}`); - console.log(`Parents of Z: ${parentsOfZ.join(', ')}`); - console.log(`Children of X: ${childrenOfX.join(', ')}\n`); - - console.log('🎉 All tests passed!'); - - } catch (error) { - console.error('❌ Error:', error); - process.exit(1); - } -} - -// Run the test -testWasm(); \ No newline at end of file From f72095806c09de9ede59a970b62945b246d9cf3e Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Fri, 4 Jul 2025 22:46:50 +0530 Subject: [PATCH 22/62] update README.md's --- README.md | 95 ++++++++++++++++----------------------- python_bindings/README.md | 53 ++++++++++++++++++++++ wasm_bindings/README.md | 92 +++++++++---------------------------- 3 files changed, 113 insertions(+), 127 deletions(-) create mode 100644 python_bindings/README.md diff --git a/README.md b/README.md index 5021de3..a3b54a5 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,39 @@ -# causalgraphs -Rust implemention of causal graphs - - -## Building & Installing the Python Extension - -First, make sure you have Rust installed on your system. If you haven't already done so, try following the instructions [here](https://www.rust-lang.org/tools/install). - - -This project uses [PyO3](https://pyo3.rs/v0.25.1/) to create Python bindings for the Rust code. -We use [Maturin](https://github.com/PyO3/maturin) build tool to compile(.so/.dll) our Rust core into a native Python module(.whl) and install it into a local venv. - -1. **Clone the repo** - ```bash - git clone https://github.com/pgmpy/causalgraphs.git - cd causalgraphs - ``` - -2. **Create & activate a venv** - ```bash - cd python - python3 -m venv .venv - source .venv/bin/activate - ``` -3. **Install Python requirements** - ```bash - pip install -r requirements.txt - ``` -4. **Build & install the Rust extension** - From the project root (where Cargo.toml and pyproject.toml live): - ```bash - maturin build --release - ``` - This will compile the Rust code and generate a .whl file in the target/wheels/ directory. You can then install it: - - ```bash - pip install target/wheels/causalgraphs-0.1.0-*.whl - ``` - - (Replace your_package_name-0.1.0-cp3x-cp3x-your_platform.whl with the actual filename.) - - ```bash - Note: The `causalgraphs` package is not yet published on PyPI. You must install it from the locally built wheel as shown above while the project is in active development. - ``` ---- - -5. **🚀 Usage** - - Once installed, you can import the `DAG` from `causalgraphs` and use it just like a regular Python class. - ```bash - >>> from causalgraphs import RustDAG - >>> dag = RustDAG() - >>> dag.add_node("A") - >>> dag.nodes() - ["A"] - ``` +# CausalGraphs + +A cross-language library for working with causal graphs (DAGs) in Rust, Python, and WebAssembly. + +## Structure + +- **rust_core/**: Core Rust implementation of DAGs and causal graph logic. +- **python_bindings/**: Python bindings using [PyO3](https://github.com/PyO3/pyo3) and [maturin](https://github.com/PyO3/maturin). +- **wasm_bindings/**: WebAssembly bindings for use in JavaScript/Node.js. + +## Quick Start + +### Rust + +```sh +cd rust_core +cargo test +``` + +### Python + +```sh +cd python_bindings +maturin develop +python -c "import causalgraphs; print(dir(causalgraphs))" +``` + +### WebAssembly (Node.js) + +```sh +cd wasm_bindings +npm install +npm run build +npm run test +``` + +## License + +MIT \ No newline at end of file diff --git a/python_bindings/README.md b/python_bindings/README.md new file mode 100644 index 0000000..bd5a9a1 --- /dev/null +++ b/python_bindings/README.md @@ -0,0 +1,53 @@ +# Python Bindings for CausalGraphs + +This package provides Python bindings for the Rust `causalgraphs` library using [PyO3](https://github.com/PyO3/pyo3). + +## Setup (Recommended) + +It is recommended to use a virtual environment for Python development: + +```sh +python3 -m venv .venv +source .venv/bin/activate +``` + +If you have a `requirements.txt`, install dependencies: + +```sh +pip install -r requirements.txt +``` + +## Build & Install + +Install [maturin](https://github.com/PyO3/maturin) if you haven't already: + +```sh +pip install maturin +``` + +Then build and install the Rust extension in your environment: + +```sh +maturin develop +``` + +## Usage Example + +```python +from causalgraphs import RustDAG + +dag = RustDAG() +dag.add_node("A") +dag.add_node("B") +dag.add_edge("A", "B") +print(dag.nodes()) +print(dag.edges()) +``` + +## Development Notes + +- Edit Rust code in `src/`. +- Run `maturin develop` to rebuild the Python extension. +- Run your Python tests as needed. + +## License \ No newline at end of file diff --git a/wasm_bindings/README.md b/wasm_bindings/README.md index 6b68408..7731a8d 100644 --- a/wasm_bindings/README.md +++ b/wasm_bindings/README.md @@ -1,84 +1,34 @@ -
+# WASM Bindings for CausalGraphs -

wasm-pack-template

+WebAssembly (WASM) bindings for the Rust `causalgraphs` library, usable from JavaScript and Node.js. - A template for kick starting a Rust and WebAssembly project using wasm-pack. +## Build -

- Build Status -

- -

- Tutorial - | - Chat -

- - Built with 🦀🕸 by The Rust and WebAssembly Working Group -
- -## About - -[**📚 Read this template tutorial! 📚**][template-docs] - -This template is designed for compiling Rust libraries into WebAssembly and -publishing the resulting package to NPM. - -Be sure to check out [other `wasm-pack` tutorials online][tutorials] for other -templates and usages of `wasm-pack`. - -[tutorials]: https://rustwasm.github.io/docs/wasm-pack/tutorials/index.html -[template-docs]: https://rustwasm.github.io/docs/wasm-pack/tutorials/npm-browser-packages/index.html - -## 🚴 Usage - -### 🐑 Use `cargo generate` to Clone this Template - -[Learn more about `cargo generate` here.](https://github.com/ashleygwilliams/cargo-generate) - -``` -cargo generate --git https://github.com/rustwasm/wasm-pack-template.git --name my-project -cd my-project +```sh +npm run build ``` -### 🛠️ Build with `wasm-pack build` +## Test +```sh +npm run test ``` -wasm-pack build -``` - -### 🔬 Test in Headless Browsers with `wasm-pack test` -``` -wasm-pack test --headless --firefox -``` +## Usage Example -### 🎁 Publish to NPM with `wasm-pack publish` +```js +import * as causalgraphs from './pkg-node/causalgraphs_wasm.js'; +const dag = new causalgraphs.RustDAG(); +dag.addNode('A'); +dag.addNode('B'); +dag.addEdge('A', 'B'); +console.log(dag.nodes()); +console.log(dag.edges()); ``` -wasm-pack publish -``` - -## 🔋 Batteries Included - -* [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating - between WebAssembly and JavaScript. -* [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook) - for logging panic messages to the developer console. -* `LICENSE-APACHE` and `LICENSE-MIT`: most Rust projects are licensed this way, so these are included for you - -## License - -Licensed under either of - -* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) -* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) - -at your option. -### Contribution +## Development -Unless you explicitly state otherwise, any contribution intentionally -submitted for inclusion in the work by you, as defined in the Apache-2.0 -license, shall be dual licensed as above, without any additional terms or -conditions. +- Rust source: `../rust_core` +- WASM bindings: `src/` +- JS tests: `js/tests/` \ No newline at end of file From 13c6c5356d7f886f238af693e1dcc0605103eab3 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 5 Jul 2025 14:48:49 +0530 Subject: [PATCH 23/62] initial r bindings changes --- Cargo.lock | 84 +++++++++++++++++++++++++++++++++++++++---- Cargo.toml | 4 +-- r_bindings/Cargo.toml | 11 ++++++ r_bindings/src/lib.rs | 73 +++++++++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 r_bindings/Cargo.toml create mode 100644 r_bindings/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 5c4d449..6f16553 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,39 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "extendr-api" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e36d66fa307948c291a6fc5b09d8295dd58e88ab5e8d782d30e23670113e9ab" +dependencies = [ + "extendr-engine", + "extendr-macros", + "lazy_static", + "libR-sys", + "paste", +] + +[[package]] +name = "extendr-engine" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8298d5a2e38bb91820b92bbd7e5aaf1d3b95ed9f096fc66393c50af38ff8155d" +dependencies = [ + "libR-sys", +] + +[[package]] +name = "extendr-macros" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09bf0849f0d48209be8163378248137fed5ccb5f464d171cf93a19f31a9e6c67" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -234,6 +267,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libR-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd728a97b9b0975f546bc865a7413e0ce6f98a8f6cea52e77dc5ee0bcea00adf" + [[package]] name = "libc" version = "0.2.174" @@ -313,6 +358,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "petgraph" version = "0.6.5" @@ -404,7 +455,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -417,7 +468,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -435,6 +486,14 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r_bindings" +version = "0.1.0" +dependencies = [ + "extendr-api", + "rust_core", +] + [[package]] name = "rand" version = "0.8.5" @@ -592,7 +651,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -607,6 +666,17 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.104" @@ -710,7 +780,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -745,7 +815,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -780,7 +850,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] [[package]] @@ -901,5 +971,5 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.104", ] diff --git a/Cargo.toml b/Cargo.toml index fb4c2dc..6eea7e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,5 +3,5 @@ members = [ "rust_core", "python_bindings", "wasm_bindings" -] -resolver = "2" \ No newline at end of file +, "r_bindings"] +resolver = "2" diff --git a/r_bindings/Cargo.toml b/r_bindings/Cargo.toml new file mode 100644 index 0000000..b74191e --- /dev/null +++ b/r_bindings/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "r_bindings" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +rust_core = { path = "../rust_core" } +extendr-api = "0.4.0" diff --git a/r_bindings/src/lib.rs b/r_bindings/src/lib.rs new file mode 100644 index 0000000..58934c7 --- /dev/null +++ b/r_bindings/src/lib.rs @@ -0,0 +1,73 @@ +use extendr_api::prelude::*; +use rust_core::RustDAG; +use extendr_api::Nullable; + +pub struct RDAG { + inner: RustDAG, +} + +#[extendr] +impl RDAG { + fn new() -> Self { + RDAG { inner: RustDAG::new() } + } + + fn add_node(&mut self, node: String, latent: Option) -> extendr_api::Result<()> { + self.inner.add_node(node, latent.unwrap_or(false)) + .map_err(Error::from) + } + + fn add_nodes_from(&mut self, nodes: Vec, latent: Nullable>) -> extendr_api::Result<()> { + let latent_opt: Option> = latent.into_option().map(|v| v.into_iter().map(|x| x != 0).collect()); + self.inner.add_nodes_from(nodes, latent_opt) + .map_err(Error::from) + } + + fn add_edge(&mut self, u: String, v: String, weight: Option) -> extendr_api::Result<()> { + self.inner.add_edge(u, v, weight) + .map_err(Error::from) + } + + fn get_parents(&self, node: String) -> extendr_api::Result> { + self.inner.get_parents(&node) + .map_err(Error::from) + } + + fn get_children(&self, node: String) -> extendr_api::Result> { + self.inner.get_children(&node) + .map_err(Error::from) + } + + fn get_ancestors_of(&self, nodes: Vec) -> extendr_api::Result> { + Ok(self.inner.get_ancestors_of(nodes) + .map_err(Error::from)? + .into_iter().collect()) + } + + fn nodes(&self) -> Vec { + self.inner.nodes() + } + + fn edges(&self) -> List { + let edges = self.inner.edges(); + let (from, to): (Vec<_>, Vec<_>) = edges.into_iter().unzip(); + list!(from = from, to = to) + } + + fn node_count(&self) -> usize { + self.inner.node_count() + } + + fn edge_count(&self) -> usize { + self.inner.edge_count() + } + + fn latents(&self) -> Vec { + self.inner.latents.iter().cloned().collect() + } +} + +extendr_module! { + mod causalgraphs; + impl RDAG; +} \ No newline at end of file From a9b2c05352f5389c605dcde439026fc5d3fc51da Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 5 Jul 2025 15:57:27 +0530 Subject: [PATCH 24/62] R bindings: extendr setup https://extendr.github.io/user-guide/r-pkgs/package-setup.html --- Cargo.lock | 82 +++++------- Cargo.toml | 3 +- r_bindings/Cargo.toml | 11 -- r_bindings/causalgraphs/.RData | Bin 0 -> 2595 bytes r_bindings/causalgraphs/.Rbuildignore | 5 + r_bindings/causalgraphs/.Rhistory | 3 + r_bindings/causalgraphs/.gitignore | 3 + r_bindings/causalgraphs/DESCRIPTION | 15 +++ r_bindings/causalgraphs/NAMESPACE | 2 + r_bindings/causalgraphs/R/extendr-wrappers.R | 12 ++ r_bindings/causalgraphs/configure | 3 + r_bindings/causalgraphs/configure.win | 2 + r_bindings/causalgraphs/src/.gitignore | 5 + r_bindings/causalgraphs/src/Makevars.in | 42 +++++++ r_bindings/causalgraphs/src/Makevars.win.in | 39 ++++++ .../causalgraphs/src/causalgraphs-win.def | 2 + r_bindings/causalgraphs/src/entrypoint.c | 8 ++ r_bindings/causalgraphs/src/rust/Cargo.toml | 14 +++ r_bindings/causalgraphs/src/rust/src/lib.rs | 118 ++++++++++++++++++ r_bindings/causalgraphs/tools/config.R | 104 +++++++++++++++ r_bindings/causalgraphs/tools/msrv.R | 116 +++++++++++++++++ r_bindings/src/lib.rs | 73 ----------- 22 files changed, 526 insertions(+), 136 deletions(-) delete mode 100644 r_bindings/Cargo.toml create mode 100644 r_bindings/causalgraphs/.RData create mode 100644 r_bindings/causalgraphs/.Rbuildignore create mode 100644 r_bindings/causalgraphs/.Rhistory create mode 100644 r_bindings/causalgraphs/.gitignore create mode 100644 r_bindings/causalgraphs/DESCRIPTION create mode 100644 r_bindings/causalgraphs/NAMESPACE create mode 100644 r_bindings/causalgraphs/R/extendr-wrappers.R create mode 100755 r_bindings/causalgraphs/configure create mode 100644 r_bindings/causalgraphs/configure.win create mode 100644 r_bindings/causalgraphs/src/.gitignore create mode 100644 r_bindings/causalgraphs/src/Makevars.in create mode 100644 r_bindings/causalgraphs/src/Makevars.win.in create mode 100644 r_bindings/causalgraphs/src/causalgraphs-win.def create mode 100644 r_bindings/causalgraphs/src/entrypoint.c create mode 100644 r_bindings/causalgraphs/src/rust/Cargo.toml create mode 100644 r_bindings/causalgraphs/src/rust/src/lib.rs create mode 100644 r_bindings/causalgraphs/tools/config.R create mode 100644 r_bindings/causalgraphs/tools/msrv.R delete mode 100644 r_bindings/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 6f16553..c34e27c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,12 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "build-print" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a2128d00b7061b82b72844a351e80acd29e05afc60e9261e2ac90dca9ecc2ac" + [[package]] name = "bumpalo" version = "3.19.0" @@ -128,35 +134,34 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "extendr-api" -version = "0.4.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e36d66fa307948c291a6fc5b09d8295dd58e88ab5e8d782d30e23670113e9ab" +checksum = "cae12193370e4f00f4a54b64f40dabc753ad755f15229c367e4b5851ed206954" dependencies = [ - "extendr-engine", + "extendr-ffi", "extendr-macros", - "lazy_static", - "libR-sys", + "once_cell", "paste", ] [[package]] -name = "extendr-engine" -version = "0.4.0" +name = "extendr-ffi" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8298d5a2e38bb91820b92bbd7e5aaf1d3b95ed9f096fc66393c50af38ff8155d" +checksum = "b48f96b7a4a2ff009ad9087f22a6de2312731a4096b520e3eb1c483df476ae95" dependencies = [ - "libR-sys", + "build-print", ] [[package]] name = "extendr-macros" -version = "0.4.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09bf0849f0d48209be8163378248137fed5ccb5f464d171cf93a19f31a9e6c67" +checksum = "fdbbac9afddafddb4dabd10aefa8082e3f057ec5bfa519c7b44af114e7ebf1a5" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] @@ -267,18 +272,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libR-sys" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd728a97b9b0975f546bc865a7413e0ce6f98a8f6cea52e77dc5ee0bcea00adf" - [[package]] name = "libc" version = "0.2.174" @@ -455,7 +448,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.104", + "syn", ] [[package]] @@ -468,7 +461,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.104", + "syn", ] [[package]] @@ -486,14 +479,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "r_bindings" -version = "0.1.0" -dependencies = [ - "extendr-api", - "rust_core", -] - [[package]] name = "rand" version = "0.8.5" @@ -564,6 +549,14 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcausalgraphs" +version = "0.1.0" +dependencies = [ + "extendr-api", + "rust_core", +] + [[package]] name = "redox_syscall" version = "0.5.13" @@ -651,7 +644,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn", ] [[package]] @@ -666,17 +659,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.104" @@ -780,7 +762,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn", "wasm-bindgen-shared", ] @@ -815,7 +797,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -850,7 +832,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn", ] [[package]] @@ -971,5 +953,5 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn", ] diff --git a/Cargo.toml b/Cargo.toml index 6eea7e7..a2532c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,5 @@ members = [ "rust_core", "python_bindings", - "wasm_bindings" -, "r_bindings"] + "wasm_bindings", "r_bindings/causalgraphs/src/rust"] resolver = "2" diff --git a/r_bindings/Cargo.toml b/r_bindings/Cargo.toml deleted file mode 100644 index b74191e..0000000 --- a/r_bindings/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "r_bindings" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -rust_core = { path = "../rust_core" } -extendr-api = "0.4.0" diff --git a/r_bindings/causalgraphs/.RData b/r_bindings/causalgraphs/.RData new file mode 100644 index 0000000000000000000000000000000000000000..c9dab282eee4bf542ac6b753684f8ffcad1913eb GIT binary patch literal 2595 zcmV+;3f%P{iwFP!000000|5*Q^Yv0hSThP(3IG5A0{{d80ssRA000001yxi=EjR!G z1Ofm60096500{s901PftVQyq^Z7y?VWn=&V01W^D0&)NVD5C%X00x#Jx8VWN1#|c8 zt>U>YVN{9K&{v;D{7ucQujB;m-O5G_WHoBXqK|GG+m=yMXFvct3In7_a3R2g8xWtP zIPMS^IGJxF?JJ9#L>h{9w%qDx9^XQ%Sha?RE=(OzvZ9rTIsGlh68Ft7^d3lSKG-xM zf;*@gKiM~>0j^1{>AH63Jq3zyGJ{{D#{ytoj`E}Q&( z{Q2sSK1hK>Id)YXAhGc%6ys~vW3s@L+yvL(Uq^^Ab{(9JjFjJbJUUIcHibOZ2VX63 zN9JlzYH*QpJ(I$FiZMEl=-|?F!F~&PBnij3?AkK+zO2@;dKRi*v}&)JJyJivw%34C&N?95n+3(*U&0OUoHobqS^C%DKpz?zR{ctY=DwC{a$H)(y5m^9b51>pK5IY77RU@o=<1Ua|N}iqr7~+mXdzE zb&cshMaK}Nwyuv8-v}J+{R=^`d+8!r(G|ER8?P-EL$(++dEg{>86(t5?m?RmK`{3?(@tFWnYkKpqsv!* zn0lHSNqmKz`M-+}_LZg(tgMd`)`Ic5JXYGIauDR{yHUC8*o#;7(Dn-*A#Nr zJ;J?`65x62?|j?dp3vN~aH2~#q$tAe#SHKmPrP@2u`*5@@xa-NdNh4WbI2}$5V}V0 z>5wuVN$egBx>Q}|sFDL;Cm;kc(ae(mMc~oYHo}!`{3z*{TC7-dbi7|;A@a!#Auu&% ze55+!&bT9HEm{XmZIFUTTYP=cffNAqQ$laFivVuFx4ak#Cs0gyb0S5h^+>hz!VDL< z_*{qH)Y7H0VgLuj6;u?-;pmuv%ld5W@X%D~?JSk-0`wDIv64d>Bw!Nk?F=$}Lg3${ zabj`68mPb2qD=J@$$YiaeKLK5AaI^(zeF_&)!2od{CfS|jxa*tlDw>XcT~_7)IzdO z9iaLR3cTB65zv}#)nZCk|LBbHf6R1G2K=px+4AAx(1644T=W|cUWl*{k2<1o`%ob# z-fVNr7?fTT=~4=!*5Ic#8q9#ru%L+&TY@|v(&*QBGVHs`j6Bh|N^<8Q#Bj`JUL;h5 zJUH-uXc}9~TF|-RtUQtNB$OTAT!PW~KfrBmaXbWvI*`p!WFoB5#0ZK!sEm$ov>dA~ z?vmaIB!JRfFcd#uJt)y#ZaM+6Owbd?WNZr~g0y*K&=OUJSK!0gGBD)a#&&3S~_fw{_rTU1iTEzmJ~uT_`-l9*kF!~ z6@2fi6wnWnd?LR*f~ZByw~XO>k-QvKR5EoC_dr2HIJAnMi%2<1db$^dHxNxppzK3h zBvgjRPZHwzyP&NhPt1L+i(n;O5W+7&<|y#a<*fNiKRj_xS8`BU>o5;x(j*L$xH$J| zWZK*viI7oPru->a@069-OF}j#VgL=3T0E2p-ca*+I-+|qJ4lOELJX9jJlxP@>QX|W zXn-MKD^#B;x#%ztn))|aM^JP&94x4l1$6El7LtLX3Sj3l(u~+o%V2+2^5WoTfT%lH zaLnlGIs7j>U$Q3UW$%!!X;~-VlKfI|c zs!*G1-Qr$#QD}`oJxrkxnEW}jGV-lL2LRl{Ms#qZI0)CxX}W3h$I$K0dF(5DpOo69 zveFv4eqiWi6-;C-#US5h5`uzd`RK<<$LtqAW<1RuK62XB18_3om|Xpiy3HV3-Zx-< z1i)lf`<@&MNNW)&=k5Jn>RQCkjTp4;I)SOnXfv@@PE^*U>SA&fy!Rxb=RsBzCC2c4 z_XNndLbXzz9iLw5(fC?%U)I^)dSh5if+hE{iNYjMjxeODvtoS<;uI-A4R+q+hQDHq|lU>Z~1m2(<=t2Se)cIw#r}WZ@*N$ zy9p0sv>vUPz07v>*PX|Y;wK{I@K8M>`0hI@rJGjx>-__HHtN-zfr#8PPcgqY-=m*f zcccPFB2ruYd8hr?E%9|w88)5banxa`*JZzPM{jwAsi+fij|Mu*+A|>C2$hYY`0@^< z!9cImuBj4Pw;N$@fb5zSJo>hPl`p|1aXA7rb52PW>71M|BMsK91$Xl~QGku}2j11D z0IL*+qU-JdFq7;jP>!Rd5l(*)_eu30528QDQUc{($-}M?m z=3m`Wz_^IP1(xsSXQKxFi)GLB%7O$ohwuhGp_$gq?~8tMYJ6Z>SgP?cdfzTCXlXda zVt&ijdTfmucD+cfYlq^sKX4};#twDZ8$0fjC>^1dkjf~hi|7Z>AZ-8u0RE=Qv&9Sw F0057%?k4~M literal 0 HcmV?d00001 diff --git a/r_bindings/causalgraphs/.Rbuildignore b/r_bindings/causalgraphs/.Rbuildignore new file mode 100644 index 0000000..e53acac --- /dev/null +++ b/r_bindings/causalgraphs/.Rbuildignore @@ -0,0 +1,5 @@ +^src/\.cargo$ +^src/rust/vendor$ +^src/rust/target$ +^src/Makevars$ +^src/Makevars\.win$ diff --git a/r_bindings/causalgraphs/.Rhistory b/r_bindings/causalgraphs/.Rhistory new file mode 100644 index 0000000..634372b --- /dev/null +++ b/r_bindings/causalgraphs/.Rhistory @@ -0,0 +1,3 @@ +setwd("/home/razak/git/causalgraphs-fresh/r-bindings") +usethis::create_package("causalgraphs") +q() diff --git a/r_bindings/causalgraphs/.gitignore b/r_bindings/causalgraphs/.gitignore new file mode 100644 index 0000000..20cff42 --- /dev/null +++ b/r_bindings/causalgraphs/.gitignore @@ -0,0 +1,3 @@ +src/rust/vendor +src/Makevars +src/Makevars.win diff --git a/r_bindings/causalgraphs/DESCRIPTION b/r_bindings/causalgraphs/DESCRIPTION new file mode 100644 index 0000000..d3062b1 --- /dev/null +++ b/r_bindings/causalgraphs/DESCRIPTION @@ -0,0 +1,15 @@ +Package: causalgraphs +Title: What the Package Does (One Line, Title Case) +Version: 0.0.0.9000 +Authors@R: + person("First", "Last", , "first.last@example.com", role = c("aut", "cre")) +Description: What the package does (one paragraph). +License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a + license +Encoding: UTF-8 +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.3.2 +Config/rextendr/version: 0.4.1 +SystemRequirements: Cargo (Rust's package manager), rustc +Depends: + R (>= 4.1) diff --git a/r_bindings/causalgraphs/NAMESPACE b/r_bindings/causalgraphs/NAMESPACE new file mode 100644 index 0000000..6ae9268 --- /dev/null +++ b/r_bindings/causalgraphs/NAMESPACE @@ -0,0 +1,2 @@ +# Generated by roxygen2: do not edit by hand + diff --git a/r_bindings/causalgraphs/R/extendr-wrappers.R b/r_bindings/causalgraphs/R/extendr-wrappers.R new file mode 100644 index 0000000..254c5e3 --- /dev/null +++ b/r_bindings/causalgraphs/R/extendr-wrappers.R @@ -0,0 +1,12 @@ +# nolint start + +#' @docType package +#' @usage NULL +#' @useDynLib causalgraphs, .registration = TRUE +NULL + +#' Return string `"Hello world!"` to R. +#' @export +hello_world <- function() .Call(wrap__hello_world) + +# nolint end diff --git a/r_bindings/causalgraphs/configure b/r_bindings/causalgraphs/configure new file mode 100755 index 0000000..c608b11 --- /dev/null +++ b/r_bindings/causalgraphs/configure @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +: "${R_HOME=`R RHOME`}" +"${R_HOME}/bin/Rscript" tools/config.R diff --git a/r_bindings/causalgraphs/configure.win b/r_bindings/causalgraphs/configure.win new file mode 100644 index 0000000..57eb255 --- /dev/null +++ b/r_bindings/causalgraphs/configure.win @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +"${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" tools/config.R diff --git a/r_bindings/causalgraphs/src/.gitignore b/r_bindings/causalgraphs/src/.gitignore new file mode 100644 index 0000000..c23c7b3 --- /dev/null +++ b/r_bindings/causalgraphs/src/.gitignore @@ -0,0 +1,5 @@ +*.o +*.so +*.dll +target +.cargo diff --git a/r_bindings/causalgraphs/src/Makevars.in b/r_bindings/causalgraphs/src/Makevars.in new file mode 100644 index 0000000..00d49e3 --- /dev/null +++ b/r_bindings/causalgraphs/src/Makevars.in @@ -0,0 +1,42 @@ +TARGET_DIR = ./rust/target +LIBDIR = $(TARGET_DIR)/@LIBDIR@ +STATLIB = $(LIBDIR)/librcausalgraphs.a +PKG_LIBS = -L$(LIBDIR) -lrcausalgraphs + +all: $(SHLIB) rust_clean + +.PHONY: $(STATLIB) + +$(SHLIB): $(STATLIB) + +CARGOTMP = $(CURDIR)/.cargo +VENDOR_DIR = $(CURDIR)/vendor + + +# RUSTFLAGS appends --print=native-static-libs to ensure that +# the correct linkers are used. Use this for debugging if need. +# +# CRAN note: Cargo and Rustc versions are reported during +# configure via tools/msrv.R. +# +# vendor.tar.xz, if present, is unzipped and used for offline compilation. +$(STATLIB): + + if [ -f ./rust/vendor.tar.xz ]; then \ + tar xf rust/vendor.tar.xz && \ + mkdir -p $(CARGOTMP) && \ + cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ + fi + + export CARGO_HOME=$(CARGOTMP) && \ + export PATH="$(PATH):$(HOME)/.cargo/bin" && \ + RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build @CRAN_FLAGS@ --lib @PROFILE@ --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) @TARGET@ + + # Always clean up CARGOTMP + rm -Rf $(CARGOTMP); + +rust_clean: $(SHLIB) + rm -Rf $(CARGOTMP) $(VENDOR_DIR) @CLEAN_TARGET@ + +clean: + rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) $(VENDOR_DIR) diff --git a/r_bindings/causalgraphs/src/Makevars.win.in b/r_bindings/causalgraphs/src/Makevars.win.in new file mode 100644 index 0000000..945a7db --- /dev/null +++ b/r_bindings/causalgraphs/src/Makevars.win.in @@ -0,0 +1,39 @@ +TARGET = $(subst 64,x86_64,$(subst 32,i686,$(WIN)))-pc-windows-gnu + +TARGET_DIR = ./rust/target +LIBDIR = $(TARGET_DIR)/$(TARGET)/@LIBDIR@ +STATLIB = $(LIBDIR)/librcausalgraphs.a +PKG_LIBS = -L$(LIBDIR) -lrcausalgraphs -lws2_32 -ladvapi32 -luserenv -lbcrypt -lntdll + +all: $(SHLIB) rust_clean + +.PHONY: $(STATLIB) + +$(SHLIB): $(STATLIB) + +CARGOTMP = $(CURDIR)/.cargo +VENDOR_DIR = vendor + +$(STATLIB): + mkdir -p $(TARGET_DIR)/libgcc_mock + touch $(TARGET_DIR)/libgcc_mock/libgcc_eh.a + + if [ -f ./rust/vendor.tar.xz ]; then \ + tar xf rust/vendor.tar.xz && \ + mkdir -p $(CARGOTMP) && \ + cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ + fi + + # Build the project using Cargo with additional flags + export CARGO_HOME=$(CARGOTMP) && \ + export LIBRARY_PATH="$(LIBRARY_PATH);$(CURDIR)/$(TARGET_DIR)/libgcc_mock" && \ + RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build @CRAN_FLAGS@ --target=$(TARGET) --lib @PROFILE@ --manifest-path=rust/Cargo.toml --target-dir=$(TARGET_DIR) + + # Always clean up CARGOTMP + rm -Rf $(CARGOTMP); + +rust_clean: $(SHLIB) + rm -Rf $(CARGOTMP) $(VENDOR_DIR) @CLEAN_TARGET@ + +clean: + rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) $(VENDOR_DIR) diff --git a/r_bindings/causalgraphs/src/causalgraphs-win.def b/r_bindings/causalgraphs/src/causalgraphs-win.def new file mode 100644 index 0000000..dbd71cf --- /dev/null +++ b/r_bindings/causalgraphs/src/causalgraphs-win.def @@ -0,0 +1,2 @@ +EXPORTS +R_init_causalgraphs diff --git a/r_bindings/causalgraphs/src/entrypoint.c b/r_bindings/causalgraphs/src/entrypoint.c new file mode 100644 index 0000000..a5f60cd --- /dev/null +++ b/r_bindings/causalgraphs/src/entrypoint.c @@ -0,0 +1,8 @@ +// We need to forward routine registration from C to Rust +// to avoid the linker removing the static library. + +void R_init_causalgraphs_extendr(void *dll); + +void R_init_causalgraphs(void *dll) { + R_init_causalgraphs_extendr(dll); +} diff --git a/r_bindings/causalgraphs/src/rust/Cargo.toml b/r_bindings/causalgraphs/src/rust/Cargo.toml new file mode 100644 index 0000000..9ad35bf --- /dev/null +++ b/r_bindings/causalgraphs/src/rust/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = 'rcausalgraphs' +publish = false +version = '0.1.0' +edition = '2021' +rust-version = '1.65' + +[lib] +crate-type = [ 'staticlib' ] +name = 'rcausalgraphs' + +[dependencies] +rust_core = { path = "../../../../rust_core" } +extendr-api = '*' diff --git a/r_bindings/causalgraphs/src/rust/src/lib.rs b/r_bindings/causalgraphs/src/rust/src/lib.rs new file mode 100644 index 0000000..c300baa --- /dev/null +++ b/r_bindings/causalgraphs/src/rust/src/lib.rs @@ -0,0 +1,118 @@ +use extendr_api::prelude::*; +use rust_core::RustDAG; +use std::collections::HashSet; + +#[extendr] +#[derive(Debug, Clone)] +pub struct RDAG { + inner: RustDAG, +} + +#[extendr] +impl RDAG { + /// Create a new DAG + /// @export + fn new() -> Self { + RDAG { inner: RustDAG::new() } + } + + /// Add a single node to the DAG + /// @param node The node name + /// @param latent Whether the node is latent (default: FALSE) + /// @export + fn add_node(&mut self, node: String, latent: Option) -> extendr_api::Result<()> { + self.inner.add_node(node, latent.unwrap_or(false)) + .map_err(Error::from) + } + + + /// Add multiple nodes to the DAG + /// @param nodes Vector of node names + /// @param latent Optional vector of latent flags + /// @export + fn add_nodes_from(&mut self, nodes: Strings, latent: Nullable) -> extendr_api::Result<()> { + let node_vec: Vec = nodes.iter().map(|s| s.to_string()).collect(); + let latent_opt: Option> = latent.into_option().map(|v| v.iter().map(|x| x.is_true()).collect()); + + self.inner.add_nodes_from(node_vec, latent_opt) + .map_err(|e| Error::Other(e)) + } + + /// Add an edge between two nodes + /// @param u Source node + /// @param v Target node + /// @param weight Optional edge weight + /// @export + fn add_edge(&mut self, u: String, v: String, weight: Nullable) -> extendr_api::Result<()> { + let w = weight.into_option(); + self.inner.add_edge(u, v, w) + .map_err(|e| Error::Other(e)) + } + + /// Get parents of a node + /// @param node The node name + /// @export + fn get_parents(&self, node: String) -> extendr_api::Result { + let parents = self.inner.get_parents(&node) + .map_err(|e| Error::Other(e))?; + Ok(parents.iter().map(|s| s.as_str()).collect::()) + } + + /// Get children of a node + /// @param node The node name + /// @export + fn get_children(&self, node: String) -> extendr_api::Result { + let children = self.inner.get_children(&node) + .map_err(|e| Error::Other(e))?; + Ok(children.iter().map(|s| s.as_str()).collect::()) + } + + /// Get ancestors of given nodes + /// @param nodes Vector of node names + /// @export + fn get_ancestors_of(&self, nodes: Strings) -> extendr_api::Result { + let node_vec: Vec = nodes.iter().map(|s| s.to_string()).collect(); + let ancestors = self.inner.get_ancestors_of(node_vec) + .map_err(|e| Error::Other(e))?; + Ok(ancestors.iter().map(|s| s.as_str()).collect::()) + } + + /// Get all nodes in the DAG + /// @export + fn nodes(&self) -> Strings { + let nodes = self.inner.nodes(); + nodes.iter().map(|s| s.as_str()).collect::() + } + + /// Get all edges in the DAG + /// @export + fn edges(&self) -> List { + let edges = self.inner.edges(); + let (from, to): (Vec<_>, Vec<_>) = edges.into_iter().unzip(); + list!(from = from, to = to) + } + + /// Get number of nodes + /// @export + fn node_count(&self) -> i32 { + self.inner.node_count() as i32 + } + + /// Get number of edges + /// @export + fn edge_count(&self) -> i32 { + self.inner.edge_count() as i32 + } + + /// Get latent nodes + /// @export + fn latents(&self) -> Strings { + self.inner.latents.iter().map(|s| s.as_str()).collect::() + } +} + +// Expose the module to R +extendr_module! { + mod causalgraphs; + impl RDAG; +} \ No newline at end of file diff --git a/r_bindings/causalgraphs/tools/config.R b/r_bindings/causalgraphs/tools/config.R new file mode 100644 index 0000000..f317f06 --- /dev/null +++ b/r_bindings/causalgraphs/tools/config.R @@ -0,0 +1,104 @@ +# Note: Any variables prefixed with `.` are used for text +# replacement in the Makevars.in and Makevars.win.in + +# check the packages MSRV first +source("tools/msrv.R") + +# check DEBUG and NOT_CRAN environment variables +env_debug <- Sys.getenv("DEBUG") +env_not_cran <- Sys.getenv("NOT_CRAN") + +# check if the vendored zip file exists +vendor_exists <- file.exists("src/rust/vendor.tar.xz") + +is_not_cran <- env_not_cran != "" +is_debug <- env_debug != "" + +if (is_debug) { + # if we have DEBUG then we set not cran to true + # CRAN is always release build + is_not_cran <- TRUE + message("Creating DEBUG build.") +} + +if (!is_not_cran) { + message("Building for CRAN.") +} + +# we set cran flags only if NOT_CRAN is empty and if +# the vendored crates are present. +.cran_flags <- ifelse( + !is_not_cran && vendor_exists, + "-j 2 --offline", + "" +) + +# when DEBUG env var is present we use `--debug` build +.profile <- ifelse(is_debug, "", "--release") +.clean_targets <- ifelse(is_debug, "", "$(TARGET_DIR)") + +# We specify this target when building for webR +webr_target <- "wasm32-unknown-emscripten" + +# here we check if the platform we are building for is webr +is_wasm <- identical(R.version$platform, webr_target) + +# print to terminal to inform we are building for webr +if (is_wasm) { + message("Building for WebR") +} + +# we check if we are making a debug build or not +# if so, the LIBDIR environment variable becomes: +# LIBDIR = $(TARGET_DIR)/{wasm32-unknown-emscripten}/debug +# this will be used to fill out the LIBDIR env var for Makevars.in +target_libpath <- if (is_wasm) "wasm32-unknown-emscripten" else NULL +cfg <- if (is_debug) "debug" else "release" + +# used to replace @LIBDIR@ +.libdir <- paste(c(target_libpath, cfg), collapse = "/") + +# use this to replace @TARGET@ +# we specify the target _only_ on webR +# there may be use cases later where this can be adapted or expanded +.target <- ifelse(is_wasm, paste0("--target=", webr_target), "") + +# read in the Makevars.in file checking +is_windows <- .Platform[["OS.type"]] == "windows" + +# if windows we replace in the Makevars.win.in +mv_fp <- ifelse( + is_windows, + "src/Makevars.win.in", + "src/Makevars.in" +) + +# set the output file +mv_ofp <- ifelse( + is_windows, + "src/Makevars.win", + "src/Makevars" +) + +# delete the existing Makevars{.win} +if (file.exists(mv_ofp)) { + message("Cleaning previous `", mv_ofp, "`.") + invisible(file.remove(mv_ofp)) +} + +# read as a single string +mv_txt <- readLines(mv_fp) + +# replace placeholder values +new_txt <- gsub("@CRAN_FLAGS@", .cran_flags, mv_txt) |> + gsub("@PROFILE@", .profile, x = _) |> + gsub("@CLEAN_TARGET@", .clean_targets, x = _) |> + gsub("@LIBDIR@", .libdir, x = _) |> + gsub("@TARGET@", .target, x = _) + +message("Writing `", mv_ofp, "`.") +con <- file(mv_ofp, open = "wb") +writeLines(new_txt, con, sep = "\n") +close(con) + +message("`tools/config.R` has finished.") diff --git a/r_bindings/causalgraphs/tools/msrv.R b/r_bindings/causalgraphs/tools/msrv.R new file mode 100644 index 0000000..59a61ab --- /dev/null +++ b/r_bindings/causalgraphs/tools/msrv.R @@ -0,0 +1,116 @@ +# read the DESCRIPTION file +desc <- read.dcf("DESCRIPTION") + +if (!"SystemRequirements" %in% colnames(desc)) { + fmt <- c( + "`SystemRequirements` not found in `DESCRIPTION`.", + "Please specify `SystemRequirements: Cargo (Rust's package manager), rustc`" + ) + stop(paste(fmt, collapse = "\n")) +} + +# extract system requirements +sysreqs <- desc[, "SystemRequirements"] + +# check that cargo and rustc is found +if (!grepl("cargo", sysreqs, ignore.case = TRUE)) { + stop("You must specify `Cargo (Rust's package manager)` in your `SystemRequirements`") +} + +if (!grepl("rustc", sysreqs, ignore.case = TRUE)) { + stop("You must specify `Cargo (Rust's package manager), rustc` in your `SystemRequirements`") +} + +# split into parts +parts <- strsplit(sysreqs, ", ")[[1]] + +# identify which is the rustc +rustc_ver <- parts[grepl("rustc", parts)] + +# perform checks for the presence of rustc and cargo on the OS +no_cargo_msg <- c( + "----------------------- [CARGO NOT FOUND]--------------------------", + "The 'cargo' command was not found on the PATH. Please install Cargo", + "from: https://www.rust-lang.org/tools/install", + "", + "Alternatively, you may install Cargo from your OS package manager:", + " - Debian/Ubuntu: apt-get install cargo", + " - Fedora/CentOS: dnf install cargo", + " - macOS: brew install rust", + "-------------------------------------------------------------------" +) + +no_rustc_msg <- c( + "----------------------- [RUST NOT FOUND]---------------------------", + "The 'rustc' compiler was not found on the PATH. Please install", + paste(rustc_ver, "or higher from:"), + "https://www.rust-lang.org/tools/install", + "", + "Alternatively, you may install Rust from your OS package manager:", + " - Debian/Ubuntu: apt-get install rustc", + " - Fedora/CentOS: dnf install rustc", + " - macOS: brew install rust", + "-------------------------------------------------------------------" +) + +# Add {user}/.cargo/bin to path before checking +new_path <- paste0( + Sys.getenv("PATH"), + ":", + paste0(Sys.getenv("HOME"), "/.cargo/bin") +) + +# set the path with the new path +Sys.setenv("PATH" = new_path) + +# check for rustc installation +rustc_version <- tryCatch( + system("rustc --version", intern = TRUE), + error = function(e) { + stop(paste(no_rustc_msg, collapse = "\n")) + } +) + +# check for cargo installation +cargo_version <- tryCatch( + system("cargo --version", intern = TRUE), + error = function(e) { + stop(paste(no_cargo_msg, collapse = "\n")) + } +) + +# helper function to extract versions +extract_semver <- function(ver) { + if (grepl("\\d+\\.\\d+(\\.\\d+)?", ver)) { + sub(".*?(\\d+\\.\\d+(\\.\\d+)?).*", "\\1", ver) + } else { + NA + } +} + +# get the MSRV +msrv <- extract_semver(rustc_ver) + +# extract current version +current_rust_version <- extract_semver(rustc_version) + +# perform check +if (!is.na(msrv)) { + # -1 when current version is later + # 0 when they are the same + # 1 when MSRV is newer than current + is_msrv <- utils::compareVersion(msrv, current_rust_version) + if (is_msrv == 1) { + fmt <- paste0( + "\n------------------ [UNSUPPORTED RUST VERSION]------------------\n", + "- Minimum supported Rust version is %s.\n", + "- Installed Rust version is %s.\n", + "---------------------------------------------------------------" + ) + stop(sprintf(fmt, msrv, current_rust_version)) + } +} + +# print the versions +versions_fmt <- "Using %s\nUsing %s" +message(sprintf(versions_fmt, cargo_version, rustc_version)) diff --git a/r_bindings/src/lib.rs b/r_bindings/src/lib.rs deleted file mode 100644 index 58934c7..0000000 --- a/r_bindings/src/lib.rs +++ /dev/null @@ -1,73 +0,0 @@ -use extendr_api::prelude::*; -use rust_core::RustDAG; -use extendr_api::Nullable; - -pub struct RDAG { - inner: RustDAG, -} - -#[extendr] -impl RDAG { - fn new() -> Self { - RDAG { inner: RustDAG::new() } - } - - fn add_node(&mut self, node: String, latent: Option) -> extendr_api::Result<()> { - self.inner.add_node(node, latent.unwrap_or(false)) - .map_err(Error::from) - } - - fn add_nodes_from(&mut self, nodes: Vec, latent: Nullable>) -> extendr_api::Result<()> { - let latent_opt: Option> = latent.into_option().map(|v| v.into_iter().map(|x| x != 0).collect()); - self.inner.add_nodes_from(nodes, latent_opt) - .map_err(Error::from) - } - - fn add_edge(&mut self, u: String, v: String, weight: Option) -> extendr_api::Result<()> { - self.inner.add_edge(u, v, weight) - .map_err(Error::from) - } - - fn get_parents(&self, node: String) -> extendr_api::Result> { - self.inner.get_parents(&node) - .map_err(Error::from) - } - - fn get_children(&self, node: String) -> extendr_api::Result> { - self.inner.get_children(&node) - .map_err(Error::from) - } - - fn get_ancestors_of(&self, nodes: Vec) -> extendr_api::Result> { - Ok(self.inner.get_ancestors_of(nodes) - .map_err(Error::from)? - .into_iter().collect()) - } - - fn nodes(&self) -> Vec { - self.inner.nodes() - } - - fn edges(&self) -> List { - let edges = self.inner.edges(); - let (from, to): (Vec<_>, Vec<_>) = edges.into_iter().unzip(); - list!(from = from, to = to) - } - - fn node_count(&self) -> usize { - self.inner.node_count() - } - - fn edge_count(&self) -> usize { - self.inner.edge_count() - } - - fn latents(&self) -> Vec { - self.inner.latents.iter().cloned().collect() - } -} - -extendr_module! { - mod causalgraphs; - impl RDAG; -} \ No newline at end of file From 80260283f9d8b42425b3d07b7ee1cf860b9cb02c Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 5 Jul 2025 15:57:46 +0530 Subject: [PATCH 25/62] R build + Ugly Temp Fixes --- r_bindings/causalgraphs/NAMESPACE | 3 ++ r_bindings/causalgraphs/R/extendr-wrappers.R | 50 +++++++++++++++++--- r_bindings/causalgraphs/test.R | 33 +++++++++++++ r_bindings/causalgraphs/tools/config.R | 17 +++++-- 4 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 r_bindings/causalgraphs/test.R diff --git a/r_bindings/causalgraphs/NAMESPACE b/r_bindings/causalgraphs/NAMESPACE index 6ae9268..e16e0fa 100644 --- a/r_bindings/causalgraphs/NAMESPACE +++ b/r_bindings/causalgraphs/NAMESPACE @@ -1,2 +1,5 @@ # Generated by roxygen2: do not edit by hand +S3method("$",RDAG) +S3method("[[",RDAG) +useDynLib(causalgraphs, .registration = TRUE) diff --git a/r_bindings/causalgraphs/R/extendr-wrappers.R b/r_bindings/causalgraphs/R/extendr-wrappers.R index 254c5e3..15a6397 100644 --- a/r_bindings/causalgraphs/R/extendr-wrappers.R +++ b/r_bindings/causalgraphs/R/extendr-wrappers.R @@ -1,12 +1,50 @@ +# Generated by extendr: Do not edit by hand + # nolint start - -#' @docType package + +# +# This file was created with the following call: +# .Call("wrap__make_causalgraphs_wrappers", use_symbols = TRUE, package_name = "causalgraphs") + #' @usage NULL #' @useDynLib causalgraphs, .registration = TRUE NULL - -#' Return string `"Hello world!"` to R. + +# Ugly fix 2: ensure that the environment is set correctly (TODO: need to find a fix for this ASAP) +# did this because `self` was not being set correctly in the R6 class +`_self` <- environment()$`_self` + +RDAG <- new.env(parent = emptyenv()) + +RDAG$new <- function() .Call(wrap__RDAG__new) + +RDAG$add_node <- function(node, latent) .Call(wrap__RDAG__add_node, `_self`, node, latent) + +RDAG$add_nodes_from <- function(nodes, latent) .Call(wrap__RDAG__add_nodes_from, self, nodes, latent) + +RDAG$add_edge <- function(u, v, weight) .Call(wrap__RDAG__add_edge, `_self`, u, v, weight) + +RDAG$get_parents <- function(node) .Call(wrap__RDAG__get_parents, self, node) + +RDAG$get_children <- function(node) .Call(wrap__RDAG__get_children, self, node) + +RDAG$get_ancestors_of <- function(nodes) .Call(wrap__RDAG__get_ancestors_of, self, nodes) + +RDAG$nodes <- function() .Call(wrap__RDAG__nodes, self) + +RDAG$edges <- function() .Call(wrap__RDAG__edges, self) + +RDAG$node_count <- function() .Call(wrap__RDAG__node_count, self) + +RDAG$edge_count <- function() .Call(wrap__RDAG__edge_count, self) + +RDAG$latents <- function() .Call(wrap__RDAG__latents, self) + #' @export -hello_world <- function() .Call(wrap__hello_world) - +`$.RDAG` <- function (self, name) { func <- RDAG[[name]]; environment(func) <- environment(); func } + +#' @export +`[[.RDAG` <- `$.RDAG` + + # nolint end diff --git a/r_bindings/causalgraphs/test.R b/r_bindings/causalgraphs/test.R new file mode 100644 index 0000000..2a6fcd7 --- /dev/null +++ b/r_bindings/causalgraphs/test.R @@ -0,0 +1,33 @@ +library(causalgraphs) + +# Create a new DAG +dag <- RDAG$new() + +# Add nodes +dag$add_node("A", latent = FALSE) +dag$add_node("B", latent = FALSE) +dag$add_node("L", latent = TRUE) # Latent node + +# Add edges +dag$add_edge("A", "B", 10) +dag$add_edge("B", "C", 20) + +# Inspect graph +cat("Nodes:", dag$nodes(), "\n") +cat("Latents:", dag$latents(), "\n") +cat("Node count:", dag$node_count(), "\n") +cat("Edge count:", dag$edge_count(), "\n") + +# Get edges +edges <- dag$edges() +cat("Edges:\n") +print(edges) + +# Get parents +cat("Parents of B:", dag$get_parents("B"), "\n") + +# Get children +cat("Children of A:", dag$get_children("A"), "\n") + +# Get ancestors +cat("Ancestors of C:", dag$get_ancestors_of(c("C")), "\n") \ No newline at end of file diff --git a/r_bindings/causalgraphs/tools/config.R b/r_bindings/causalgraphs/tools/config.R index f317f06..90a6cb6 100644 --- a/r_bindings/causalgraphs/tools/config.R +++ b/r_bindings/causalgraphs/tools/config.R @@ -89,12 +89,19 @@ if (file.exists(mv_ofp)) { # read as a single string mv_txt <- readLines(mv_fp) +# Ugly fix 1: pipe operator is not working for R < 4.2 (TODO: Find a way to fix this, by upgrading to R >= 4.2) +# before: +# new_txt <- gsub("@CRAN_FLAGS@", .cran_flags, mv_txt) |> +# gsub("@PROFILE@", .profile, x = _) |> +# gsub("@CLEAN_TARGET@", .clean_targets, x = _) |> +# gsub("@LIBDIR@", .libdir, x = _) |> +# gsub("@TARGET@", .target, x = _) # replace placeholder values -new_txt <- gsub("@CRAN_FLAGS@", .cran_flags, mv_txt) |> - gsub("@PROFILE@", .profile, x = _) |> - gsub("@CLEAN_TARGET@", .clean_targets, x = _) |> - gsub("@LIBDIR@", .libdir, x = _) |> - gsub("@TARGET@", .target, x = _) +new_txt <- gsub("@CRAN_FLAGS@", .cran_flags, mv_txt) +new_txt <- gsub("@PROFILE@", .profile, new_txt) +new_txt <- gsub("@CLEAN_TARGET@", .clean_targets, new_txt) +new_txt <- gsub("@LIBDIR@", .libdir, new_txt) +new_txt <- gsub("@TARGET@", .target, new_txt) message("Writing `", mv_ofp, "`.") con <- file(mv_ofp, open = "wb") From a1c1018e1310a4bdc6b8ba13b81758dcba16da31 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 5 Jul 2025 19:48:06 +0530 Subject: [PATCH 26/62] Revert dirty fixes by updating R bindings and configuration for compatibility with R 4.2+ --- r_bindings/causalgraphs/.RData | Bin 2595 -> 2748 bytes r_bindings/causalgraphs/.Rhistory | 10 ++++++++++ r_bindings/causalgraphs/DESCRIPTION | 2 +- r_bindings/causalgraphs/R/extendr-wrappers.R | 8 ++------ r_bindings/causalgraphs/src/rust/src/lib.rs | 4 +++- r_bindings/causalgraphs/tools/config.R | 20 +++++++++---------- 6 files changed, 26 insertions(+), 18 deletions(-) diff --git a/r_bindings/causalgraphs/.RData b/r_bindings/causalgraphs/.RData index c9dab282eee4bf542ac6b753684f8ffcad1913eb..3c93a3b66533a42a6129e7e2d85f1ef25219868c 100644 GIT binary patch literal 2748 zcmV;t3PbfDiwFP!0000016@{UARCJJO^MOe96~5OVV#g?V#4KEFX z?pK{FpFGNNLgEya_f6^)&jGG~j}vp{>TJqbfh)xGZvnGJAqb}`XqHOLd76s4IkmdKGWGBR32@fm;Opij zp4WF#VoNU>4bB0tykn+0V7sHl?d?;~@NhMRrD`&Bm>EdUrbu8~7u01nU@H}qgRL9F zbeDrDS^0XAUA5-Q_q(n5i>04>k^TM#Ec{%D~xLia209Us$H?a*pf)}W~zTB+82 zGoU6T6PSb}L!;0y5OA8~%F_Z1?~fRk z`pQxw_FXn1=51KYlD2*~Pg%puH2x`ear(h2-Pch|avRtt=lEY<>~f3~4&{=MJ4q)H z>XfxD*7~t+lSaSQdB6Dw{6EQ6Qu#E&CV8<}il?tN2&+a+%ia-@88BYc&E?jGY(x9a zoqqn&EE-%&wu^ZhnImG1jmz6@OXB*ORl!qToyIqWKfr3=IxL>C+h`2X?hgmw?Xx#= zOj14F9IbPKy$f61Fc#tb<7vZ9nQ%^jv_FVJgh8y(wpON$1Q3s&Rhy>$WYr-|&R#6| z#yes7j>WJ5dgw|=D}ir1VtE}qK3PDWs0}VLQ7=>ISncFs2wM>E#Jo{97yHpZni#1X zZH)P-gyU&3SX)Y0mN2GEav11|p~~32?iuktoc5*3$GYMq0o8 zFQfX+4i2HF9j8Z=1AXPSZUD9qf=Gc+GmB4g8YI@9Er={k35)2g&?L0#Ola9 zNUGofqSjoZXpa*_9CzoX<^&G9UAH}2OlI7pgj%rf*CdW^%*;@6AJ&|<#JHW~UvsCy zN`ik`N}Zhyz>tMAo%xomK_x@e&(uUIxZc-A34D%t_?f=N;XBIow>`;lTH|%}cQ7fK z;^i!hybp&mqJ2ai$qv-!>1ue!57|tKNhdsQN2fh7Fa!>K1WWI=^pLKmk=PUAmC32M z>%?Cut`R2LWO02?5CI*RD+;t*w_zvVm{R$>JOWP1;OipXseCqZcGDA5!;~(g=xO72 zTj}`xScQkEQ4X(D@^DE8W8)(?o)H>&UvF&|ZI2pqfCZ!;G8A3|i5#8T0tm=X2Tpp# zH8oifyIv(g?`)q*{x@04Vxg;<=p_Bj~(Ed?30$8cVQ(_m$i(NkO+>>?Cy`1=+Jn|BBQt6?`qk)l96_6-GW8TIrZe`3ANb)FD2< zP-U6x_jT4e%*rR_>J`&so~Qeoe=2X(AlLvC9R{JnB~bR^Uf&kqjc)4d=Lq_RsKU&# zvf4h;<`~yS4?$Cpi=E&pPd+@}b9b@ZxSKAd?FH|Z&_<69Fb6BoJasxqZRrI&Olr7B z1KT}peiev=jQn{OzONqKT6Q3PP7$tKg~dK6`>4FJF-H!SExVxwh}*Ko-*s*I<+>Qy zZQ#d7M7?#sM4|H%ZO0+RWoUFV(%s$!FSq?Bp+nX(_4W9JpQEay5R>;Lb7IBj#By*@ z!kKZso6q6PN7^E`po?)%&}ecgs5@5>^IBu1{B!%)>|Ziavs)Ko28n^wq{;?C9Iu5c zjhV0v2Wp9jDnHA_K5``O&}8<;Y;C9=suuyi`lR(g)A|b=*@d`fv zgg0zlt`Fs&d@?jz_%+D8NMZLuIVb`s7C_XDi1}u&Qs!hi{OX)^4ca`~PG%{uC`7dR z5K=}W_m1`Tw#f(Xtrf*U1M}Ty7>bm~L8OYhmW40Rhv&k=y{HQ=%?y_XBoWKzMH8w)Pw%OfxmSU>YVN{9K&{v;D{7ucQujB;m-O5G_WHoBXqK|GG+m=yMXFvct3In7_a3R2g8xWtP zIPMS^IGJxF?JJ9#L>h{9w%qDx9^XQ%Sha?RE=(OzvZ9rTIsGlh68Ft7^d3lSKG-xM zf;*@gKiM~>0j^1{>AH63Jq3zyGJ{{D#{ytoj`E}Q&( z{Q2sSK1hK>Id)YXAhGc%6ys~vW3s@L+yvL(Uq^^Ab{(9JjFjJbJUUIcHibOZ2VX63 zN9JlzYH*QpJ(I$FiZMEl=-|?F!F~&PBnij3?AkK+zO2@;dKRi*v}&)JJyJivw%34C&N?95n+3(*U&0OUoHobqS^C%DKpz?zR{ctY=DwC{a$H)(y5m^9b51>pK5IY77RU@o=<1Ua|N}iqr7~+mXdzE zb&cshMaK}Nwyuv8-v}J+{R=^`d+8!r(G|ER8?P-EL$(++dEg{>86(t5?m?RmK`{3?(@tFWnYkKpqsv!* zn0lHSNqmKz`M-+}_LZg(tgMd`)`Ic5JXYGIauDR{yHUC8*o#;7(Dn-*A#Nr zJ;J?`65x62?|j?dp3vN~aH2~#q$tAe#SHKmPrP@2u`*5@@xa-NdNh4WbI2}$5V}V0 z>5wuVN$egBx>Q}|sFDL;Cm;kc(ae(mMc~oYHo}!`{3z*{TC7-dbi7|;A@a!#Auu&% ze55+!&bT9HEm{XmZIFUTTYP=cffNAqQ$laFivVuFx4ak#Cs0gyb0S5h^+>hz!VDL< z_*{qH)Y7H0VgLuj6;u?-;pmuv%ld5W@X%D~?JSk-0`wDIv64d>Bw!Nk?F=$}Lg3${ zabj`68mPb2qD=J@$$YiaeKLK5AaI^(zeF_&)!2od{CfS|jxa*tlDw>XcT~_7)IzdO z9iaLR3cTB65zv}#)nZCk|LBbHf6R1G2K=px+4AAx(1644T=W|cUWl*{k2<1o`%ob# z-fVNr7?fTT=~4=!*5Ic#8q9#ru%L+&TY@|v(&*QBGVHs`j6Bh|N^<8Q#Bj`JUL;h5 zJUH-uXc}9~TF|-RtUQtNB$OTAT!PW~KfrBmaXbWvI*`p!WFoB5#0ZK!sEm$ov>dA~ z?vmaIB!JRfFcd#uJt)y#ZaM+6Owbd?WNZr~g0y*K&=OUJSK!0gGBD)a#&&3S~_fw{_rTU1iTEzmJ~uT_`-l9*kF!~ z6@2fi6wnWnd?LR*f~ZByw~XO>k-QvKR5EoC_dr2HIJAnMi%2<1db$^dHxNxppzK3h zBvgjRPZHwzyP&NhPt1L+i(n;O5W+7&<|y#a<*fNiKRj_xS8`BU>o5;x(j*L$xH$J| zWZK*viI7oPru->a@069-OF}j#VgL=3T0E2p-ca*+I-+|qJ4lOELJX9jJlxP@>QX|W zXn-MKD^#B;x#%ztn))|aM^JP&94x4l1$6El7LtLX3Sj3l(u~+o%V2+2^5WoTfT%lH zaLnlGIs7j>U$Q3UW$%!!X;~-VlKfI|c zs!*G1-Qr$#QD}`oJxrkxnEW}jGV-lL2LRl{Ms#qZI0)CxX}W3h$I$K0dF(5DpOo69 zveFv4eqiWi6-;C-#US5h5`uzd`RK<<$LtqAW<1RuK62XB18_3om|Xpiy3HV3-Zx-< z1i)lf`<@&MNNW)&=k5Jn>RQCkjTp4;I)SOnXfv@@PE^*U>SA&fy!Rxb=RsBzCC2c4 z_XNndLbXzz9iLw5(fC?%U)I^)dSh5if+hE{iNYjMjxeODvtoS<;uI-A4R+q+hQDHq|lU>Z~1m2(<=t2Se)cIw#r}WZ@*N$ zy9p0sv>vUPz07v>*PX|Y;wK{I@K8M>`0hI@rJGjx>-__HHtN-zfr#8PPcgqY-=m*f zcccPFB2ruYd8hr?E%9|w88)5banxa`*JZzPM{jwAsi+fij|Mu*+A|>C2$hYY`0@^< z!9cImuBj4Pw;N$@fb5zSJo>hPl`p|1aXA7rb52PW>71M|BMsK91$Xl~QGku}2j11D z0IL*+qU-JdFq7;jP>!Rd5l(*)_eu30528QDQUc{($-}M?m z=3m`Wz_^IP1(xsSXQKxFi)GLB%7O$ohwuhGp_$gq?~8tMYJ6Z>SgP?cdfzTCXlXda zVt&ijdTfmucD+cfYlq^sKX4};#twDZ8$0fjC>^1dkjf~hi|7Z>AZ-8u0RE=Qv&9Sw F0057%?k4~M diff --git a/r_bindings/causalgraphs/.Rhistory b/r_bindings/causalgraphs/.Rhistory index 634372b..542f780 100644 --- a/r_bindings/causalgraphs/.Rhistory +++ b/r_bindings/causalgraphs/.Rhistory @@ -1,3 +1,13 @@ setwd("/home/razak/git/causalgraphs-fresh/r-bindings") usethis::create_package("causalgraphs") q() +rextendr::document() +rextendr::document() +rextendr::document() +source('test.R') +source('test.R') +source('test.R') +source('test.R') +source('test.R') +source('test.R') +q() diff --git a/r_bindings/causalgraphs/DESCRIPTION b/r_bindings/causalgraphs/DESCRIPTION index d3062b1..6b76765 100644 --- a/r_bindings/causalgraphs/DESCRIPTION +++ b/r_bindings/causalgraphs/DESCRIPTION @@ -12,4 +12,4 @@ RoxygenNote: 7.3.2 Config/rextendr/version: 0.4.1 SystemRequirements: Cargo (Rust's package manager), rustc Depends: - R (>= 4.1) + R (>= 4.2) diff --git a/r_bindings/causalgraphs/R/extendr-wrappers.R b/r_bindings/causalgraphs/R/extendr-wrappers.R index 15a6397..64ec308 100644 --- a/r_bindings/causalgraphs/R/extendr-wrappers.R +++ b/r_bindings/causalgraphs/R/extendr-wrappers.R @@ -10,19 +10,15 @@ #' @useDynLib causalgraphs, .registration = TRUE NULL -# Ugly fix 2: ensure that the environment is set correctly (TODO: need to find a fix for this ASAP) -# did this because `self` was not being set correctly in the R6 class -`_self` <- environment()$`_self` - RDAG <- new.env(parent = emptyenv()) RDAG$new <- function() .Call(wrap__RDAG__new) -RDAG$add_node <- function(node, latent) .Call(wrap__RDAG__add_node, `_self`, node, latent) +RDAG$add_node <- function(node, latent) .Call(wrap__RDAG__add_node, self, node, latent) RDAG$add_nodes_from <- function(nodes, latent) .Call(wrap__RDAG__add_nodes_from, self, nodes, latent) -RDAG$add_edge <- function(u, v, weight) .Call(wrap__RDAG__add_edge, `_self`, u, v, weight) +RDAG$add_edge <- function(u, v, weight) .Call(wrap__RDAG__add_edge, self, u, v, weight) RDAG$get_parents <- function(node) .Call(wrap__RDAG__get_parents, self, node) diff --git a/r_bindings/causalgraphs/src/rust/src/lib.rs b/r_bindings/causalgraphs/src/rust/src/lib.rs index c300baa..a771ccd 100644 --- a/r_bindings/causalgraphs/src/rust/src/lib.rs +++ b/r_bindings/causalgraphs/src/rust/src/lib.rs @@ -111,7 +111,9 @@ impl RDAG { } } -// Expose the module to R +// Macro to generate exports. +// This ensures exported functions are registered with R. +// See corresponding C code in `entrypoint.c` extendr_module! { mod causalgraphs; impl RDAG; diff --git a/r_bindings/causalgraphs/tools/config.R b/r_bindings/causalgraphs/tools/config.R index 90a6cb6..c0434b4 100644 --- a/r_bindings/causalgraphs/tools/config.R +++ b/r_bindings/causalgraphs/tools/config.R @@ -91,17 +91,17 @@ mv_txt <- readLines(mv_fp) # Ugly fix 1: pipe operator is not working for R < 4.2 (TODO: Find a way to fix this, by upgrading to R >= 4.2) # before: -# new_txt <- gsub("@CRAN_FLAGS@", .cran_flags, mv_txt) |> -# gsub("@PROFILE@", .profile, x = _) |> -# gsub("@CLEAN_TARGET@", .clean_targets, x = _) |> -# gsub("@LIBDIR@", .libdir, x = _) |> -# gsub("@TARGET@", .target, x = _) +new_txt <- gsub("@CRAN_FLAGS@", .cran_flags, mv_txt) |> + gsub("@PROFILE@", .profile, x = _) |> + gsub("@CLEAN_TARGET@", .clean_targets, x = _) |> + gsub("@LIBDIR@", .libdir, x = _) |> + gsub("@TARGET@", .target, x = _) # replace placeholder values -new_txt <- gsub("@CRAN_FLAGS@", .cran_flags, mv_txt) -new_txt <- gsub("@PROFILE@", .profile, new_txt) -new_txt <- gsub("@CLEAN_TARGET@", .clean_targets, new_txt) -new_txt <- gsub("@LIBDIR@", .libdir, new_txt) -new_txt <- gsub("@TARGET@", .target, new_txt) +# new_txt <- gsub("@CRAN_FLAGS@", .cran_flags, mv_txt) +# new_txt <- gsub("@PROFILE@", .profile, new_txt) +# new_txt <- gsub("@CLEAN_TARGET@", .clean_targets, new_txt) +# new_txt <- gsub("@LIBDIR@", .libdir, new_txt) +# new_txt <- gsub("@TARGET@", .target, new_txt) message("Writing `", mv_ofp, "`.") con <- file(mv_ofp, open = "wb") From 2ac6b01bcfda09544c12fbd18228f2eb5a8b087f Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 5 Jul 2025 19:48:32 +0530 Subject: [PATCH 27/62] revert config.R temp change --- r_bindings/causalgraphs/tools/config.R | 8 -------- 1 file changed, 8 deletions(-) diff --git a/r_bindings/causalgraphs/tools/config.R b/r_bindings/causalgraphs/tools/config.R index c0434b4..a12ea88 100644 --- a/r_bindings/causalgraphs/tools/config.R +++ b/r_bindings/causalgraphs/tools/config.R @@ -89,19 +89,11 @@ if (file.exists(mv_ofp)) { # read as a single string mv_txt <- readLines(mv_fp) -# Ugly fix 1: pipe operator is not working for R < 4.2 (TODO: Find a way to fix this, by upgrading to R >= 4.2) -# before: new_txt <- gsub("@CRAN_FLAGS@", .cran_flags, mv_txt) |> gsub("@PROFILE@", .profile, x = _) |> gsub("@CLEAN_TARGET@", .clean_targets, x = _) |> gsub("@LIBDIR@", .libdir, x = _) |> gsub("@TARGET@", .target, x = _) -# replace placeholder values -# new_txt <- gsub("@CRAN_FLAGS@", .cran_flags, mv_txt) -# new_txt <- gsub("@PROFILE@", .profile, new_txt) -# new_txt <- gsub("@CLEAN_TARGET@", .clean_targets, new_txt) -# new_txt <- gsub("@LIBDIR@", .libdir, new_txt) -# new_txt <- gsub("@TARGET@", .target, new_txt) message("Writing `", mv_ofp, "`.") con <- file(mv_ofp, open = "wb") From a4cc3614086c2fe41842076bc63ccc8fcad45454 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 6 Jul 2025 14:37:37 +0530 Subject: [PATCH 28/62] add readme --- r_bindings/causalgraphs/.Rbuildignore | 1 + r_bindings/causalgraphs/README.Rmd | 115 ++++++++++++++++++++ r_bindings/causalgraphs/src/rust/src/lib.rs | 1 - 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 r_bindings/causalgraphs/README.Rmd diff --git a/r_bindings/causalgraphs/.Rbuildignore b/r_bindings/causalgraphs/.Rbuildignore index e53acac..dadc236 100644 --- a/r_bindings/causalgraphs/.Rbuildignore +++ b/r_bindings/causalgraphs/.Rbuildignore @@ -3,3 +3,4 @@ ^src/rust/target$ ^src/Makevars$ ^src/Makevars\.win$ +^README\.Rmd$ diff --git a/r_bindings/causalgraphs/README.Rmd b/r_bindings/causalgraphs/README.Rmd new file mode 100644 index 0000000..ad47134 --- /dev/null +++ b/r_bindings/causalgraphs/README.Rmd @@ -0,0 +1,115 @@ +--- +output: github_document +--- + + + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + fig.path = "man/figures/README-", + out.width = "100%" +) +``` + +# causalgraphs + + + + +**causalgraphs** provides fast, flexible, and cross-language causal graph (DAG) manipulation in R, powered by Rust via [extendr](https://extendr.github.io/). +It is ideal for scientific computing, causal inference, and graph-based modeling. + +## Features + +- Add/remove nodes and edges in directed acyclic graphs (DAGs) +- Mark nodes as latent (unobserved) +- Query parents, children, ancestors, and other graph properties +- Efficient Rust backend for high performance +- Seamless R interface + +## Installation + +You need a working Rust toolchain (`cargo`, `rustc`) and R (>= 4.2). + +Install the development version from your local checkout: + +```r +# Make sure you're in the right directory +setwd("causalgraphs/r-bindings/causalgraphs") + +# Generate documentation and build the package +rextendr::document() + +# Load the package for testing +devtools::load_all() +``` + +Or, You can install the development version of `causalgraphs` from GitHub using `pak` or `devtools`. +Remember to replace `your-github-username` with the actual GitHub user or organization name.: + +```r + +# Recommended: using pak +# install.packages("pak") +pak::pkg_install("your-github-username/causalgraphs/r_bindings/causalgraphs") + +# Or using devtools: +# install.packages("devtools") +devtools::install_github("your-github-username/causalgraphs", subdir = "r_bindings/causalgraphs") +``` + +## Example + +```{r example, eval=FALSE} +library(causalgraphs) + +# Create a new DAG +dag <- RDAG$new() + +# Add nodes +dag$add_node("A", latent = FALSE) +dag$add_node("B", latent = FALSE) +dag$add_node("L", latent = TRUE) # Latent node + +# Add edges +dag$add_edge("A", "B", 10) +dag$add_edge("B", "C", 20) + +# Inspect graph +cat("Nodes:", dag$nodes(), "\n") +cat("Latents:", dag$latents(), "\n") +cat("Node count:", dag$node_count(), "\n") +cat("Edge count:", dag$edge_count(), "\n") + +# Get edges +edges <- dag$edges() +cat("Edges:\n") +print(edges) + +# Query relationships +cat("Parents of B:", dag$get_parents("B"), "\n") +cat("Children of A:", dag$get_children("A"), "\n") +cat("Ancestors of C:", dag$get_ancestors_of(c("C")), "\n") +``` + +## Development + +- Rust source: `src/rust/` +- R wrappers: `R/` +- Test and experiment: `test.R` + +To update Rust wrappers after editing Rust code, run: + +```r +rextendr::document() +``` + +## License + +MIT + +--- + +*This README was generated from `README.Rmd`. Please edit that file and knit to update.* \ No newline at end of file diff --git a/r_bindings/causalgraphs/src/rust/src/lib.rs b/r_bindings/causalgraphs/src/rust/src/lib.rs index a771ccd..f55fcbf 100644 --- a/r_bindings/causalgraphs/src/rust/src/lib.rs +++ b/r_bindings/causalgraphs/src/rust/src/lib.rs @@ -1,6 +1,5 @@ use extendr_api::prelude::*; use rust_core::RustDAG; -use std::collections::HashSet; #[extendr] #[derive(Debug, Clone)] From b9454ca5dad8f05b6e1b1c148a445df17fce2d97 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 6 Jul 2025 18:05:18 +0530 Subject: [PATCH 29/62] update README.md --- r_bindings/causalgraphs/README.Rmd | 60 +++++++++++++++--------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/r_bindings/causalgraphs/README.Rmd b/r_bindings/causalgraphs/README.Rmd index ad47134..58edfa4 100644 --- a/r_bindings/causalgraphs/README.Rmd +++ b/r_bindings/causalgraphs/README.Rmd @@ -31,33 +31,43 @@ It is ideal for scientific computing, causal inference, and graph-based modeling ## Installation -You need a working Rust toolchain (`cargo`, `rustc`) and R (>= 4.2). - -Install the development version from your local checkout: +First, ensure you have a working Rust toolchain. You can install it by following the instructions at rustup.rs. +You can install the development version of `causalgraphs` from GitHub. We recommend using the `pak` package for a smooth installation of all dependencies. ```r -# Make sure you're in the right directory -setwd("causalgraphs/r-bindings/causalgraphs") - -# Generate documentation and build the package -rextendr::document() +# install.packages("pak") +pak::pkg_install("pgmpy/causalgraphs/r_bindings/causalgraphs") -# Load the package for testing -devtools::load_all() +# Or using devtools: +# install.packages("devtools") +devtools::install_github("pgmpy/causalgraphs", subdir = "r_bindings/causalgraphs") ``` -Or, You can install the development version of `causalgraphs` from GitHub using `pak` or `devtools`. -Remember to replace `your-github-username` with the actual GitHub user or organization name.: +## Development -```r +- Rust source: `src/rust/` +- R wrappers: `R/` +- Test and experiment: `test.R` -# Recommended: using pak -# install.packages("pak") -pak::pkg_install("your-github-username/causalgraphs/r_bindings/causalgraphs") +If you want to contribute to the package, you'll need to build it from a local clone of the repository. -# Or using devtools: -# install.packages("devtools") -devtools::install_github("your-github-username/causalgraphs", subdir = "r_bindings/causalgraphs") +**Clone the repository:** +```sh + git clone https://github.com/pgmpy/causalgraphs.git + cd causalgraphs/r_bindings/causalgraphs +``` + +**Build and Test in R:** +From an R session inside the `r_bindings/causalgraphs` directory: +```r +# Sync R ↔ Rust bindings: +# 1. Compiles the Rust crate into a shared library (causalgraphs.so/.dll/.dylib) +# 2. Generates/updates R wrapper functions in R/extendr-wrappers.R +# Run this whenever you change the Rust code or add new #[extendr] functions. +rextendr::document() + +# Load the package into R +devtools::load_all() ``` ## Example @@ -94,18 +104,6 @@ cat("Children of A:", dag$get_children("A"), "\n") cat("Ancestors of C:", dag$get_ancestors_of(c("C")), "\n") ``` -## Development - -- Rust source: `src/rust/` -- R wrappers: `R/` -- Test and experiment: `test.R` - -To update Rust wrappers after editing Rust code, run: - -```r -rextendr::document() -``` - ## License MIT From 0fcecbb0dfca990ac211872fe0615661a6cb7831 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Fri, 11 Jul 2025 00:52:39 +0530 Subject: [PATCH 30/62] R remote installable by updating rust_core dependency --- Cargo.lock | 17 ++++++++++++++--- r_bindings/causalgraphs/README.Rmd | 4 ++++ r_bindings/causalgraphs/src/rust/Cargo.toml | 7 ++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c34e27c..f0f6e0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,7 +50,7 @@ name = "causalgraphs" version = "0.1.0" dependencies = [ "pyo3", - "rust_core", + "rust_core 0.1.0", ] [[package]] @@ -61,7 +61,7 @@ dependencies = [ "getrandom 0.2.16", "getrandom 0.3.3", "js-sys", - "rust_core", + "rust_core 0.1.0", "serde", "serde-wasm-bindgen", "uuid", @@ -554,7 +554,7 @@ name = "rcausalgraphs" version = "0.1.0" dependencies = [ "extendr-api", - "rust_core", + "rust_core 0.1.0 (git+https://github.com/pgmpy/causalgraphs.git?branch=main)", ] [[package]] @@ -576,6 +576,17 @@ dependencies = [ "rustworkx-core", ] +[[package]] +name = "rust_core" +version = "0.1.0" +source = "git+https://github.com/pgmpy/causalgraphs.git?branch=main#e8625b5a019ba8a6f67e53a9d1df7c9e925bac84" +dependencies = [ + "ahash", + "indexmap 2.10.0", + "petgraph", + "rustworkx-core", +] + [[package]] name = "rustversion" version = "1.0.21" diff --git a/r_bindings/causalgraphs/README.Rmd b/r_bindings/causalgraphs/README.Rmd index 58edfa4..f2cedd9 100644 --- a/r_bindings/causalgraphs/README.Rmd +++ b/r_bindings/causalgraphs/README.Rmd @@ -70,6 +70,10 @@ rextendr::document() devtools::load_all() ``` +## Developer Notes + +Whenever you're working on both `rust_core` and `r_causalgraphs` locally, make sure you comment the github `rust_core` dependency in `Cargo.toml` and uncomment the local path dependency if you are developing locally. + ## Example ```{r example, eval=FALSE} diff --git a/r_bindings/causalgraphs/src/rust/Cargo.toml b/r_bindings/causalgraphs/src/rust/Cargo.toml index 9ad35bf..14ec67e 100644 --- a/r_bindings/causalgraphs/src/rust/Cargo.toml +++ b/r_bindings/causalgraphs/src/rust/Cargo.toml @@ -10,5 +10,10 @@ crate-type = [ 'staticlib' ] name = 'rcausalgraphs' [dependencies] -rust_core = { path = "../../../../rust_core" } + +rust_core = { git = "https://github.com/pgmpy/causalgraphs.git", branch = "main", package = "rust_core" } + +# For local development, comment out the Git line above and uncomment this: +# rust_core = { path = "../../../../rust_core" } + extendr-api = '*' From 0a824299972056be27c778ede3027d6aac4998a9 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 12 Jul 2025 18:08:41 +0530 Subject: [PATCH 31/62] Add MakeFile & test setup --- Makefile | 54 + python_bindings/requirements.txt | 1 + python_bindings/tests/test_basic.py | 20 + r_bindings/causalgraphs/.Rhistory | 2 + r_bindings/causalgraphs/DESCRIPTION | 3 + r_bindings/causalgraphs/tests/testthat.R | 12 + r_bindings/causalgraphs/tests/testthat/test.R | 14 + rust_core/tests/dag.rs | 12 + wasm_bindings/.gitignore | 1 + wasm_bindings/jest.config.cjs | 4 + wasm_bindings/js/tests/test-wasm.js | 99 +- wasm_bindings/package-lock.json | 4079 +++++++++++++++++ wasm_bindings/package.json | 9 +- 13 files changed, 4222 insertions(+), 88 deletions(-) create mode 100644 Makefile create mode 100644 python_bindings/tests/test_basic.py create mode 100644 r_bindings/causalgraphs/tests/testthat.R create mode 100644 r_bindings/causalgraphs/tests/testthat/test.R create mode 100644 rust_core/tests/dag.rs create mode 100644 wasm_bindings/jest.config.cjs create mode 100644 wasm_bindings/package-lock.json diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7b0a903 --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +# Top-level Makefile for CausalGraphs project +PROJECT := causalgraphs +RUST_CORE := rust_core +PY_BINDINGS := python_bindings +WASM_BINDINGS := wasm_bindings +R_BINDINGS := r_bindings/causalgraphs + +# Build targets +.PHONY: all core python wasm r install test clean + +all: core python wasm r + +core: + @echo "\n=== Building Rust Core ===" + cd $(RUST_CORE) && cargo build --release + +python: core + @echo "\n=== Building Python Bindings ===" + cd $(PY_BINDINGS) && maturin develop --release + +wasm: core + @echo "\n=== Building WebAssembly Bindings ===" + cd $(WASM_BINDINGS) && \ + npm install && \ + npm run build && \ + npm run build:node + +r: core + @echo "\n=== Building R Bindings ===" + cd $(R_BINDINGS) && \ + Rscript -e "rextendr::document()" + +install: python wasm r + +test: test-core test-python test-wasm test-r + +test-core: + cd $(RUST_CORE) && cargo test + +test-python: + cd $(PY_BINDINGS) && pytest tests/ + +test-wasm: + cd $(WASM_BINDINGS) && npm test + +test-r: + cd $(R_BINDINGS) && Rscript -e 'devtools::test()' + +clean: + @echo "\n=== Cleaning All Build Artifacts ===" + cd $(RUST_CORE) && cargo clean + cd $(PY_BINDINGS) && rm -rf target/ *.so + cd $(WASM_BINDINGS) && rm -rf js/pkg-* node_modules + cd $(R_BINDINGS) && rm -rf src/rust/target src/.cargo \ No newline at end of file diff --git a/python_bindings/requirements.txt b/python_bindings/requirements.txt index 120eaa2..dcdf4b8 100644 --- a/python_bindings/requirements.txt +++ b/python_bindings/requirements.txt @@ -1,2 +1,3 @@ maturin==1.9.0 tomli==2.2.1 +pytest diff --git a/python_bindings/tests/test_basic.py b/python_bindings/tests/test_basic.py new file mode 100644 index 0000000..37d2bd0 --- /dev/null +++ b/python_bindings/tests/test_basic.py @@ -0,0 +1,20 @@ +import pytest +from causalgraphs import RustDAG + +@pytest.fixture +def dag(): + d = RustDAG() + d.add_node("X") + d.add_node("Y") + d.add_edge("X", "Y") + return d + +def test_nodes_and_edges(dag): + assert set(dag.nodes()) == {"X", "Y"} + assert dag.node_count() == 2 + assert dag.edge_count() == 1 + assert dag.edges() == [("X", "Y")] + +def test_parents_children(dag): + assert dag.get_parents("Y") == ["X"] + assert dag.get_children("X") == ["Y"] diff --git a/r_bindings/causalgraphs/.Rhistory b/r_bindings/causalgraphs/.Rhistory index 542f780..f6ab8b8 100644 --- a/r_bindings/causalgraphs/.Rhistory +++ b/r_bindings/causalgraphs/.Rhistory @@ -11,3 +11,5 @@ source('test.R') source('test.R') source('test.R') q() +usethis::use_testthat() +q() diff --git a/r_bindings/causalgraphs/DESCRIPTION b/r_bindings/causalgraphs/DESCRIPTION index 6b76765..7e1ad49 100644 --- a/r_bindings/causalgraphs/DESCRIPTION +++ b/r_bindings/causalgraphs/DESCRIPTION @@ -13,3 +13,6 @@ Config/rextendr/version: 0.4.1 SystemRequirements: Cargo (Rust's package manager), rustc Depends: R (>= 4.2) +Suggests: + testthat (>= 3.0.0) +Config/testthat/edition: 3 diff --git a/r_bindings/causalgraphs/tests/testthat.R b/r_bindings/causalgraphs/tests/testthat.R new file mode 100644 index 0000000..5495355 --- /dev/null +++ b/r_bindings/causalgraphs/tests/testthat.R @@ -0,0 +1,12 @@ +# This file is part of the standard setup for testthat. +# It is recommended that you do not modify it. +# +# Where should you do additional test configuration? +# Learn more about the roles of various files in: +# * https://r-pkgs.org/testing-design.html#sec-tests-files-overview +# * https://testthat.r-lib.org/articles/special-files.html + +library(testthat) +library(causalgraphs) + +test_check("causalgraphs") diff --git a/r_bindings/causalgraphs/tests/testthat/test.R b/r_bindings/causalgraphs/tests/testthat/test.R new file mode 100644 index 0000000..749c454 --- /dev/null +++ b/r_bindings/causalgraphs/tests/testthat/test.R @@ -0,0 +1,14 @@ +library(causalgraphs) +library(testthat) + +test_that("basic DAG operations", { + dag <- RDAG$new() + dag$add_node("A", FALSE) + dag$add_node("B", FALSE) + dag$add_edge("A", "B", 20) + expect_setequal(dag$nodes(), c("A", "B")) + expect_equal(dag$node_count(), 2) + expect_equal(dag$edge_count(), 1) + expect_equal(dag$get_parents("B"), "A") + expect_equal(dag$get_children("A"), "B") +}) \ No newline at end of file diff --git a/rust_core/tests/dag.rs b/rust_core/tests/dag.rs new file mode 100644 index 0000000..530c177 --- /dev/null +++ b/rust_core/tests/dag.rs @@ -0,0 +1,12 @@ +use rust_core::RustDAG; + +#[test] +fn test_add_nodes_and_edges() { + let mut dag = RustDAG::new(); + dag.add_node("A".to_string(), false).unwrap(); + dag.add_node("B".to_string(), false).unwrap(); + dag.add_edge("A".to_string(), "B".to_string(), None).unwrap(); + assert_eq!(dag.node_count(), 2); + assert_eq!(dag.edge_count(), 1); + assert!(dag.get_parents("B").unwrap().contains(&"A".to_string())); +} \ No newline at end of file diff --git a/wasm_bindings/.gitignore b/wasm_bindings/.gitignore index 4e30131..8c7b93a 100644 --- a/wasm_bindings/.gitignore +++ b/wasm_bindings/.gitignore @@ -4,3 +4,4 @@ Cargo.lock bin/ pkg/ wasm-pack.log +node_modules/ diff --git a/wasm_bindings/jest.config.cjs b/wasm_bindings/jest.config.cjs new file mode 100644 index 0000000..0b769e6 --- /dev/null +++ b/wasm_bindings/jest.config.cjs @@ -0,0 +1,4 @@ +module.exports = { + testEnvironment: "node", + testMatch: ["**/js/tests/**/*.js"] +}; diff --git a/wasm_bindings/js/tests/test-wasm.js b/wasm_bindings/js/tests/test-wasm.js index f985d74..10a140e 100644 --- a/wasm_bindings/js/tests/test-wasm.js +++ b/wasm_bindings/js/tests/test-wasm.js @@ -1,85 +1,14 @@ -/** - * Node.js test for CausalGraphs WASM - */ - -import * as causalgraphs from '../pkg-node/causalgraphs_wasm.js'; - -async function testWasm() { - console.log('🚀 Testing CausalGraphs WASM...\n'); - - try { - // No need to call init for node target! - // Create a new DAG - const dag = new causalgraphs.RustDAG(); - console.log('✅ DAG created successfully!\n'); - - // Add some nodes - dag.addNode('A'); - dag.addNode('B'); - dag.addNode('C'); - console.log('Added nodes: A, B, C'); - - // Add edges - dag.addEdge('A', 'B'); - dag.addEdge('B', 'C'); - console.log('Added edges: A→B, B→C\n'); - - // Get graph information - const nodes = dag.nodes(); - const edges = dag.edges(); - const nodeCount = dag.nodeCount; - const edgeCount = dag.edgeCount; - - console.log(`Nodes: ${nodes.join(', ')}`); - console.log(`Edges: ${JSON.stringify(edges)}`); - console.log(`Node count: ${nodeCount}`); - console.log(`Edge count: ${edgeCount}\n`); - - // Test graph traversal - const parentsOfC = dag.getParents('C'); - const childrenOfA = dag.getChildren('A'); - const ancestorsOfC = dag.getAncestorsOf(['C']); - - console.log(`Parents of C: ${parentsOfC.join(', ')}`); - console.log(`Children of A: ${childrenOfA.join(', ')}`); - console.log(`Ancestors of C: ${ancestorsOfC.join(', ')}\n`); - - // Test with latent variables - dag.addNode('L', true); // Add latent node - dag.addEdge('L', 'A'); - console.log('Added latent node L → A'); - - // Test more complex graph - const dag2 = new causalgraphs.RustDAG(); - const nodeNames = ['X', 'Y', 'Z', 'W', 'V']; - dag2.addNodesFrom(nodeNames); - - dag2.addEdge('X', 'Y'); - dag2.addEdge('Y', 'Z'); - dag2.addEdge('X', 'W'); - dag2.addEdge('W', 'Z'); - dag2.addEdge('V', 'X'); - - console.log('\nCreated complex graph:'); - console.log('V → X → Y → Z'); - console.log(' ↓ ↑'); - console.log(' W → Z\n'); - - const ancestorsOfZ = dag2.getAncestorsOf(['Z']); - const parentsOfZ = dag2.getParents('Z'); - const childrenOfX = dag2.getChildren('X'); - - console.log(`Ancestors of Z: ${ancestorsOfZ.join(', ')}`); - console.log(`Parents of Z: ${parentsOfZ.join(', ')}`); - console.log(`Children of X: ${childrenOfX.join(', ')}\n`); - - console.log('🎉 All tests passed!'); - - } catch (error) { - console.error('❌ Error:', error); - process.exit(1); - } -} - -// Run the test -testWasm(); \ No newline at end of file +const cg = require("../pkg-node/causalgraphs_wasm.js"); + +describe("RustDAG wasm (CJS)", () => { + it("should add nodes & edges", () => { + const dag = new cg.RustDAG(); + dag.addNode("U"); + dag.addNode("V"); + dag.addEdge("U","V"); + expect(dag.nodes()).toEqual(["U","V"]); + expect(dag.nodeCount).toBe(2); + expect(dag.edges()).toEqual([["U","V"]]); + expect(dag.edgeCount).toBe(1); + }); +}); diff --git a/wasm_bindings/package-lock.json b/wasm_bindings/package-lock.json new file mode 100644 index 0000000..9efd50d --- /dev/null +++ b/wasm_bindings/package-lock.json @@ -0,0 +1,4079 @@ +{ + "name": "causalgraphs-js", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "causalgraphs-js", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "jest": "^30.0.4" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", + "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@emnapi/core": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.4.tgz", + "integrity": "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g==", + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz", + "integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.3.tgz", + "integrity": "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.4.tgz", + "integrity": "sha512-tMLCDvBJBwPqMm4OAiuKm2uF5y5Qe26KgcMn+nrDSWpEW+eeFmqA0iO4zJfL16GP7gE3bUUQ3hIuUJ22AqVRnw==", + "dev": true, + "dependencies": { + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.0.2", + "jest-util": "30.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.4.tgz", + "integrity": "sha512-MWScSO9GuU5/HoWjpXAOBs6F/iobvK1XlioelgOM9St7S0Z5WTI9kjCQLPeo4eQRRYusyLW25/J7J5lbFkrYXw==", + "dev": true, + "dependencies": { + "@jest/console": "30.0.4", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.0.4", + "@jest/test-result": "30.0.4", + "@jest/transform": "30.0.4", + "@jest/types": "30.0.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.0.2", + "jest-config": "30.0.4", + "jest-haste-map": "30.0.2", + "jest-message-util": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.2", + "jest-resolve-dependencies": "30.0.4", + "jest-runner": "30.0.4", + "jest-runtime": "30.0.4", + "jest-snapshot": "30.0.4", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "jest-watcher": "30.0.4", + "micromatch": "^4.0.8", + "pretty-format": "30.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.4.tgz", + "integrity": "sha512-5NT+sr7ZOb8wW7C4r7wOKnRQ8zmRWQT2gW4j73IXAKp5/PX1Z8MCStBLQDYfIG3n1Sw0NRfYGdp0iIPVooBAFQ==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "30.0.4", + "@jest/types": "30.0.1", + "@types/node": "*", + "jest-mock": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.4.tgz", + "integrity": "sha512-Z/DL7t67LBHSX4UzDyeYKqOxE/n7lbrrgEwWM3dGiH5Dgn35nk+YtgzKudmfIrBI8DRRrKYY5BCo3317HZV1Fw==", + "dev": true, + "dependencies": { + "expect": "30.0.4", + "jest-snapshot": "30.0.4" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.4.tgz", + "integrity": "sha512-EgXecHDNfANeqOkcak0DxsoVI4qkDUsR7n/Lr2vtmTBjwLPBnnPOF71S11Q8IObWzxm2QgQoY6f9hzrRD3gHRA==", + "dev": true, + "dependencies": { + "@jest/get-type": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.4.tgz", + "integrity": "sha512-qZ7nxOcL5+gwBO6LErvwVy5k06VsX/deqo2XnVUSTV0TNC9lrg8FC3dARbi+5lmrr5VyX5drragK+xLcOjvjYw==", + "dev": true, + "dependencies": { + "@jest/types": "30.0.1", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.0.2", + "jest-mock": "30.0.2", + "jest-util": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", + "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", + "dev": true, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.4.tgz", + "integrity": "sha512-avyZuxEHF2EUhFF6NEWVdxkRRV6iXXcIES66DLhuLlU7lXhtFG/ySq/a8SRZmEJSsLkNAFX6z6mm8KWyXe9OEA==", + "dev": true, + "dependencies": { + "@jest/environment": "30.0.4", + "@jest/expect": "30.0.4", + "@jest/types": "30.0.1", + "jest-mock": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.4.tgz", + "integrity": "sha512-6ycNmP0JSJEEys1FbIzHtjl9BP0tOZ/KN6iMeAKrdvGmUsa1qfRdlQRUDKJ4P84hJ3xHw1yTqJt4fvPNHhyE+g==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.0.4", + "@jest/test-result": "30.0.4", + "@jest/transform": "30.0.4", + "@jest/types": "30.0.1", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.0.2", + "jest-util": "30.0.2", + "jest-worker": "30.0.2", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz", + "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.4.tgz", + "integrity": "sha512-BEpX8M/Y5lG7MI3fmiO+xCnacOrVsnbqVrcDZIT8aSGkKV1w2WwvRQxSWw5SIS8ozg7+h8tSj5EO1Riqqxcdag==", + "dev": true, + "dependencies": { + "@jest/types": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.4.tgz", + "integrity": "sha512-Mfpv8kjyKTHqsuu9YugB6z1gcdB3TSSOaKlehtVaiNlClMkEHY+5ZqCY2CrEE3ntpBMlstX/ShDAf84HKWsyIw==", + "dev": true, + "dependencies": { + "@jest/console": "30.0.4", + "@jest/types": "30.0.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.4.tgz", + "integrity": "sha512-bj6ePmqi4uxAE8EHE0Slmk5uBYd9Vd/PcVt06CsBxzH4bbA8nGsI1YbXl/NH+eii4XRtyrRx+Cikub0x8H4vDg==", + "dev": true, + "dependencies": { + "@jest/test-result": "30.0.4", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.4.tgz", + "integrity": "sha512-atvy4hRph/UxdCIBp+UB2jhEA/jJiUeGZ7QPgBi9jUUKNgi3WEoMXGNG7zbbELG2+88PMabUNCDchmqgJy3ELg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.0.1", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.0", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.2", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.1.tgz", + "integrity": "sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==", + "dev": true, + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.37", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz", + "integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "24.0.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", + "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==", + "dev": true, + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.4.tgz", + "integrity": "sha512-UjG2j7sAOqsp2Xua1mS/e+ekddkSu3wpf4nZUSvXNHuVWdaOUXQ77+uyjJLDE9i0atm5x4kds8K9yb5lRsRtcA==", + "dev": true, + "dependencies": { + "@jest/transform": "30.0.4", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.0", + "babel-preset-jest": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", + "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", + "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", + "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.182", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.182.tgz", + "integrity": "sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.4.tgz", + "integrity": "sha512-dDLGjnP2cKbEppxVICxI/Uf4YemmGMPNy0QytCbfafbpYk9AFQsxb8Uyrxii0RPK7FWgLGlSem+07WirwS3cFQ==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "30.0.4", + "@jest/get-type": "30.0.1", + "jest-matcher-utils": "30.0.4", + "jest-message-util": "30.0.2", + "jest-mock": "30.0.2", + "jest-util": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.4.tgz", + "integrity": "sha512-9QE0RS4WwTj/TtTC4h/eFVmFAhGNVerSB9XpJh8sqaXlP73ILcPcZ7JWjjEtJJe2m8QyBLKKfPQuK+3F+Xij/g==", + "dev": true, + "dependencies": { + "@jest/core": "30.0.4", + "@jest/types": "30.0.1", + "import-local": "^3.2.0", + "jest-cli": "30.0.4" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.2.tgz", + "integrity": "sha512-Ius/iRST9FKfJI+I+kpiDh8JuUlAISnRszF9ixZDIqJF17FckH5sOzKC8a0wd0+D+8em5ADRHA5V5MnfeDk2WA==", + "dev": true, + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.0.2", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.4.tgz", + "integrity": "sha512-o6UNVfbXbmzjYgmVPtSQrr5xFZCtkDZGdTlptYvGFSN80RuOOlTe73djvMrs+QAuSERZWcHBNIOMH+OEqvjWuw==", + "dev": true, + "dependencies": { + "@jest/environment": "30.0.4", + "@jest/expect": "30.0.4", + "@jest/test-result": "30.0.4", + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.0.2", + "jest-matcher-utils": "30.0.4", + "jest-message-util": "30.0.2", + "jest-runtime": "30.0.4", + "jest-snapshot": "30.0.4", + "jest-util": "30.0.2", + "p-limit": "^3.1.0", + "pretty-format": "30.0.2", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.4.tgz", + "integrity": "sha512-3dOrP3zqCWBkjoVG1zjYJpD9143N9GUCbwaF2pFF5brnIgRLHmKcCIw+83BvF1LxggfMWBA0gxkn6RuQVuRhIQ==", + "dev": true, + "dependencies": { + "@jest/core": "30.0.4", + "@jest/test-result": "30.0.4", + "@jest/types": "30.0.1", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.0.4", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.4.tgz", + "integrity": "sha512-3dzbO6sh34thAGEjJIW0fgT0GA0EVlkski6ZzMcbW6dzhenylXAE/Mj2MI4HonroWbkKc6wU6bLVQ8dvBSZ9lA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.0.1", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.0.4", + "@jest/types": "30.0.1", + "babel-jest": "30.0.4", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.0.4", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.0.4", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.2", + "jest-runner": "30.0.4", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.0.2", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.4.tgz", + "integrity": "sha512-TSjceIf6797jyd+R64NXqicttROD+Qf98fex7CowmlSn7f8+En0da1Dglwr1AXxDtVizoxXYZBlUQwNhoOXkNw==", + "dev": true, + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "pretty-format": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", + "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", + "dev": true, + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.2.tgz", + "integrity": "sha512-ZFRsTpe5FUWFQ9cWTMguCaiA6kkW5whccPy9JjD1ezxh+mJeqmz8naL8Fl/oSbNJv3rgB0x87WBIkA5CObIUZQ==", + "dev": true, + "dependencies": { + "@jest/get-type": "30.0.1", + "@jest/types": "30.0.1", + "chalk": "^4.1.2", + "jest-util": "30.0.2", + "pretty-format": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.4.tgz", + "integrity": "sha512-p+rLEzC2eThXqiNh9GHHTC0OW5Ca4ZfcURp7scPjYBcmgpR9HG6750716GuUipYf2AcThU3k20B31USuiaaIEg==", + "dev": true, + "dependencies": { + "@jest/environment": "30.0.4", + "@jest/fake-timers": "30.0.4", + "@jest/types": "30.0.1", + "@types/node": "*", + "jest-mock": "30.0.2", + "jest-util": "30.0.2", + "jest-validate": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.2.tgz", + "integrity": "sha512-telJBKpNLeCb4MaX+I5k496556Y2FiKR/QLZc0+MGBYl4k3OO0472drlV2LUe7c1Glng5HuAu+5GLYp//GpdOQ==", + "dev": true, + "dependencies": { + "@jest/types": "30.0.1", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.2", + "jest-worker": "30.0.2", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.2.tgz", + "integrity": "sha512-U66sRrAYdALq+2qtKffBLDWsQ/XoNNs2Lcr83sc9lvE/hEpNafJlq2lXCPUBMNqamMECNxSIekLfe69qg4KMIQ==", + "dev": true, + "dependencies": { + "@jest/get-type": "30.0.1", + "pretty-format": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.4.tgz", + "integrity": "sha512-ubCewJ54YzeAZ2JeHHGVoU+eDIpQFsfPQs0xURPWoNiO42LGJ+QGgfSf+hFIRplkZDkhH5MOvuxHKXRTUU3dUQ==", + "dev": true, + "dependencies": { + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "jest-diff": "30.0.4", + "pretty-format": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.2.tgz", + "integrity": "sha512-vXywcxmr0SsKXF/bAD7t7nMamRvPuJkras00gqYeB1V0WllxZrbZ0paRr3XqpFU2sYYjD0qAaG2fRyn/CGZ0aw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.2.tgz", + "integrity": "sha512-PnZOHmqup/9cT/y+pXIVbbi8ID6U1XHRmbvR7MvUy4SLqhCbwpkmXhLbsWbGewHrV5x/1bF7YDjs+x24/QSvFA==", + "dev": true, + "dependencies": { + "@jest/types": "30.0.1", + "@types/node": "*", + "jest-util": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.2.tgz", + "integrity": "sha512-q/XT0XQvRemykZsvRopbG6FQUT6/ra+XV6rPijyjT6D0msOyCvR2A5PlWZLd+fH0U8XWKZfDiAgrUNDNX2BkCw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.0.2", + "jest-validate": "30.0.2", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.4.tgz", + "integrity": "sha512-EQBYow19B/hKr4gUTn+l8Z+YLlP2X0IoPyp0UydOtrcPbIOYzJ8LKdFd+yrbwztPQvmlBFUwGPPEzHH1bAvFAw==", + "dev": true, + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.0.4" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.4.tgz", + "integrity": "sha512-mxY0vTAEsowJwvFJo5pVivbCpuu6dgdXRmt3v3MXjBxFly7/lTk3Td0PaMyGOeNQUFmSuGEsGYqhbn7PA9OekQ==", + "dev": true, + "dependencies": { + "@jest/console": "30.0.4", + "@jest/environment": "30.0.4", + "@jest/test-result": "30.0.4", + "@jest/transform": "30.0.4", + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.0.4", + "jest-haste-map": "30.0.2", + "jest-leak-detector": "30.0.2", + "jest-message-util": "30.0.2", + "jest-resolve": "30.0.2", + "jest-runtime": "30.0.4", + "jest-util": "30.0.2", + "jest-watcher": "30.0.4", + "jest-worker": "30.0.2", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.4.tgz", + "integrity": "sha512-tUQrZ8+IzoZYIHoPDQEB4jZoPyzBjLjq7sk0KVyd5UPRjRDOsN7o6UlvaGF8ddpGsjznl9PW+KRgWqCNO+Hn7w==", + "dev": true, + "dependencies": { + "@jest/environment": "30.0.4", + "@jest/fake-timers": "30.0.4", + "@jest/globals": "30.0.4", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.0.4", + "@jest/transform": "30.0.4", + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.2", + "jest-message-util": "30.0.2", + "jest-mock": "30.0.2", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.2", + "jest-snapshot": "30.0.4", + "jest-util": "30.0.2", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.4.tgz", + "integrity": "sha512-S/8hmSkeUib8WRUq9pWEb5zMfsOjiYWDWzFzKnjX7eDyKKgimsu9hcmsUEg8a7dPAw8s/FacxsXquq71pDgPjQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.0.4", + "@jest/get-type": "30.0.1", + "@jest/snapshot-utils": "30.0.4", + "@jest/transform": "30.0.4", + "@jest/types": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0", + "chalk": "^4.1.2", + "expect": "30.0.4", + "graceful-fs": "^4.2.11", + "jest-diff": "30.0.4", + "jest-matcher-utils": "30.0.4", + "jest-message-util": "30.0.2", + "jest-util": "30.0.2", + "pretty-format": "30.0.2", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.2.tgz", + "integrity": "sha512-8IyqfKS4MqprBuUpZNlFB5l+WFehc8bfCe1HSZFHzft2mOuND8Cvi9r1musli+u6F3TqanCZ/Ik4H4pXUolZIg==", + "dev": true, + "dependencies": { + "@jest/types": "30.0.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.2.tgz", + "integrity": "sha512-noOvul+SFER4RIvNAwGn6nmV2fXqBq67j+hKGHKGFCmK4ks/Iy1FSrqQNBLGKlu4ZZIRL6Kg1U72N1nxuRCrGQ==", + "dev": true, + "dependencies": { + "@jest/get-type": "30.0.1", + "@jest/types": "30.0.1", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.0.4", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.4.tgz", + "integrity": "sha512-YESbdHDs7aQOCSSKffG8jXqOKFqw4q4YqR+wHYpR5GWEQioGvL0BfbcjvKIvPEM0XGfsfJrka7jJz3Cc3gI4VQ==", + "dev": true, + "dependencies": { + "@jest/test-result": "30.0.4", + "@jest/types": "30.0.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.0.2", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.2.tgz", + "integrity": "sha512-RN1eQmx7qSLFA+o9pfJKlqViwL5wt+OL3Vff/A+/cPsmuw7NPwfgl33AP+/agRmHzPOFgXviRycR9kYwlcRQXg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.2", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/napi-postinstall": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.0.tgz", + "integrity": "sha512-M7NqKyhODKV1gRLdkwE7pDsZP2/SC2a2vHkOYh9MCpKMbWVfyVfUw5MaH83Fv6XMjxr5jryUp3IDDL9rlxsTeA==", + "dev": true, + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "30.0.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", + "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==", + "dev": true, + "dependencies": { + "@jest/schemas": "30.0.1", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "optional": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/wasm_bindings/package.json b/wasm_bindings/package.json index faf9c9a..7451adb 100644 --- a/wasm_bindings/package.json +++ b/wasm_bindings/package.json @@ -5,7 +5,7 @@ "type": "module", "main": "causalgraphs.js", "scripts": { - "test": "node js/tests/test-wasm.js", + "test": "jest", "demo": "python3 -m http.server 8001", "build": "wasm-pack build --target web --out-dir js/pkg-web", "build:node": "wasm-pack build --target nodejs --out-dir js/pkg-node", @@ -25,5 +25,8 @@ "pkg-node/", "demo/", "test/" - ] -} \ No newline at end of file + ], + "devDependencies": { + "jest": "^30.0.4" + } +} From f5398236594a3848916731a87bc974bfba9e4d54 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 12 Jul 2025 18:31:54 +0530 Subject: [PATCH 32/62] ci initial setup --- .github/workflows/ci.yml | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5f4af35 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Set up R + uses: r-lib/actions/setup-r@v2 + with: + r-version: "4.2" + + - name: Install JS dependencies + working-directory: wasm_bindings + run: npm ci + + - name: Install Python deps + working-directory: python_bindings + run: pip install --upgrade maturin + + - name: Install R deps + run: | + Rscript -e 'install.packages(c("devtools","rextendr"), repos="https://cloud.r-project.org")' + + - name: Build all components + run: make all + + - name: Run full test suite + run: make test From 779fdab51b6d649568cd3ddb2e3c9f85472d09dd Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 12 Jul 2025 18:43:53 +0530 Subject: [PATCH 33/62] Enable manual triggering of CI workflow --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f4af35..b4853f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: [ main ] pull_request: branches: [ main ] + workflow_dispatch: jobs: build-and-test: From e7b5f8516ccaa71e08b6e2bf8e6069111a1b8caa Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 12 Jul 2025 19:35:11 +0530 Subject: [PATCH 34/62] Update Python bindings installation in Makefile to use maturin release build --- Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7b0a903..3e74ea8 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,11 @@ core: python: core @echo "\n=== Building Python Bindings ===" - cd $(PY_BINDINGS) && maturin develop --release + cd $(PY_BINDINGS) && \ + pip install maturin && \ + maturin build --release --out target/wheels && \ + pip install target/wheels/*.whl + wasm: core @echo "\n=== Building WebAssembly Bindings ===" From cf278eb22f46f634a143c0250d2b1763a2b303a5 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 12 Jul 2025 19:59:07 +0530 Subject: [PATCH 35/62] Add installation step for wasm-pack in CI workflow --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4853f5..34a00b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,8 +19,12 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: stable + components: rustfmt, clippy override: true + - name: Install wasm-pack + run: cargo install wasm-pack + - name: Set up Python uses: actions/setup-python@v5 with: From 8ed97d52697a1c5831fb2742157665ad95ecb6f2 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 12 Jul 2025 20:24:09 +0530 Subject: [PATCH 36/62] r deps change --- .github/workflows/ci.yml | 5 +---- Makefile | 5 +++++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34a00b9..f63958a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,9 +22,6 @@ jobs: components: rustfmt, clippy override: true - - name: Install wasm-pack - run: cargo install wasm-pack - - name: Set up Python uses: actions/setup-python@v5 with: @@ -36,7 +33,7 @@ jobs: node-version: "18" - name: Set up R - uses: r-lib/actions/setup-r@v2 + uses: r-lib/actions/setup-r-dependencies@v2 with: r-version: "4.2" diff --git a/Makefile b/Makefile index 3e74ea8..c06f661 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,11 @@ python: core wasm: core @echo "\n=== Building WebAssembly Bindings ===" + # ensure wasm-pack is in PATH (installs it 1st time only) + @if ! command -v wasm-pack >/dev/null 2>&1; then \ + echo "→ Installing wasm-pack…"; \ + cargo install wasm-pack; \ + fi cd $(WASM_BINDINGS) && \ npm install && \ npm run build && \ From c2d73ed68b014197d79a8720f609a22f0c56f500 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 12 Jul 2025 20:29:38 +0530 Subject: [PATCH 37/62] r deps update 2 --- .github/workflows/ci.yml | 7 ++++--- Makefile | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f63958a..3dcc3a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,9 +33,9 @@ jobs: node-version: "18" - name: Set up R - uses: r-lib/actions/setup-r-dependencies@v2 + uses: r-lib/actions/setup-r@v2 with: - r-version: "4.2" + r-version: "4.5" - name: Install JS dependencies working-directory: wasm_bindings @@ -47,7 +47,8 @@ jobs: - name: Install R deps run: | - Rscript -e 'install.packages(c("devtools","rextendr"), repos="https://cloud.r-project.org")' + sudo apt-get install -y libcurl4-openssl-dev libssl-dev + Rscript -e 'install.packages(c("devtools", "rextendr"), repos="https://cloud.r-project.org")' - name: Build all components run: make all diff --git a/Makefile b/Makefile index c06f661..006ec1c 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,8 @@ wasm: core r: core @echo "\n=== Building R Bindings ===" cd $(R_BINDINGS) && \ - Rscript -e "rextendr::document()" + Rscript -e "if(!require('rextendr')) install.packages('rextendr', repos='https://cloud.r-project.org')" && \ + Rscript -e "rextendr::document()" install: python wasm r From 68818b97f96a41da55e0c5daf2fe4c9655694bef Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 12 Jul 2025 21:01:55 +0530 Subject: [PATCH 38/62] R devtools install step --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 006ec1c..b1277a8 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,6 @@ python: core maturin build --release --out target/wheels && \ pip install target/wheels/*.whl - wasm: core @echo "\n=== Building WebAssembly Bindings ===" # ensure wasm-pack is in PATH (installs it 1st time only) @@ -37,6 +36,7 @@ wasm: core r: core @echo "\n=== Building R Bindings ===" cd $(R_BINDINGS) && \ + Rscript -e "if(!require('devtools')) install.packages('devtools', repos='https://cloud.r-project.org')" && \ Rscript -e "if(!require('rextendr')) install.packages('rextendr', repos='https://cloud.r-project.org')" && \ Rscript -e "rextendr::document()" From 005bf181e004806db1eb79d48ed6e0ce98bf8c2d Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 12 Jul 2025 21:17:40 +0530 Subject: [PATCH 39/62] add system dependencies --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3dcc3a7..4ebe0aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,20 @@ jobs: with: r-version: "4.5" + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libcurl4-openssl-dev \ + libssl-dev \ + libfontconfig1-dev \ + libfreetype6-dev \ + libharfbuzz-dev \ + libfribidi-dev \ + libpng-dev \ + libtiff5-dev \ + libjpeg-dev + - name: Install JS dependencies working-directory: wasm_bindings run: npm ci From 8a7c1e8c29a98daed9741671014000ffd37fed47 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 12 Jul 2025 21:45:55 +0530 Subject: [PATCH 40/62] add pytest installation --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b1277a8..4b75fdb 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ core: python: core @echo "\n=== Building Python Bindings ===" cd $(PY_BINDINGS) && \ - pip install maturin && \ + pip install -r requirements.txt && \ maturin build --release --out target/wheels && \ pip install target/wheels/*.whl From a8fe36c135a30e0650a5868f30fead29b90faf60 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 13 Jul 2025 11:16:06 +0530 Subject: [PATCH 41/62] macos test --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ebe0aa..9dea1df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,10 @@ on: jobs: build-and-test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] steps: - name: Checkout code From b44154b2caec38581c756c9622d06c5079daa4cf Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 13 Jul 2025 11:19:30 +0530 Subject: [PATCH 42/62] refactor --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9dea1df..5542f72 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,7 @@ jobs: r-version: "4.5" - name: Install system dependencies + if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y \ @@ -63,6 +64,7 @@ jobs: run: pip install --upgrade maturin - name: Install R deps + if: runner.os == 'Linux' run: | sudo apt-get install -y libcurl4-openssl-dev libssl-dev Rscript -e 'install.packages(c("devtools", "rextendr"), repos="https://cloud.r-project.org")' From 0a6628a294b88eccbce36bd063935fe91d80e909 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 13 Jul 2025 11:25:18 +0530 Subject: [PATCH 43/62] refactor: ci: update CI configuration for macOS --- .github/workflows/ci.yml | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5542f72..8c7c65b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] workflow_dispatch: jobs: @@ -18,6 +18,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + # Setup: Toolchains - name: Set up Rust uses: actions-rs/toolchain@v1 with: @@ -40,7 +41,8 @@ jobs: with: r-version: "4.5" - - name: Install system dependencies + # OS-specific system dependencies (Linux only) + - name: Install system dependencies (Linux only) if: runner.os == 'Linux' run: | sudo apt-get update @@ -55,22 +57,29 @@ jobs: libtiff5-dev \ libjpeg-dev + # Node dependencies - name: Install JS dependencies working-directory: wasm_bindings run: npm ci - - name: Install Python deps + # Python dependencies + - name: Install Python dependencies working-directory: python_bindings - run: pip install --upgrade maturin + run: | + pip install --upgrade pip + pip install maturin pytest - - name: Install R deps + # R dependencies (Linux only) + - name: Install R dependencies (Linux only) if: runner.os == 'Linux' run: | sudo apt-get install -y libcurl4-openssl-dev libssl-dev Rscript -e 'install.packages(c("devtools", "rextendr"), repos="https://cloud.r-project.org")' + # Build all bindings (Rust, Python, WASM, R) - name: Build all components run: make all + # Run full test suite - name: Run full test suite run: make test From 9e966d51f01b013590a7d9d9a8ecc53b0a44aa8d Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 13 Jul 2025 11:51:31 +0530 Subject: [PATCH 44/62] update README.md --- .gitignore | 4 +++- README.md | 12 +++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 9258eff..a2be81a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,6 @@ target/ venv/ .venv/ -*pyc* \ No newline at end of file +*pyc* + +node_modules/ \ No newline at end of file diff --git a/README.md b/README.md index a3b54a5..2b0ceda 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,17 @@ A cross-language library for working with causal graphs (DAGs) in Rust, Python, - **rust_core/**: Core Rust implementation of DAGs and causal graph logic. - **python_bindings/**: Python bindings using [PyO3](https://github.com/PyO3/pyo3) and [maturin](https://github.com/PyO3/maturin). -- **wasm_bindings/**: WebAssembly bindings for use in JavaScript/Node.js. +- **wasm_bindings/**: WebAssembly bindings for use in JavaScript and Node.js environments via [wasm-bindgen](https://rustwasm.github.io/docs/wasm-bindgen/) and [wasm-pack](https://rustwasm.github.io/docs/wasm-pack/) +- **r_bindings/**: R bindings using [extendr](https://github.com/extendr/extendr). + +## Prerequisites + +To build and develop this project locally, you will need: +* [Rust](https://www.rust-lang.org/tools/install) (stable toolchain) +* [Python 3.x](https://www.python.org/downloads/) with `pip` +* [Node.js](https://nodejs.org/) (LTS recommended) with `npm` +* [R 4.2+](https://www.r-project.org/) +* [make](https://www.gnu.org/software/make/) (usually pre-installed on Linux/macOS, available via build tools on Windows) ## Quick Start From f543c6b156b7d8c9d9f9198139dee08d03308d6a Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 13 Jul 2025 15:44:27 +0530 Subject: [PATCH 45/62] update README.md --- README.md | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 2b0ceda..63cdb32 100644 --- a/README.md +++ b/README.md @@ -18,31 +18,36 @@ To build and develop this project locally, you will need: * [R 4.2+](https://www.r-project.org/) * [make](https://www.gnu.org/software/make/) (usually pre-installed on Linux/macOS, available via build tools on Windows) +## Supported Platforms + +This library is actively developed and tested on: +- **Windows** (via WSL, or native MSVC toolchain for Rust + Rtools for R) +- **Linux** (Ubuntu, other distros) +- **macOS** + ## Quick Start -### Rust +We provide a top‑level `Makefile` to save you typing: + +- **Build everything** (Rust core + Python + WASM + R): -```sh -cd rust_core -cargo test -``` + ```sh + make all + ``` -### Python +- **Run all tests**: -```sh -cd python_bindings -maturin develop -python -c "import causalgraphs; print(dir(causalgraphs))" -``` + ```sh + make test + ``` -### WebAssembly (Node.js) +| Target | What it does | +| ------------- | ------------------------------------------------ | +| `make core` | Builds only the `rust_core` crate. | +| `make python` | Builds & installs Python bindings. | +| `make wasm` | Builds WASM modules for JS/Node via wasm-pack | +| `make r` | Generates R wrappers via `rextendr::document()`. | -```sh -cd wasm_bindings -npm install -npm run build -npm run test -``` ## License From af59ba6aedbfab6bc12e417fdfbb09208cc667e4 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 13 Jul 2025 16:06:24 +0530 Subject: [PATCH 46/62] test --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 63cdb32..47e1da5 100644 --- a/README.md +++ b/README.md @@ -51,4 +51,4 @@ We provide a top‑level `Makefile` to save you typing: ## License -MIT \ No newline at end of file +MIT From b41ffe87e7fe5e259a6a269cc15da5079f3baea4 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 19 Jul 2025 12:56:55 +0530 Subject: [PATCH 47/62] active_trail_nodes & is_dconnected impl --- rust_core/src/dag.rs | 146 ++++++++++++++++++++++++++++++++++++++++- rust_core/tests/dag.rs | 77 ++++++++++++++++++++++ 2 files changed, 222 insertions(+), 1 deletion(-) diff --git a/rust_core/src/dag.rs b/rust_core/src/dag.rs index 183a185..989f895 100644 --- a/rust_core/src/dag.rs +++ b/rust_core/src/dag.rs @@ -2,7 +2,7 @@ use petgraph::Direction; use rustworkx_core::petgraph::graph::{DiGraph, NodeIndex}; use std::collections::{HashMap, HashSet, VecDeque}; -// Remove #[pyclass] here. This is a pure Rust struct. + #[derive(Debug, Clone)] // Add Debug for easier printing in Rust tests pub struct RustDAG { pub graph: DiGraph, // Make fields public if bindings need direct access, @@ -65,6 +65,26 @@ impl RustDAG { Ok(()) } + pub fn add_edges_from( + &mut self, + ebunch: Vec<(String, String)>, + weights: Option>, + ) -> Result<(), String> { + if let Some(ws) = &weights { + if ebunch.len() != ws.len() { + return Err("The number of elements in ebunch and weights should be equal".to_string()); + } + for (i, (u, v)) in ebunch.iter().enumerate() { + self.add_edge(u.clone(), v.clone(), Some(ws[i]))?; + } + } else { + for (u, v) in ebunch { + self.add_edge(u, v, None)?; + } + } + Ok(()) + } + /// Get parents of a node pub fn get_parents(&self, node: &str) -> Result, String> { let node_idx = self.node_map.get(node) @@ -122,6 +142,123 @@ impl RustDAG { Ok(ancestors) } + + pub fn active_trail_nodes(&self, variables: Vec, observed: Option>, include_latents: bool) -> Result>, String> { + let observed_list: HashSet = observed.unwrap_or_default().into_iter().collect(); + let ancestors_list: HashSet = self.get_ancestors_of(observed_list.iter().cloned().collect())?; + + let mut active_trails: HashMap> = HashMap::new(); + for start in variables { + let mut visit_list: HashSet<(String, &str)> = HashSet::new(); + let mut traversed_list: HashSet<(String, &str)> = HashSet::new(); + let mut active_nodes: HashSet = HashSet::new(); + + if !self.node_map.contains_key(&start) { + return Err(format!("Node {} not in graph", start)); + } + + visit_list.insert((start.clone(), "up")); + while let Some((node, direction)) = visit_list.iter().next().map(|x| x.clone()) { + visit_list.remove(&(node.clone(), direction)); + if !traversed_list.contains(&(node.clone(), direction)) { + if !observed_list.contains(&node) { + active_nodes.insert(node.clone()); + } + traversed_list.insert((node.clone(), direction)); + + if direction == "up" && !observed_list.contains(&node) { + for parent in self.get_parents(&node)? { + visit_list.insert((parent, "up")); + } + for child in self.get_children(&node)? { + visit_list.insert((child, "down")); + } + } else if direction == "down" { + if !observed_list.contains(&node) { + for child in self.get_children(&node)? { + visit_list.insert((child, "down")); + } + } + if ancestors_list.contains(&node) { + for parent in self.get_parents(&node)? { + visit_list.insert((parent, "up")); + } + } + } + } + } + + let final_nodes: HashSet = if include_latents { + active_nodes + } else { + active_nodes.difference(&self.latents).cloned().collect() + }; + active_trails.insert(start, final_nodes); + } + + Ok(active_trails) + } + + pub fn is_dconnected(&self, start: &str, end: &str, observed: Option>, include_latents: bool) -> Result { + let trails = self.active_trail_nodes(vec![start.to_string()], observed, include_latents)?; + Ok(trails.get(start).map(|nodes| nodes.contains(end)).unwrap_or(false)) + } + + /// Check if two nodes are neighbors (directly connected in either direction) + pub fn are_neighbors(&self, start: &str, end: &str) -> Result { + let start_idx = self.node_map.get(start) + .ok_or_else(|| format!("Node {} not found", start))?; + let end_idx = self.node_map.get(end) + .ok_or_else(|| format!("Node {} not found", end))?; + + // Check for edge in either direction + let has_edge = self.graph.find_edge(*start_idx, *end_idx).is_some() || + self.graph.find_edge(*end_idx, *start_idx).is_some(); + + Ok(has_edge) + } + + /// Get ancestral graph containing only ancestors of the given nodes + pub fn get_ancestral_graph(&self, nodes: Vec) -> Result { + let ancestors = self.get_ancestors_of(nodes)?; + let mut ancestral_graph = RustDAG::new(); + + // Add all ancestor nodes with their latent status + for node in &ancestors { + let is_latent = self.latents.contains(node); + ancestral_graph.add_node(node.clone(), is_latent)?; + } + + // Add edges between ancestors only + for (source, target) in self.edges() { + if ancestors.contains(&source) && ancestors.contains(&target) { + ancestral_graph.add_edge(source, target, None)?; + } + } + + Ok(ancestral_graph) + } + + + + /// Returns a list of leaves (nodes with out-degree 0) + pub fn get_leaves(&self) -> Vec { + self.graph + .node_indices() + .filter(|&idx| self.graph.neighbors_directed(idx, Direction::Outgoing).next().is_none()) + .map(|idx| self.reverse_node_map[&idx].clone()) + .collect() + } + + /// Returns a list of roots (nodes with in-degree 0) + pub fn get_roots(&self) -> Vec { + self.graph + .node_indices() + .filter(|&idx| self.graph.neighbors_directed(idx, Direction::Incoming).next().is_none()) + .map(|idx| self.reverse_node_map[&idx].clone()) + .collect() + } + /// Get all nodes in the graph pub fn nodes(&self) -> Vec { self.node_map.keys().cloned().collect() @@ -141,6 +278,13 @@ impl RustDAG { .collect() } + pub fn has_edge(&self, u: &str, v: &str) -> bool { + match (self.node_map.get(u), self.node_map.get(v)) { + (Some(u_idx), Some(v_idx)) => self.graph.find_edge(*u_idx, *v_idx).is_some(), + _ => false, + } + } + /// Get number of nodes pub fn node_count(&self) -> usize { self.graph.node_count() diff --git a/rust_core/tests/dag.rs b/rust_core/tests/dag.rs index 530c177..fce2ff9 100644 --- a/rust_core/tests/dag.rs +++ b/rust_core/tests/dag.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use rust_core::RustDAG; #[test] @@ -9,4 +11,79 @@ fn test_add_nodes_and_edges() { assert_eq!(dag.node_count(), 2); assert_eq!(dag.edge_count(), 1); assert!(dag.get_parents("B").unwrap().contains(&"A".to_string())); +} + +#[test] +fn test_active_trail_basic() { + let mut dag: RustDAG = RustDAG::new(); + dag.add_edges_from( + vec![ + ("diff".to_string(), "grades".to_string()), + ("intel".to_string(), "grades".to_string()), + ], + None, + ).unwrap(); + + let result = dag.active_trail_nodes( + vec!["diff".to_string()], + None, + false + ).unwrap(); + + let expected: HashSet = vec!["diff".to_string(), "grades".to_string()] + .into_iter().collect(); + assert_eq!(result["diff"], expected); +} + + +#[test] +fn test_active_trail_with_observed() { + let mut dag = RustDAG::new(); + dag.add_edges_from( + vec![ + ("diff".to_string(), "grades".to_string()), + ("intel".to_string(), "grades".to_string()), + ], + None, + ).unwrap(); + + let result = dag.active_trail_nodes( + vec!["diff".to_string(), "intel".to_string()], + Some(vec!["grades".to_string()]), + false + ).unwrap(); + // With grades observed, diff and intel should be in each other's active trail + let expected_diff: HashSet = vec!["diff".to_string(), "intel".to_string()] + .into_iter().collect(); + let expected_intel: HashSet = vec!["diff".to_string(), "intel".to_string()] + .into_iter().collect(); + + assert_eq!(result["diff"], expected_diff); + assert_eq!(result["intel"], expected_intel); +} + + + #[test] +fn test_is_dconnected() { + let mut dag = RustDAG::new(); + dag.add_edges_from( + vec![ + ("diff".to_string(), "grades".to_string()), + ("intel".to_string(), "grades".to_string()), + ("grades".to_string(), "letter".to_string()), + ("intel".to_string(), "sat".to_string()), + ], + None, + ).unwrap(); + // diff and intel are not d-connected (blocked by collider at grades) + assert_eq!(dag.is_dconnected("diff", "intel", None, false).unwrap(), false); + + // grades and sat are d-connected through intel + assert_eq!(dag.is_dconnected("grades", "sat", None, false).unwrap(), true); + + // diff and intel become d-connected when grades is observed + assert_eq!( + dag.is_dconnected("diff", "intel", Some(vec!["grades".to_string()]), false).unwrap(), + true + ); } \ No newline at end of file From 16702feb01969d51d772471d72de798559257484 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 19 Jul 2025 17:39:20 +0530 Subject: [PATCH 48/62] core: d-separation impl & tests --- rust_core/src/dag.rs | 59 +++++++++++++++- rust_core/tests/dag.rs | 152 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 209 insertions(+), 2 deletions(-) diff --git a/rust_core/src/dag.rs b/rust_core/src/dag.rs index 989f895..d5d16e7 100644 --- a/rust_core/src/dag.rs +++ b/rust_core/src/dag.rs @@ -6,7 +6,7 @@ use std::collections::{HashMap, HashSet, VecDeque}; #[derive(Debug, Clone)] // Add Debug for easier printing in Rust tests pub struct RustDAG { pub graph: DiGraph, // Make fields public if bindings need direct access, - pub node_map: HashMap, // or provide internal methods. + pub node_map: HashMap, pub reverse_node_map: HashMap, pub latents: HashSet, } @@ -204,6 +204,63 @@ impl RustDAG { Ok(trails.get(start).map(|nodes| nodes.contains(end)).unwrap_or(false)) } + pub fn minimal_dseparator( + &self, + start: &str, + end: &str, + include_latents: bool + ) -> Result>, String> { + if self.has_edge(start, end) || self.has_edge(end, start) { + return Err("No possible separators because start and end are adjacent".to_string()); + } + + // Create proper ancestral graph + let ancestral_graph = self.get_ancestral_graph(vec![start.to_string(), end.to_string()])?; + + let mut separator: HashSet = self.get_parents(start)? + .into_iter() + .chain(self.get_parents(end)?.into_iter()) + .collect(); + + if !include_latents { + let mut changed = true; + while changed { + changed = false; + let mut new_separator: HashSet = HashSet::new(); + + for node in &separator { + if self.latents.contains(node) { + new_separator.extend(self.get_parents(node)?); + changed = true; + } else { + new_separator.insert(node.clone()); + } + } + separator = new_separator; + } + } + + separator.remove(start); + separator.remove(end); + + // If the initial set is not able to d-separate, no d-separator is possible. + if ancestral_graph.is_dconnected(start, end, Some(separator.iter().cloned().collect()), include_latents)? { + return Ok(None); + } + + let mut minimal_separator = separator.clone(); + for u in separator { + let test_separator: Vec = minimal_separator.iter().cloned().filter(|x| x != &u).collect(); + + // If still d-separated WITHOUT this node, we can remove it + if !ancestral_graph.is_dconnected(start, end, Some(test_separator), include_latents)? { + minimal_separator.remove(&u); + } + } + + Ok(Some(minimal_separator)) + } + /// Check if two nodes are neighbors (directly connected in either direction) pub fn are_neighbors(&self, start: &str, end: &str) -> Result { let start_idx = self.node_map.get(start) diff --git a/rust_core/tests/dag.rs b/rust_core/tests/dag.rs index fce2ff9..9e8de77 100644 --- a/rust_core/tests/dag.rs +++ b/rust_core/tests/dag.rs @@ -86,4 +86,154 @@ fn test_is_dconnected() { dag.is_dconnected("diff", "intel", Some(vec!["grades".to_string()]), false).unwrap(), true ); -} \ No newline at end of file +} + +#[test] +fn test_are_neighbors() { + let mut dag = RustDAG::new(); + dag.add_edges_from( + vec![ + ("A".to_string(), "B".to_string()), + ("B".to_string(), "C".to_string()), + ], + None + ).unwrap(); + assert_eq!(dag.are_neighbors("A", "B").unwrap(), true); + assert_eq!(dag.are_neighbors("B", "A").unwrap(), true); // Should work both ways + assert_eq!(dag.are_neighbors("A", "C").unwrap(), false); +} + + +#[test] +fn test_minimal_dseparator_simple() { + let mut dag = RustDAG::new(); + dag.add_edges_from( + vec![ + ("A".to_string(), "B".to_string()), + ("B".to_string(), "C".to_string()), + ], + None + ).unwrap(); + let result = dag.minimal_dseparator("A", "C", false).unwrap(); + let expected: HashSet = vec!["B".to_string()].into_iter().collect(); + assert_eq!(result, Some(expected)); +} + + +#[test] +fn test_minimal_dseparator_complex() { + let mut dag = RustDAG::new(); + dag.add_edges_from( + vec![ + ("A".to_string(), "B".to_string()), + ("B".to_string(), "C".to_string()), + ("C".to_string(), "D".to_string()), + ("A".to_string(), "E".to_string()), + ("E".to_string(), "D".to_string()), + ], None + ).unwrap(); + let result = dag.minimal_dseparator("A", "D", false).unwrap(); + let expected: HashSet = vec!["C".to_string(), "E".to_string()] + .into_iter().collect(); + assert_eq!(result, Some(expected)); +} + +#[test] +fn test_minimal_dseparator_latent_case_1() { + let mut dag = RustDAG::new(); + dag.add_node("A".to_string(), false).unwrap(); + dag.add_node("B".to_string(), true).unwrap(); // latent + dag.add_node("C".to_string(), false).unwrap(); + dag.add_edges_from( + vec![ + ("A".to_string(), "B".to_string()), + ("B".to_string(), "C".to_string()), + ], + None + ).unwrap(); + // No d-separator should exist because B is latent + let result = dag.minimal_dseparator("A", "C", false).unwrap(); + assert_eq!(result, None); +} + +#[test] +fn test_minimal_dseparator_latent_case_2() { + let mut dag = RustDAG::new(); + dag.add_node("A".to_string(), false).unwrap(); + dag.add_node("D".to_string(), false).unwrap(); + dag.add_node("B".to_string(), true).unwrap(); // B is latent + dag.add_node("C".to_string(), false).unwrap(); + dag.add_edges_from( + vec![ + ("A".to_string(), "D".to_string()), + ("D".to_string(), "B".to_string()), + ("B".to_string(), "C".to_string()), + ], + None + ).unwrap(); + + let result = dag.minimal_dseparator("A", "C", false).unwrap(); + let expected: HashSet = vec!["D".to_string()].into_iter().collect(); + assert_eq!(result, Some(expected), "Expected D to d-separate A and C when B is latent"); +} + +#[test] +fn test_minimal_dseparator_latent_case_3() { + let mut dag = RustDAG::new(); + dag.add_node("A".to_string(), false).unwrap(); + dag.add_node("B".to_string(), false).unwrap(); + dag.add_node("C".to_string(), false).unwrap(); + dag.add_node("D".to_string(), true).unwrap(); // D is latent + dag.add_edges_from( + vec![ + ("A".to_string(), "B".to_string()), + ("B".to_string(), "C".to_string()), + ("A".to_string(), "D".to_string()), + ("D".to_string(), "C".to_string()), + ], + None + ).unwrap(); + let result = dag.minimal_dseparator("A", "C", false).unwrap(); + assert_eq!(result, None, "Expected no d-separator when D is latent with multiple paths A→B→C and A→D→C (D is unobservable, because of its latent status)"); +} + + +#[test] +fn test_minimal_dseparator_latent_case_5() { + // dag_lat5 = DAG([("A", "B"), ("B", "C"), ("A", "D"), ("D", "E"), ("E", "C")], latents={"E"}) + // self.assertEqual(dag_lat5.minimal_dseparator(start="A", end="C"), {"B", "D"}) + let mut dag = RustDAG::new(); + dag.add_node("A".to_string(), false).unwrap(); + dag.add_node("B".to_string(), false).unwrap(); + dag.add_node("C".to_string(), false).unwrap(); + dag.add_node("D".to_string(), false).unwrap(); + dag.add_node("E".to_string(), true).unwrap(); // E is latent + dag.add_edges_from( + vec![ + ("A".to_string(), "B".to_string()), + ("B".to_string(), "C".to_string()), + ("A".to_string(), "D".to_string()), + ("D".to_string(), "E".to_string()), + ("E".to_string(), "C".to_string()), + ], None + ).unwrap(); + let result = dag.minimal_dseparator("A", "C", false).unwrap(); + let expected: HashSet = vec!["B".to_string(), "D".to_string()].into_iter().collect(); + assert_eq!(result, Some(expected), "Expected [B, D] to d-separate A and C when E is latent(Observe B & parent of C => D)"); +} + + +#[test] +fn test_minimal_dseparator_adjacent_error() { + let mut dag = RustDAG::new(); + dag.add_edges_from( + vec![ + ("A".to_string(), "B".to_string()), + ], + None + ).unwrap(); + + let result = dag.minimal_dseparator("A", "B", false); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("adjacent")); +} From d0687dcabdeb9d59d4736f6f2576aadf70da8c81 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 20 Jul 2025 08:25:37 +0530 Subject: [PATCH 49/62] Independencies initial commit --- Cargo.lock | 12 +- rust_core/Cargo.toml | 3 +- .../src/independencies/independencies.rs | 348 ++++++++++++++-- rust_core/src/lib.rs | 4 +- rust_core/tests/independence_tests.rs | 370 ++++++++++++++++++ 5 files changed, 692 insertions(+), 45 deletions(-) create mode 100644 rust_core/tests/independence_tests.rs diff --git a/Cargo.lock b/Cargo.lock index f0f6e0e..9f84888 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -262,6 +262,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -535,7 +544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "059f538b55efd2309c9794130bc149c6a553db90e9d99c2030785c82f0bd7df9" dependencies = [ "either", - "itertools", + "itertools 0.11.0", "rayon", ] @@ -572,6 +581,7 @@ version = "0.1.0" dependencies = [ "ahash", "indexmap 2.10.0", + "itertools 0.14.0", "petgraph", "rustworkx-core", ] diff --git a/rust_core/Cargo.toml b/rust_core/Cargo.toml index a498f6e..c34c4df 100644 --- a/rust_core/Cargo.toml +++ b/rust_core/Cargo.toml @@ -11,4 +11,5 @@ path = "src/lib.rs" petgraph = "0.6" ahash = "0.8" indexmap = "2.0" -rustworkx-core = "0.14" \ No newline at end of file +rustworkx-core = "0.14" +itertools = "0.14.0" diff --git a/rust_core/src/independencies/independencies.rs b/rust_core/src/independencies/independencies.rs index e2971ad..b8fae08 100644 --- a/rust_core/src/independencies/independencies.rs +++ b/rust_core/src/independencies/independencies.rs @@ -1,90 +1,354 @@ -use itertools::Itertools; -use std::collections::HashSet; +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::hash::{Hash, Hasher}; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Eq)] pub struct IndependenceAssertion { pub event1: HashSet, - pub event2: HashSet, + pub event2: HashSet, pub event3: HashSet, + pub all_vars: HashSet, +} + +impl Hash for IndependenceAssertion { + fn hash(&self, state: &mut H) { + // Convert HashSets to BTreeSets for deterministic, sortable representation + let event1_ordered: BTreeSet<_> = self.event1.iter().collect(); + let event2_ordered: BTreeSet<_> = self.event2.iter().collect(); + let event3_ordered: BTreeSet<_> = self.event3.iter().collect(); + + // Create symmetric hash: X ⊥ Y | Z should equal Y ⊥ X | Z + let mut symmetric_pair: Vec> = vec![event1_ordered, event2_ordered]; + symmetric_pair.sort(); // BTreeSet implements Ord, so this works + + // Hash the components + symmetric_pair.hash(state); // Symmetric part + event3_ordered.hash(state); // Conditioning set + } } impl IndependenceAssertion { - pub fn new(event1: HashSet, event2: HashSet, event3: HashSet) -> Self { - IndependenceAssertion { event1, event2, event3 } + pub fn new( + event1: HashSet, + event2: HashSet, + event3: Option>, + ) -> Result { + if event1.is_empty() { + return Err("event1 needs to be specified".to_string()); + } + if event2.is_empty() { + return Err("event2 needs to be specified".to_string()); + } + + let e3 = event3.unwrap_or_default(); + + let mut all_vars = HashSet::new(); + all_vars.extend(event1.iter().cloned()); + all_vars.extend(event2.iter().cloned()); + all_vars.extend(e3.iter().cloned()); + + Ok(Self { + event1, + event2, + event3: e3, + all_vars, + }) + } + + pub fn is_unconditional(&self) -> bool { + self.event3.is_empty() + } + + pub fn to_latex(&self) -> String { + let e1_str = self.event1.iter().cloned().collect::>().join(", "); + let e2_str = self.event2.iter().cloned().collect::>().join(", "); + + if self.event3.is_empty() { + format!("{} \\perp {}", e1_str, e2_str) + } else { + let e3_str = self.event3.iter().cloned().collect::>().join(", "); + format!("{} \\perp {} \\mid {}", e1_str, e2_str, e3_str) + } } +} - pub fn latex_string(&self) -> String { +impl std::fmt::Display for IndependenceAssertion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let e1_str = self.event1.iter().cloned().collect::>().join(", "); + let e2_str = self.event2.iter().cloned().collect::>().join(", "); + if self.event3.is_empty() { - format!( - "{} \\perp {}", - self.event1.iter().join(", "), - self.event2.iter().join(", ") - ) + write!(f, "({} ⊥ {})", e1_str, e2_str) } else { - format!( - "{} \\perp {} \\mid {}", - self.event1.iter().join(", "), - self.event2.iter().join(", "), - self.event3.iter().join(", ") - ) + let e3_str = self.event3.iter().cloned().collect::>().join(", "); + write!(f, "({} ⊥ {} | {})", e1_str, e2_str, e3_str) } } } +impl PartialEq for IndependenceAssertion { + fn eq(&self, other: &Self) -> bool { + // Check if event3 (conditioning set) is the same + if self.event3 != other.event3 { + return false; + } + + // Check both orientations for event1 and event2 (symmetry) + (self.event1 == other.event1 && self.event2 == other.event2) || + (self.event1 == other.event2 && self.event2 == other.event1) + } +} + + #[derive(Debug, Clone)] pub struct Independencies { - pub assertions: Vec, + assertions: Vec, } impl Independencies { pub fn new() -> Self { - Independencies { assertions: Vec::new() } + Self { + assertions: Vec::new(), + } } - + + pub fn from_assertions(assertions: Vec) -> Self { + Self { assertions } + } + pub fn add_assertion(&mut self, assertion: IndependenceAssertion) { self.assertions.push(assertion); } + + pub fn add_assertions_from_tuples(&mut self, tuples: Vec<(Vec, Vec, Option>)>) -> Result<(), String> { + for (e1, e2, e3) in tuples { + let assertion = IndependenceAssertion::new( + e1.into_iter().collect::>(), + e2.into_iter().collect::>(), + e3.map(|x| x.into_iter().collect::>()), + )?; + self.add_assertion(assertion); + } + Ok(()) + } + + pub fn get_assertions(&self) -> &Vec { + &self.assertions + } + + pub fn get_all_variables(&self) -> HashSet { + self.assertions + .iter() + .flat_map(|a| a.all_vars.iter().cloned()) + .collect() + } + + pub fn contains(&self, assertion: &IndependenceAssertion) -> bool { + self.assertions.contains(assertion) + } + + /// Generate closure using semi-graphoid axioms + pub fn closure(&self) -> Self { + let mut all_independencies: HashSet = HashSet::new(); + let mut new_inds: HashSet = self.assertions.iter().cloned().collect(); + + while !new_inds.is_empty() { + // Generate pairs for contraction rule + let new_pairs: Vec<(IndependenceAssertion, IndependenceAssertion)> = new_inds + .iter() + .flat_map(|ind1| { + all_independencies.iter().chain(new_inds.iter()) + .map(move |ind2| (ind1.clone(), ind2.clone())) + }) + .collect(); + + all_independencies.extend(new_inds.iter().cloned()); + + let mut next_round = HashSet::new(); + + // Apply axioms + for ind in &new_inds { + next_round.extend(self.sg1_decomposition(ind)); + next_round.extend(self.sg2_weak_union(ind)); + } + + for (ind1, ind2) in new_pairs { + next_round.extend(self.sg3_contraction(&ind1, &ind2)); + } + + // Remove already known assertions + next_round.retain(|ind| !all_independencies.contains(ind)); + new_inds = next_round; + } + + Self::from_assertions(all_independencies.into_iter().collect()) + } + + /// Decomposition rule: 'X ⊥ Y,W | Z' -> 'X ⊥ Y | Z', 'X ⊥ W | Z' + fn sg1_decomposition(&self, ind: &IndependenceAssertion) -> Vec { + if ind.event2.len() <= 1 { + return vec![]; + } + + let mut results = Vec::new(); + for elem in &ind.event2 { + let mut new_event2 = ind.event2.clone(); + new_event2.remove(elem); + + if let Ok(assertion) = IndependenceAssertion::new( + ind.event1.clone(), + new_event2, + Some(ind.event3.clone()), + ) { + results.push(assertion); + } + } + + // Apply symmetry + for elem in &ind.event1 { + let mut new_event1 = ind.event1.clone(); + new_event1.remove(elem); + + if let Ok(assertion) = IndependenceAssertion::new( + new_event1, + ind.event2.clone(), + Some(ind.event3.clone()), + ) { + results.push(assertion); + } + } + + results + } + + /// Weak Union rule: 'X ⊥ Y,W | Z' -> 'X ⊥ Y | W,Z', 'X ⊥ W | Y,Z' + fn sg2_weak_union(&self, ind: &IndependenceAssertion) -> Vec { + if ind.event2.len() <= 1 { + return vec![]; + } + + let mut results = Vec::new(); + for elem in &ind.event2 { + let mut new_event2 = ind.event2.clone(); + new_event2.remove(elem); + let mut new_event3 = ind.event3.clone(); + new_event3.insert(elem.clone()); + + if let Ok(assertion) = IndependenceAssertion::new( + ind.event1.clone(), + new_event2, + Some(new_event3), + ) { + results.push(assertion); + } + } + + // Apply symmetry for event1 + for elem in &ind.event1 { + let mut new_event1 = ind.event1.clone(); + new_event1.remove(elem); + let mut new_event3 = ind.event3.clone(); + new_event3.insert(elem.clone()); + + if let Ok(assertion) = IndependenceAssertion::new( + new_event1, + ind.event2.clone(), + Some(new_event3), + ) { + results.push(assertion); + } + } + + results + } + + /// Contraction rule: 'X ⊥ W | Y,Z' & 'X ⊥ Y | Z' -> 'X ⊥ W,Y | Z' + fn sg3_contraction(&self, ind1: &IndependenceAssertion, ind2: &IndependenceAssertion) -> Vec { + if ind1.event1 != ind2.event1 { + return vec![]; + } + + let y = &ind2.event2; + let z = &ind2.event3; + let y_z = &ind1.event3; + + // Check if Y ⊂ Y∪Z and Z ⊂ Y∪Z and Y ∩ Z = ∅ + if y.is_subset(y_z) && z.is_subset(y_z) && y.is_disjoint(z) { + let mut new_event2 = ind1.event2.clone(); + new_event2.extend(y.iter().cloned()); + + if let Ok(assertion) = IndependenceAssertion::new( + ind1.event1.clone(), + new_event2, + Some(z.clone()), + ) { + return vec![assertion]; + } + } + + vec![] + } - pub fn reduce(&mut self) -> Independencies { - let mut unique_assertions: HashSet = self.assertions.iter().cloned().collect(); + pub fn reduce(&self) -> Self { + let mut unique_assertions: Vec = + self.assertions.iter().cloned().collect::>() + .into_iter().collect(); + let mut reduced_assertions = Vec::new(); for assertion in unique_assertions { - let mut temp_independencies = Independencies { assertions: reduced_assertions.clone() }; - let assertion_temp = Independencies { assertions: vec![assertion.clone()] }; + let temp_independencies = Self::from_assertions(reduced_assertions.clone()); + let assertion_temp = Self::from_assertions(vec![assertion.clone()]); - let entails = temp_independencies.assertions.iter().any(|existing| { - existing.event1 == assertion.event1 && - existing.event2 == assertion.event2 && - existing.event3 == assertion.event3 - }); - - if !entails { + if !temp_independencies.entails(&assertion_temp) { let mut removed_any = true; while removed_any { removed_any = false; - let current_reduced = reduced_assertions.clone(); - for existing in current_reduced.iter() { - let existing_temp = Independencies { assertions: vec![existing.clone()] }; - if existing != &assertion { - // Placeholder for entailment logic - let remove_old = false; // Replace with actual entailment check if needed + let mut i = 0; + while i < reduced_assertions.len() { + let existing_temp = Self::from_assertions(vec![reduced_assertions[i].clone()]); + + if existing_temp != assertion_temp { + let remove_old = !existing_temp.entails(&assertion_temp) && + assertion_temp.entails(&existing_temp); + if remove_old { - reduced_assertions.retain(|x| x != existing); + reduced_assertions.remove(i); removed_any = true; break; } } + i += 1; } } reduced_assertions.push(assertion); } } - Independencies { assertions: reduced_assertions } + Self::from_assertions(reduced_assertions) + } + + pub fn reduce_inplace(&mut self) { + let reduced = self.reduce(); + self.assertions = reduced.assertions; } + + /// Check if this set of independencies entails another set + pub fn entails(&self, other: &Independencies) -> bool { + let closure_assertions = self.closure().assertions; + other.assertions.iter().all(|assertion| closure_assertions.contains(assertion)) + } + + /// Check if two sets of independencies are equivalent + pub fn is_equivalent(&self, other: &Independencies) -> bool { + self.entails(other) && other.entails(self) + } +} - pub fn latex_string(&self) -> Vec { - self.assertions.iter().map(|assertion| assertion.latex_string()).collect() +impl PartialEq for Independencies { + fn eq(&self, other: &Self) -> bool { + // Convert to sets to ignore order + let self_set: std::collections::HashSet<_> = self.assertions.iter().collect(); + let other_set: std::collections::HashSet<_> = other.assertions.iter().collect(); + self_set == other_set } } diff --git a/rust_core/src/lib.rs b/rust_core/src/lib.rs index cf99111..77d0741 100644 --- a/rust_core/src/lib.rs +++ b/rust_core/src/lib.rs @@ -1,5 +1,7 @@ // Re-export modules/structs from your core logic pub mod dag; +pub mod independencies; // pub mod pdag; // Add PDAG.rs later if needed -pub use dag::RustDAG; \ No newline at end of file +pub use dag::RustDAG; +pub use independencies::{IndependenceAssertion, Independencies}; \ No newline at end of file diff --git a/rust_core/tests/independence_tests.rs b/rust_core/tests/independence_tests.rs new file mode 100644 index 0000000..4ea6394 --- /dev/null +++ b/rust_core/tests/independence_tests.rs @@ -0,0 +1,370 @@ +#[cfg(test)] +mod independence_tests { + use rust_core::IndependenceAssertion; + + use super::*; + use std::collections::HashSet; + + // Helper function to create HashSet from vector + fn set_from_vec(vec: Vec<&str>) -> HashSet { + vec.into_iter().map(|s| s.to_string()).collect() + } + + // Helper function to create IndependenceAssertion from strings + fn create_assertion(e1: Vec<&str>, e2: Vec<&str>, e3: Option>) -> IndependenceAssertion { + IndependenceAssertion::new( + set_from_vec(e1), + set_from_vec(e2), + e3.map(set_from_vec), + ).unwrap() + } + + #[cfg(test)] + mod test_independence_assertion { + use rust_core::IndependenceAssertion; + + use super::*; + + #[test] + fn test_new_assertion() { + let assertion = IndependenceAssertion::new( + set_from_vec(vec!["U"]), + set_from_vec(vec!["V"]), + Some(set_from_vec(vec!["Z"])), + ).unwrap(); + + assert_eq!(assertion.event1, set_from_vec(vec!["U"])); + assert_eq!(assertion.event2, set_from_vec(vec!["V"])); + assert_eq!(assertion.event3, set_from_vec(vec!["Z"])); + } + + #[test] + fn test_assertion_with_multiple_variables() { + let assertion = IndependenceAssertion::new( + set_from_vec(vec!["U", "V"]), + set_from_vec(vec!["Y", "Z"]), + Some(set_from_vec(vec!["A", "B"])), + ).unwrap(); + + assert_eq!(assertion.event1, set_from_vec(vec!["U", "V"])); + assert_eq!(assertion.event2, set_from_vec(vec!["Y", "Z"])); + assert_eq!(assertion.event3, set_from_vec(vec!["A", "B"])); + } + + #[test] + fn test_assertion_without_conditioning() { + let assertion = IndependenceAssertion::new( + set_from_vec(vec!["U"]), + set_from_vec(vec!["V"]), + None + ).unwrap(); + + assert_eq!(assertion.event1, set_from_vec(vec!["U"])); + assert_eq!(assertion.event2, set_from_vec(vec!["V"])); + assert!(assertion.event3.is_empty()); + assert!(assertion.is_unconditional()); + } + + #[test] + fn test_assertion_validation_errors() { + // event1 empty should fail + let result = IndependenceAssertion::new( + HashSet::new(), + set_from_vec(vec!["V"]), + None, + ); + assert!(result.is_err()); + + // event2 empty should fail + let result = IndependenceAssertion::new( + set_from_vec(vec!["U"]), + HashSet::new(), + None, + ); + assert!(result.is_err()); + } + + #[test] + fn test_all_variables() { + let assertion = create_assertion( + vec!["A", "B"], + vec!["C"], + Some(vec!["D", "E"]), + ); + + let expected_vars = set_from_vec(vec!["A", "B", "C", "D", "E"]); + assert_eq!(assertion.all_vars, expected_vars); + } + + #[test] + fn test_display_formatting() { + let assertion1 = create_assertion(vec!["X"], vec!["Y"], None); + assert_eq!(format!("{}", assertion1), "(X ⊥ Y)"); + + let assertion2 = create_assertion(vec!["X"], vec!["Y"], Some(vec!["Z"])); + assert!(format!("{}", assertion2).contains("⊥") && format!("{}", assertion2).contains("|")); + } + + #[test] + fn test_latex_formatting() { + let assertion1 = create_assertion(vec!["X"], vec!["Y"], None); + assert_eq!(assertion1.to_latex(), "X \\perp Y"); + + let assertion2 = create_assertion(vec!["X"], vec!["Y"], Some(vec!["Z"])); + assert_eq!(assertion2.to_latex(), "X \\perp Y \\mid Z"); + } + } + + #[cfg(test)] + mod test_independence_assertion_equality { + use super::*; + + #[test] + fn test_equality_basic() { + let i1 = create_assertion(vec!["a"], vec!["b"], Some(vec!["c"])); + let i2 = create_assertion(vec!["a"], vec!["b"], None); + let i3 = create_assertion(vec!["a"], vec!["b", "c", "d"], None); + + assert_ne!(i1, i2); + assert_ne!(i1, i3); + assert_ne!(i2, i3); + } + + #[test] + fn test_equality_symmetry() { + let i4 = create_assertion(vec!["a"], vec!["b", "c", "d"], Some(vec!["e"])); + let i5 = create_assertion(vec!["a"], vec!["d", "c", "b"], Some(vec!["e"])); + + // Order shouldn't matter for sets + assert_eq!(i4, i5); + } + + #[test] + fn test_equality_with_swapped_events() { + // Test symmetry: X ⊥ Y | Z should equal Y ⊥ X | Z + let i9 = create_assertion(vec!["a"], vec!["d", "k", "b"], Some(vec!["e"])); + let i10 = create_assertion(vec!["k", "b", "d"], vec!["a"], Some(vec!["e"])); + + assert_eq!(i9, i10); // Should be equal due to symmetry + } + + #[test] + fn test_inequality_different_conditioning() { + let i6 = create_assertion(vec!["a"], vec!["d", "c"], Some(vec!["e", "b"])); + let i7 = create_assertion(vec!["a"], vec!["c", "d"], Some(vec!["b", "e"])); + let i8 = create_assertion(vec!["a"], vec!["f", "d"], Some(vec!["b", "e"])); + + assert_eq!(i6, i7); // Same conditioning set, different order + assert_ne!(i7, i8); // Different variables + } + } + + #[cfg(test)] + mod test_independencies_collection { + use rust_core::Independencies; + + use super::*; + + #[test] + fn test_empty_independencies() { + let independencies = Independencies::new(); + assert_eq!(independencies.get_assertions().len(), 0); + assert_eq!(independencies.get_all_variables().len(), 0); + } + + #[test] + fn test_add_assertion() { + let mut independencies = Independencies::new(); + let assertion = create_assertion(vec!["X"], vec!["Y"], Some(vec!["Z"])); + + independencies.add_assertion(assertion.clone()); + assert_eq!(independencies.get_assertions().len(), 1); + assert!(independencies.contains(&assertion)); + } + + #[test] + fn test_get_all_variables() { + let mut independencies = Independencies::new(); + independencies.add_assertion(create_assertion( + vec!["a"], vec!["b", "c", "d"], Some(vec!["e", "f", "g"]) + )); + independencies.add_assertion(create_assertion( + vec!["c"], vec!["d", "e", "f"], Some(vec!["g", "h"]) + )); + + let expected_vars = set_from_vec(vec!["a", "b", "c", "d", "e", "f", "g", "h"]); + assert_eq!(independencies.get_all_variables(), expected_vars); + } + + #[test] + fn test_independencies_equality() { + let mut ind1 = Independencies::new(); + ind1.add_assertion(create_assertion( + vec!["a"], vec!["b", "c", "d"], Some(vec!["e", "f", "g"]) + )); + + let mut ind2 = Independencies::new(); + ind2.add_assertion(create_assertion( + vec!["a"], vec!["b", "c", "d"], Some(vec!["e", "f", "g"]) + )); + + assert_eq!(ind1, ind2); + } + } + + #[cfg(test)] + mod test_closure_and_entailment { + use rust_core::Independencies; + + use super::*; + + #[test] + fn test_simple_closure() { + let mut ind = Independencies::new(); + ind.add_assertion(create_assertion( + vec!["A"], vec!["B", "C"], Some(vec!["D"]) + )); + + let closure = ind.closure(); + let closure_assertions = closure.get_assertions(); + + // Should contain original assertion + assert!(closure_assertions.len() >= 1); + + // Should contain decompositions: A ⊥ B | D and A ⊥ C | D + let decomp1 = create_assertion(vec!["A"], vec!["B"], Some(vec!["D"])); + let decomp2 = create_assertion(vec!["A"], vec!["C"], Some(vec!["D"])); + + assert!(closure.contains(&decomp1) || + closure_assertions.iter().any(|a| a.event1 == decomp1.event1 && + a.event2 == decomp1.event2 && + a.event3 == decomp1.event3)); + } + + #[test] + fn test_entailment() { + let mut ind1 = Independencies::new(); + ind1.add_assertion(create_assertion( + vec!["W"], vec!["X", "Y", "Z"], None + )); + + let mut ind2 = Independencies::new(); + ind2.add_assertion(create_assertion(vec!["W"], vec!["X"], None)); + + // W ⊥ X,Y,Z should entail W ⊥ X + assert!(ind1.entails(&ind2)); + assert!(!ind2.entails(&ind1)); + } + + #[test] + fn test_equivalence() { + let mut ind1 = Independencies::new(); + ind1.add_assertion(create_assertion( + vec!["X"], vec!["Y", "W"], Some(vec!["Z"]) + )); + + let mut ind2 = Independencies::new(); + ind2.add_assertion(create_assertion(vec!["X"], vec!["Y"], Some(vec!["Z"]))); + ind2.add_assertion(create_assertion(vec!["X"], vec!["W"], Some(vec!["Z"]))); + + // These should be equivalent + assert!(ind1.is_equivalent(&ind2)); + } + } + + #[cfg(test)] + mod test_reduce_method { + use rust_core::Independencies; + + use super::*; + + #[test] + fn test_reduce_duplicates() { + let mut ind = Independencies::new(); + let assertion = create_assertion(vec!["X"], vec!["Y"], Some(vec!["Z"])); + + // Add the same assertion twice + ind.add_assertion(assertion.clone()); + ind.add_assertion(assertion.clone()); + + let reduced = ind.reduce(); + assert_eq!(reduced.get_assertions().len(), 1); + } + + #[test] + fn test_reduce_entailment() { + let mut ind = Independencies::new(); + + // More general assertion + ind.add_assertion(create_assertion( + vec!["W"], vec!["X", "Y", "Z"], None + )); + // More specific assertion (should be removed) + ind.add_assertion(create_assertion(vec!["W"], vec!["X"], None)); + + let reduced = ind.reduce(); + assert_eq!(reduced.get_assertions().len(), 1); + + // Should keep the more general assertion + let general = create_assertion(vec!["W"], vec!["X", "Y", "Z"], None); + assert!(reduced.contains(&general)); + } + + #[test] + fn test_reduce_independent_assertions() { + let mut ind = Independencies::new(); + ind.add_assertion(create_assertion(vec!["A"], vec!["B"], Some(vec!["C"]))); + ind.add_assertion(create_assertion(vec!["D"], vec!["E"], Some(vec!["F"]))); + + let reduced = ind.reduce(); + assert_eq!(reduced.get_assertions().len(), 2); + } + + #[test] + fn test_reduce_inplace() { + let mut ind = Independencies::new(); + let assertion = create_assertion(vec!["X"], vec!["Y"], Some(vec!["Z"])); + + ind.add_assertion(assertion.clone()); + ind.add_assertion(assertion.clone()); + ind.add_assertion(create_assertion(vec!["A"], vec!["B"], Some(vec!["C"]))); + + let original_len = ind.get_assertions().len(); + ind.reduce_inplace(); + + assert_ne!(original_len, ind.get_assertions().len()); + assert_eq!(ind.get_assertions().len(), 2); // Should have 2 unique assertions + } + + #[test] + fn test_reduce_empty() { + let ind = Independencies::new(); + let reduced = ind.reduce(); + assert_eq!(reduced.get_assertions().len(), 0); + } + + #[test] + fn test_reduce_complex_case() { + let mut ind = Independencies::new(); + + // General assertion that entails the specific ones + ind.add_assertion(create_assertion( + vec!["A"], vec!["B", "C"], Some(vec!["D"]) + )); + // Specific assertions that should be removed + ind.add_assertion(create_assertion(vec!["A"], vec!["B"], Some(vec!["D"]))); + ind.add_assertion(create_assertion(vec!["A"], vec!["C"], Some(vec!["D"]))); + // Independent assertion + ind.add_assertion(create_assertion(vec!["E"], vec!["F"], Some(vec!["G"]))); + + let reduced = ind.reduce(); + assert_eq!(reduced.get_assertions().len(), 2); + + let general = create_assertion(vec!["A"], vec!["B", "C"], Some(vec!["D"])); + let independent = create_assertion(vec!["E"], vec!["F"], Some(vec!["G"])); + + assert!(reduced.contains(&general)); + assert!(reduced.contains(&independent)); + } + } +} \ No newline at end of file From c5aabd7f3f3a3683972eff95fdabb62f75b1b99c Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 20 Jul 2025 10:34:40 +0530 Subject: [PATCH 50/62] fix closure & sg3 --- .../src/independencies/independencies.rs | 83 ++++++++++++++----- 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/rust_core/src/independencies/independencies.rs b/rust_core/src/independencies/independencies.rs index b8fae08..3a9a35d 100644 --- a/rust_core/src/independencies/independencies.rs +++ b/rust_core/src/independencies/independencies.rs @@ -152,27 +152,38 @@ impl Independencies { let mut new_inds: HashSet = self.assertions.iter().cloned().collect(); while !new_inds.is_empty() { - // Generate pairs for contraction rule - let new_pairs: Vec<(IndependenceAssertion, IndependenceAssertion)> = new_inds - .iter() - .flat_map(|ind1| { - all_independencies.iter().chain(new_inds.iter()) - .map(move |ind2| (ind1.clone(), ind2.clone())) - }) - .collect(); - + // Add current new independencies to the complete set all_independencies.extend(new_inds.iter().cloned()); let mut next_round = HashSet::new(); - // Apply axioms + // Apply decomposition and weak union to all new independencies for ind in &new_inds { next_round.extend(self.sg1_decomposition(ind)); next_round.extend(self.sg2_weak_union(ind)); } - for (ind1, ind2) in new_pairs { - next_round.extend(self.sg3_contraction(&ind1, &ind2)); + // Apply contraction rule to all pairs + // We need to check new × new, new × all, and all × new pairs + let all_current: Vec = all_independencies.iter().cloned().collect(); + let new_current: Vec = new_inds.iter().cloned().collect(); + + // new × new pairs + for i in 0..new_current.len() { + for j in i..new_current.len() { + next_round.extend(self.sg3_contraction(&new_current[i], &new_current[j])); + if i != j { + next_round.extend(self.sg3_contraction(&new_current[j], &new_current[i])); + } + } + } + + // new × all pairs + for new_ind in &new_current { + for all_ind in &all_current { + next_round.extend(self.sg3_contraction(new_ind, all_ind)); + next_round.extend(self.sg3_contraction(all_ind, new_ind)); + } } // Remove already known assertions @@ -182,6 +193,7 @@ impl Independencies { Self::from_assertions(all_independencies.into_iter().collect()) } + /// Decomposition rule: 'X ⊥ Y,W | Z' -> 'X ⊥ Y | Z', 'X ⊥ W | Z' fn sg1_decomposition(&self, ind: &IndependenceAssertion) -> Vec { @@ -263,23 +275,52 @@ impl Independencies { /// Contraction rule: 'X ⊥ W | Y,Z' & 'X ⊥ Y | Z' -> 'X ⊥ W,Y | Z' fn sg3_contraction(&self, ind1: &IndependenceAssertion, ind2: &IndependenceAssertion) -> Vec { + let mut results = Vec::new(); + + // Must have same event1 if ind1.event1 != ind2.event1 { - return vec![]; + return results; + } + + // Simple case: same conditioning set, combine the independence sets + if ind1.event3 == ind2.event3 { + let mut combined_event2 = ind1.event2.clone(); + combined_event2.extend(ind2.event2.iter().cloned()); + + // Only add if it's actually combining something new + if combined_event2 != ind1.event2 && combined_event2 != ind2.event2 { + if let Ok(assertion) = IndependenceAssertion::new( + ind1.event1.clone(), + combined_event2, + if ind1.event3.is_empty() { None } else { Some(ind1.event3.clone()) }, + ) { + results.push(assertion); + } + } } - let y = &ind2.event2; - let z = &ind2.event3; - let y_z = &ind1.event3; + // Standard contraction rule cases + results.extend(self.try_standard_contraction(ind1, ind2)); + results.extend(self.try_standard_contraction(ind2, ind1)); + + results + } + + fn try_standard_contraction(&self, larger: &IndependenceAssertion, smaller: &IndependenceAssertion) -> Vec { + let y = &smaller.event2; + let z = &smaller.event3; + let y_z = &larger.event3; - // Check if Y ⊂ Y∪Z and Z ⊂ Y∪Z and Y ∩ Z = ∅ - if y.is_subset(y_z) && z.is_subset(y_z) && y.is_disjoint(z) { - let mut new_event2 = ind1.event2.clone(); + // Check if larger condition is Y ∪ Z + let y_union_z: HashSet = y.union(z).cloned().collect(); + if y_union_z == *y_z && y.is_disjoint(z) { + let mut new_event2 = larger.event2.clone(); new_event2.extend(y.iter().cloned()); if let Ok(assertion) = IndependenceAssertion::new( - ind1.event1.clone(), + larger.event1.clone(), new_event2, - Some(z.clone()), + if z.is_empty() { None } else { Some(z.clone()) }, ) { return vec![assertion]; } From cc731b904235da3914a3a0115cb6327e47459ec9 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 20 Jul 2025 14:08:10 +0530 Subject: [PATCH 51/62] add comments --- .../src/independencies/independencies.rs | 41 ++++++++++++------- rust_core/tests/independence_tests.rs | 19 ++++++++- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/rust_core/src/independencies/independencies.rs b/rust_core/src/independencies/independencies.rs index 3a9a35d..3516bc7 100644 --- a/rust_core/src/independencies/independencies.rs +++ b/rust_core/src/independencies/independencies.rs @@ -151,19 +151,24 @@ impl Independencies { let mut all_independencies: HashSet = HashSet::new(); let mut new_inds: HashSet = self.assertions.iter().cloned().collect(); + // Example: Start with {A ⊥ B,C | D} + // Iteration 1: SG1 generates {A ⊥ B | D, A ⊥ C | D} + // Iteration 2: SG2 generates {A ⊥ B | C,D, A ⊥ C | B,D} + // Iteration 3: SG3 might combine pairs, continues until no new assertions while !new_inds.is_empty() { // Add current new independencies to the complete set all_independencies.extend(new_inds.iter().cloned()); let mut next_round = HashSet::new(); - // Apply decomposition and weak union to all new independencies + // Apply unary axioms (SG1, SG2) to each new assertion individually for ind in &new_inds { next_round.extend(self.sg1_decomposition(ind)); next_round.extend(self.sg2_weak_union(ind)); } - // Apply contraction rule to all pairs + // Apply binary axiom (SG3) to all pairs - this is the expensive O(n²) part + // Example: {A ⊥ B | D} + {A ⊥ C | D} → {A ⊥ B,C | D} via contraction // We need to check new × new, new × all, and all × new pairs let all_current: Vec = all_independencies.iter().cloned().collect(); let new_current: Vec = new_inds.iter().cloned().collect(); @@ -193,7 +198,6 @@ impl Independencies { Self::from_assertions(all_independencies.into_iter().collect()) } - /// Decomposition rule: 'X ⊥ Y,W | Z' -> 'X ⊥ Y | Z', 'X ⊥ W | Z' fn sg1_decomposition(&self, ind: &IndependenceAssertion) -> Vec { @@ -201,9 +205,9 @@ impl Independencies { return vec![]; } - let mut results = Vec::new(); + let mut results: Vec = Vec::new(); for elem in &ind.event2 { - let mut new_event2 = ind.event2.clone(); + let mut new_event2: HashSet = ind.event2.clone(); new_event2.remove(elem); if let Ok(assertion) = IndependenceAssertion::new( @@ -307,11 +311,13 @@ impl Independencies { } fn try_standard_contraction(&self, larger: &IndependenceAssertion, smaller: &IndependenceAssertion) -> Vec { - let y = &smaller.event2; - let z = &smaller.event3; - let y_z = &larger.event3; + let y = &smaller.event2; // Variables we're independent from in smaller assertion + let z = &smaller.event3; // What we condition on in smaller assertion + let y_z = &larger.event3; // What we condition on in larger assertion - // Check if larger condition is Y ∪ Z + // Check if larger conditions on exactly Y∪Z and Y∩Z = ∅ + // Example: smaller = {A ⊥ C | D}, larger = {A ⊥ B | C,D} + // Here: y={C}, z={D}, y_z={C,D}, y∪z={C,D} ✓, y∩z=∅ ✓ let y_union_z: HashSet = y.union(z).cloned().collect(); if y_union_z == *y_z && y.is_disjoint(z) { let mut new_event2 = larger.event2.clone(); @@ -337,10 +343,11 @@ impl Independencies { let mut reduced_assertions = Vec::new(); for assertion in unique_assertions { - let temp_independencies = Self::from_assertions(reduced_assertions.clone()); - let assertion_temp = Self::from_assertions(vec![assertion.clone()]); + let temp_independencies: Independencies = Self::from_assertions(reduced_assertions.clone()); + let new_assertion: Independencies = Self::from_assertions(vec![assertion.clone()]); - if !temp_independencies.entails(&assertion_temp) { + // Check if the current new assertion is already entailed by the reduced set, meaning if it doesn't add new information + if !temp_independencies.entails(&new_assertion) { let mut removed_any = true; while removed_any { removed_any = false; @@ -348,9 +355,13 @@ impl Independencies { while i < reduced_assertions.len() { let existing_temp = Self::from_assertions(vec![reduced_assertions[i].clone()]); - if existing_temp != assertion_temp { - let remove_old = !existing_temp.entails(&assertion_temp) && - assertion_temp.entails(&existing_temp); + if existing_temp != new_assertion { + // Old assertion: A ⊥ B | D (existing in reduced_assertions) & New assertion: A ⊥ B,C | D (being added) + // Does A ⊥ B | D entail A ⊥ B,C | D? NO (specific doesn't imply general) + // Does A ⊥ B,C | D entail A ⊥ B | D? YES (via SG1 decomposition) + // Result: Remove the old assertion since the new one contains all its information plus more + let remove_old = !existing_temp.entails(&new_assertion) && + new_assertion.entails(&existing_temp); if remove_old { reduced_assertions.remove(i); diff --git a/rust_core/tests/independence_tests.rs b/rust_core/tests/independence_tests.rs index 4ea6394..3b71826 100644 --- a/rust_core/tests/independence_tests.rs +++ b/rust_core/tests/independence_tests.rs @@ -241,6 +241,21 @@ mod independence_tests { a.event3 == decomp1.event3)); } + #[test] + fn test_complex_closure() { + let mut ind = Independencies::new(); + ind.add_assertion(create_assertion( + vec!["A"], vec!["B", "C", "D"], Some(vec!["E"]) + )); + + let closure = ind.closure(); + let closure_assertions= closure.get_assertions(); + + assert!(closure_assertions.len() == 19); + } + + + #[test] fn test_entailment() { let mut ind1 = Independencies::new(); @@ -312,11 +327,11 @@ mod independence_tests { #[test] fn test_reduce_independent_assertions() { - let mut ind = Independencies::new(); + let mut ind: Independencies = Independencies::new(); ind.add_assertion(create_assertion(vec!["A"], vec!["B"], Some(vec!["C"]))); ind.add_assertion(create_assertion(vec!["D"], vec!["E"], Some(vec!["F"]))); - let reduced = ind.reduce(); + let reduced: Independencies = ind.reduce(); assert_eq!(reduced.get_assertions().len(), 2); } From d4cd189ece9ad063c0c4ee058eed3a9e265006a0 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 20 Jul 2025 16:43:18 +0530 Subject: [PATCH 52/62] fix(mac build) reduce method: sort assertions on event2 to prevent non generic to be filled first --- .../src/independencies/independencies.rs | 70 +++++++------------ 1 file changed, 27 insertions(+), 43 deletions(-) diff --git a/rust_core/src/independencies/independencies.rs b/rust_core/src/independencies/independencies.rs index 3516bc7..df8d426 100644 --- a/rust_core/src/independencies/independencies.rs +++ b/rust_core/src/independencies/independencies.rs @@ -9,23 +9,6 @@ pub struct IndependenceAssertion { pub all_vars: HashSet, } -impl Hash for IndependenceAssertion { - fn hash(&self, state: &mut H) { - // Convert HashSets to BTreeSets for deterministic, sortable representation - let event1_ordered: BTreeSet<_> = self.event1.iter().collect(); - let event2_ordered: BTreeSet<_> = self.event2.iter().collect(); - let event3_ordered: BTreeSet<_> = self.event3.iter().collect(); - - // Create symmetric hash: X ⊥ Y | Z should equal Y ⊥ X | Z - let mut symmetric_pair: Vec> = vec![event1_ordered, event2_ordered]; - symmetric_pair.sort(); // BTreeSet implements Ord, so this works - - // Hash the components - symmetric_pair.hash(state); // Symmetric part - event3_ordered.hash(state); // Conditioning set - } -} - impl IndependenceAssertion { pub fn new( event1: HashSet, @@ -85,6 +68,23 @@ impl std::fmt::Display for IndependenceAssertion { } } +impl Hash for IndependenceAssertion { + fn hash(&self, state: &mut H) { + // Convert HashSets to BTreeSets for deterministic, sortable representation + let event1_ordered: BTreeSet<_> = self.event1.iter().collect(); + let event2_ordered: BTreeSet<_> = self.event2.iter().collect(); + let event3_ordered: BTreeSet<_> = self.event3.iter().collect(); + + // Create symmetric hash: X ⊥ Y | Z should equal Y ⊥ X | Z + let mut symmetric_pair: Vec> = vec![event1_ordered, event2_ordered]; + symmetric_pair.sort(); // BTreeSet implements Ord, so this works + + // Hash the components + symmetric_pair.hash(state); // Symmetric part + event3_ordered.hash(state); // Conditioning set + } +} + impl PartialEq for IndependenceAssertion { fn eq(&self, other: &Self) -> bool { // Check if event3 (conditioning set) is the same @@ -337,41 +337,25 @@ impl Independencies { pub fn reduce(&self) -> Self { let mut unique_assertions: Vec = - self.assertions.iter().cloned().collect::>() + self.assertions.iter().cloned().collect::>() .into_iter().collect(); + // Sort by event2 size (descending) to process more general assertions first + // unique_assertions.sort_by(|a, b| b.event2.len().cmp(&a.event2.len())); + let mut reduced_assertions = Vec::new(); for assertion in unique_assertions { let temp_independencies: Independencies = Self::from_assertions(reduced_assertions.clone()); let new_assertion: Independencies = Self::from_assertions(vec![assertion.clone()]); - // Check if the current new assertion is already entailed by the reduced set, meaning if it doesn't add new information + // Only add if not entailed by current reduced set if !temp_independencies.entails(&new_assertion) { - let mut removed_any = true; - while removed_any { - removed_any = false; - let mut i = 0; - while i < reduced_assertions.len() { - let existing_temp = Self::from_assertions(vec![reduced_assertions[i].clone()]); - - if existing_temp != new_assertion { - // Old assertion: A ⊥ B | D (existing in reduced_assertions) & New assertion: A ⊥ B,C | D (being added) - // Does A ⊥ B | D entail A ⊥ B,C | D? NO (specific doesn't imply general) - // Does A ⊥ B,C | D entail A ⊥ B | D? YES (via SG1 decomposition) - // Result: Remove the old assertion since the new one contains all its information plus more - let remove_old = !existing_temp.entails(&new_assertion) && - new_assertion.entails(&existing_temp); - - if remove_old { - reduced_assertions.remove(i); - removed_any = true; - break; - } - } - i += 1; - } - } + // Remove any existing assertions that are entailed by the new assertion + reduced_assertions.retain(|existing| { + let existing_temp = Self::from_assertions(vec![existing.clone()]); + !new_assertion.entails(&existing_temp) + }); reduced_assertions.push(assertion); } } From 01436c99460160c70aa1c875d454298451392745 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 20 Jul 2025 16:54:53 +0530 Subject: [PATCH 53/62] add sort e2 func --- rust_core/src/independencies/independencies.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust_core/src/independencies/independencies.rs b/rust_core/src/independencies/independencies.rs index df8d426..066089b 100644 --- a/rust_core/src/independencies/independencies.rs +++ b/rust_core/src/independencies/independencies.rs @@ -341,7 +341,7 @@ impl Independencies { .into_iter().collect(); // Sort by event2 size (descending) to process more general assertions first - // unique_assertions.sort_by(|a, b| b.event2.len().cmp(&a.event2.len())); + unique_assertions.sort_by(|a, b| b.event2.len().cmp(&a.event2.len())); let mut reduced_assertions = Vec::new(); From 323836e72694e835b53674d7fdd0804a6c689a19 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 20 Jul 2025 17:14:56 +0530 Subject: [PATCH 54/62] add comments --- rust_core/src/dag.rs | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/rust_core/src/dag.rs b/rust_core/src/dag.rs index d5d16e7..7bbe0cf 100644 --- a/rust_core/src/dag.rs +++ b/rust_core/src/dag.rs @@ -145,10 +145,15 @@ impl RustDAG { pub fn active_trail_nodes(&self, variables: Vec, observed: Option>, include_latents: bool) -> Result>, String> { let observed_list: HashSet = observed.unwrap_or_default().into_iter().collect(); + // Precompute ancestors of observed nodes (needed for collider rule) + // Example: If C is observed in A→B←C→D, ancestors_list = {A, B, C} let ancestors_list: HashSet = self.get_ancestors_of(observed_list.iter().cloned().collect())?; let mut active_trails: HashMap> = HashMap::new(); + // For each starting variable, find all nodes reachable via active trails for start in variables { + // BFS with direction tracking: (node, direction_of_arrival) + // "up" = coming from child toward parents, "down" = coming from parent toward children let mut visit_list: HashSet<(String, &str)> = HashSet::new(); let mut traversed_list: HashSet<(String, &str)> = HashSet::new(); let mut active_nodes: HashSet = HashSet::new(); @@ -161,19 +166,24 @@ impl RustDAG { while let Some((node, direction)) = visit_list.iter().next().map(|x| x.clone()) { visit_list.remove(&(node.clone(), direction)); if !traversed_list.contains(&(node.clone(), direction)) { + // Add to active trail if not observed (observed nodes block but aren't "reachable") if !observed_list.contains(&node) { active_nodes.insert(node.clone()); } traversed_list.insert((node.clone(), direction)); + // If arriving "up" at unobserved B, can continue to parents and switch to children if direction == "up" && !observed_list.contains(&node) { for parent in self.get_parents(&node)? { - visit_list.insert((parent, "up")); + visit_list.insert((parent, "up")); // Continue up the chain } for child in self.get_children(&node)? { - visit_list.insert((child, "down")); + visit_list.insert((child, "down")); // Switch direction } - } else if direction == "down" { + } + + // If arriving "down", can continue down if unobserved, or go up if it's a collider + else if direction == "down" { if !observed_list.contains(&node) { for child in self.get_children(&node)? { visit_list.insert((child, "down")); @@ -210,18 +220,25 @@ impl RustDAG { end: &str, include_latents: bool ) -> Result>, String> { + // Example: For DAG A→B←C, B→D, trying to separate A and C + // Adjacent nodes can't be separated by any conditioning set if self.has_edge(start, end) || self.has_edge(end, start) { return Err("No possible separators because start and end are adjacent".to_string()); } - // Create proper ancestral graph + // Create ancestral graph containing only ancestors of start and end + // Example: For separating A and D in A→B←C, B→D, ancestral graph = {A, B, C, D} let ancestral_graph = self.get_ancestral_graph(vec![start.to_string(), end.to_string()])?; + // Initial separator: all parents of both nodes (theoretical upper bound) + // Example: parents(A)={} ∪ parents(D)={B} → separator = {B} let mut separator: HashSet = self.get_parents(start)? .into_iter() .chain(self.get_parents(end)?.into_iter()) .collect(); + // Replace latent variables with their observable parents + // Example: If B were latent with parent L, replace B with L in separator if !include_latents { let mut changed = true; while changed { @@ -243,11 +260,13 @@ impl RustDAG { separator.remove(start); separator.remove(end); - // If the initial set is not able to d-separate, no d-separator is possible. + // Sanity check: if our "guaranteed" separator doesn't work, no separator exists if ancestral_graph.is_dconnected(start, end, Some(separator.iter().cloned().collect()), include_latents)? { return Ok(None); } + // Greedy minimization: remove each node if separation still holds without it + // Example: If separator = {B, C} but {B} alone separates A from D, remove C let mut minimal_separator = separator.clone(); for u in separator { let test_separator: Vec = minimal_separator.iter().cloned().filter(|x| x != &u).collect(); From f246b8ce2addb1545e09136593e0ce5934a81fcb Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 20 Jul 2025 20:52:26 +0530 Subject: [PATCH 55/62] add more tests --- .../src/independencies/independencies.rs | 24 +-- rust_core/tests/independence_tests.rs | 186 ++++++++++++++++-- 2 files changed, 172 insertions(+), 38 deletions(-) diff --git a/rust_core/src/independencies/independencies.rs b/rust_core/src/independencies/independencies.rs index 066089b..1e1e6ad 100644 --- a/rust_core/src/independencies/independencies.rs +++ b/rust_core/src/independencies/independencies.rs @@ -281,29 +281,7 @@ impl Independencies { fn sg3_contraction(&self, ind1: &IndependenceAssertion, ind2: &IndependenceAssertion) -> Vec { let mut results = Vec::new(); - // Must have same event1 - if ind1.event1 != ind2.event1 { - return results; - } - - // Simple case: same conditioning set, combine the independence sets - if ind1.event3 == ind2.event3 { - let mut combined_event2 = ind1.event2.clone(); - combined_event2.extend(ind2.event2.iter().cloned()); - - // Only add if it's actually combining something new - if combined_event2 != ind1.event2 && combined_event2 != ind2.event2 { - if let Ok(assertion) = IndependenceAssertion::new( - ind1.event1.clone(), - combined_event2, - if ind1.event3.is_empty() { None } else { Some(ind1.event3.clone()) }, - ) { - results.push(assertion); - } - } - } - - // Standard contraction rule cases + // Standard contraction rule cases (try both directions) results.extend(self.try_standard_contraction(ind1, ind2)); results.extend(self.try_standard_contraction(ind2, ind1)); diff --git a/rust_core/tests/independence_tests.rs b/rust_core/tests/independence_tests.rs index 3b71826..1400666 100644 --- a/rust_core/tests/independence_tests.rs +++ b/rust_core/tests/independence_tests.rs @@ -270,21 +270,6 @@ mod independence_tests { assert!(ind1.entails(&ind2)); assert!(!ind2.entails(&ind1)); } - - #[test] - fn test_equivalence() { - let mut ind1 = Independencies::new(); - ind1.add_assertion(create_assertion( - vec!["X"], vec!["Y", "W"], Some(vec!["Z"]) - )); - - let mut ind2 = Independencies::new(); - ind2.add_assertion(create_assertion(vec!["X"], vec!["Y"], Some(vec!["Z"]))); - ind2.add_assertion(create_assertion(vec!["X"], vec!["W"], Some(vec!["Z"]))); - - // These should be equivalent - assert!(ind1.is_equivalent(&ind2)); - } } #[cfg(test)] @@ -382,4 +367,175 @@ mod independence_tests { assert!(reduced.contains(&independent)); } } + + #[cfg(test)] + mod test_complex_pgmpy_scenarios { + use super::*; + use rust_core::Independencies; + + // Helper to create complex Independencies matching PGMPY patterns + fn create_independencies_3() -> Independencies { + let mut ind = Independencies::new(); + ind.add_assertions_from_tuples(vec![ + (vec!["a".to_string()], vec!["b".to_string(), "c".to_string(), "d".to_string()], Some(vec!["e".to_string(), "f".to_string(), "g".to_string()])), + (vec!["c".to_string()], vec!["d".to_string(), "e".to_string(), "f".to_string()], Some(vec!["g".to_string(), "h".to_string()])) + ]).unwrap(); + ind + } + + fn create_independencies_4() -> Independencies { + let mut ind = Independencies::new(); + ind.add_assertions_from_tuples(vec![ + (vec!["f".to_string(), "d".to_string(), "e".to_string()], vec!["c".to_string()], Some(vec!["h".to_string(), "g".to_string()])), + (vec!["b".to_string(), "c".to_string(), "d".to_string()], vec!["a".to_string()], Some(vec!["f".to_string(), "g".to_string(), "e".to_string()])) + ]).unwrap(); + ind + } + + fn create_independencies_5() -> Independencies { + let mut ind = Independencies::new(); + ind.add_assertions_from_tuples(vec![ + (vec!["a".to_string()], vec!["b".to_string(), "c".to_string(), "d".to_string()], Some(vec!["e".to_string(), "f".to_string(), "g".to_string()])), + (vec!["c".to_string()], vec!["d".to_string(), "e".to_string(), "f".to_string()], Some(vec!["g".to_string()])) + ]).unwrap(); + ind + } + + #[test] + fn test_complex_multi_assertion_equality() { + // This tests the complex scenario from PGMPY setUp method + let ind3 = create_independencies_3(); + let ind4 = create_independencies_4(); + let ind5 = create_independencies_5(); + + // These should be equal due to symmetric equivalence + assert_eq!(ind3, ind4, "Independencies3 and Independencies4 should be equal"); + + // These should not be equal + assert_ne!(ind3, ind5, "Independencies3 and Independencies5 should not be equal"); + assert_ne!(ind4, ind5, "Independencies4 and Independencies5 should not be equal"); + } + + #[test] + fn test_pgmpy_complex_equivalence_scenarios() { + // Test case 1: ind1 vs ind2 (should NOT be equivalent) + let mut ind1 = Independencies::new(); + ind1.add_assertions_from_tuples(vec![ + (vec!["X".to_string()], vec!["Y".to_string(), "W".to_string()], Some(vec!["Z".to_string()])) + ]).unwrap(); + + let mut ind2 = Independencies::new(); + ind2.add_assertions_from_tuples(vec![ + (vec!["X".to_string()], vec!["Y".to_string()], Some(vec!["Z".to_string()])), + (vec!["X".to_string()], vec!["W".to_string()], Some(vec!["Z".to_string()])) + ]).unwrap(); + + // This should be FALSE - ind1 should NOT be equivalent to ind2 + assert!(!ind1.is_equivalent(&ind2), "ind1 should NOT be equivalent to ind2"); + + // Test case 2: ind1 vs ind3 (should be equivalent) + let mut ind3 = Independencies::new(); + ind3.add_assertions_from_tuples(vec![ + (vec!["X".to_string()], vec!["Y".to_string()], Some(vec!["Z".to_string()])), + (vec!["X".to_string()], vec!["W".to_string()], Some(vec!["Z".to_string()])), + (vec!["X".to_string()], vec!["Y".to_string()], Some(vec!["W".to_string(), "Z".to_string()])) + ]).unwrap(); + + // This should be TRUE - ind1 should be equivalent to ind3 + assert!(ind1.is_equivalent(&ind3), "ind1 should be equivalent to ind3"); + } + + #[test] + fn test_comprehensive_equality_edge_cases() { + let empty_ind = Independencies::new(); + + let mut non_empty_ind = Independencies::new(); + non_empty_ind.add_assertions_from_tuples(vec![ + (vec!["A".to_string()], vec!["B".to_string()], Some(vec!["C".to_string()])) + ]).unwrap(); + + // Empty vs non-empty should be false + assert_ne!(empty_ind, non_empty_ind, "Empty and non-empty independencies should not be equal"); + + // Non-empty vs empty should be false + assert_ne!(non_empty_ind, empty_ind, "Non-empty and empty independencies should not be equal"); + + // Empty vs empty should be true + let another_empty = Independencies::new(); + assert_eq!(empty_ind, another_empty, "Two empty independencies should be equal"); + + // Test inequality operator consistency + assert!(empty_ind != non_empty_ind, "Inequality operator should work"); + assert!(!(empty_ind != another_empty), "Double negative inequality should work"); + } + + #[test] + fn test_complex_symmetric_equivalence() { + // Create complex assertions that test symmetry at multiple levels + let mut ind_a = Independencies::new(); + ind_a.add_assertions_from_tuples(vec![ + (vec!["X".to_string(), "Y".to_string()], vec!["A".to_string(), "B".to_string()], Some(vec!["Z".to_string()])), + (vec!["P".to_string()], vec!["Q".to_string(), "R".to_string(), "S".to_string()], Some(vec!["T".to_string(), "U".to_string()])) + ]).unwrap(); + + let mut ind_b = Independencies::new(); + ind_b.add_assertions_from_tuples(vec![ + (vec!["A".to_string(), "B".to_string()], vec!["X".to_string(), "Y".to_string()], Some(vec!["Z".to_string()])), + (vec!["P".to_string()], vec!["S".to_string(), "Q".to_string(), "R".to_string()], Some(vec!["U".to_string(), "T".to_string()])) + ]).unwrap(); + + // These should be equal due to symmetric equivalence and set ordering + assert_eq!(ind_a, ind_b, "Symmetric complex independencies should be equal"); + } + + #[test] + fn test_pgmpy_setup_variable_extraction() { + // Test the get_all_variables method with complex scenarios + let ind3 = create_independencies_3(); + let ind4 = create_independencies_4(); + let ind5 = create_independencies_5(); + + let vars3 = ind3.get_all_variables(); + let vars4 = ind4.get_all_variables(); + let vars5 = ind5.get_all_variables(); + + // All should contain the same variables + let expected_vars: HashSet = set_from_vec(vec!["a", "b", "c", "d", "e", "f", "g", "h"]); + assert_eq!(vars3, expected_vars, "Independencies3 should have all expected variables"); + assert_eq!(vars4, expected_vars, "Independencies4 should have all expected variables"); + + let expected_vars5: HashSet = set_from_vec(vec!["a", "b", "c", "d", "e", "f", "g"]); + assert_eq!(vars5, expected_vars5, "Independencies5 should have subset of variables"); + } + + #[test] + fn test_bidirectional_equivalence_complex() { + // Test that equivalence is truly bidirectional in complex scenarios + let mut ind_x = Independencies::new(); + ind_x.add_assertions_from_tuples(vec![ + (vec!["A".to_string()], vec!["B".to_string(), "C".to_string()], Some(vec!["D".to_string()])), + (vec!["E".to_string()], vec!["F".to_string()], Some(vec!["G".to_string(), "H".to_string()])) + ]).unwrap(); + + let mut ind_y = Independencies::new(); + ind_y.add_assertions_from_tuples(vec![ + (vec!["A".to_string()], vec!["B".to_string()], Some(vec!["D".to_string()])), + (vec!["A".to_string()], vec!["C".to_string()], Some(vec!["D".to_string()])), + (vec!["E".to_string()], vec!["F".to_string()], Some(vec!["G".to_string(), "H".to_string()])) + ]).unwrap(); + + // Test that decomposition creates equivalence + assert!(ind_x.entails(&ind_y), "Complex independencies should entail their decomposition"); + + // But decomposition might not entail the original (depending on axioms) + let reverse_entailment = ind_y.entails(&ind_x); + + // If they entail each other, they should be equivalent + if reverse_entailment { + assert!(ind_x.is_equivalent(&ind_y), "Bidirectional entailment should mean equivalence"); + assert!(ind_y.is_equivalent(&ind_x), "Equivalence should be symmetric"); + } + } + } + } \ No newline at end of file From f3362393c30bbf19f84b87667df56900e0367453 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Thu, 24 Jul 2025 23:01:37 +0530 Subject: [PATCH 56/62] Fix bogus assertion generations + add large test --- .../src/independencies/independencies.rs | 234 +++++++++++------- rust_core/tests/independence_tests.rs | 21 ++ 2 files changed, 161 insertions(+), 94 deletions(-) diff --git a/rust_core/src/independencies/independencies.rs b/rust_core/src/independencies/independencies.rs index 1e1e6ad..d46ad79 100644 --- a/rust_core/src/independencies/independencies.rs +++ b/rust_core/src/independencies/independencies.rs @@ -98,7 +98,6 @@ impl PartialEq for IndependenceAssertion { } } - #[derive(Debug, Clone)] pub struct Independencies { assertions: Vec, @@ -150,86 +149,103 @@ impl Independencies { pub fn closure(&self) -> Self { let mut all_independencies: HashSet = HashSet::new(); let mut new_inds: HashSet = self.assertions.iter().cloned().collect(); - + // Example: Start with {A ⊥ B,C | D} // Iteration 1: SG1 generates {A ⊥ B | D, A ⊥ C | D} // Iteration 2: SG2 generates {A ⊥ B | C,D, A ⊥ C | B,D} // Iteration 3: SG3 might combine pairs, continues until no new assertions while !new_inds.is_empty() { - // Add current new independencies to the complete set - all_independencies.extend(new_inds.iter().cloned()); - + // Do Collect pairs for SG3 before modifying all_independencies + let mut new_pairs: HashSet<(&IndependenceAssertion, &IndependenceAssertion)> = HashSet::new(); + let all_current: Vec = all_independencies.iter().cloned().collect(); + let new_current: Vec = new_inds.iter().cloned().collect(); let mut next_round = HashSet::new(); - // Apply unary axioms (SG1, SG2) to each new assertion individually + // Apply unary axioms (SG1, SG2) to each new assertion for ind in &new_inds { next_round.extend(self.sg1_decomposition(ind)); next_round.extend(self.sg2_weak_union(ind)); } - - // Apply binary axiom (SG3) to all pairs - this is the expensive O(n²) part + + // Apply binary axiom (SG3) to all pairs // Example: {A ⊥ B | D} + {A ⊥ C | D} → {A ⊥ B,C | D} via contraction - // We need to check new × new, new × all, and all × new pairs - let all_current: Vec = all_independencies.iter().cloned().collect(); - let new_current: Vec = new_inds.iter().cloned().collect(); + // We need to check new × new, new × all // new × new pairs for i in 0..new_current.len() { - for j in i..new_current.len() { - next_round.extend(self.sg3_contraction(&new_current[i], &new_current[j])); - if i != j { - next_round.extend(self.sg3_contraction(&new_current[j], &new_current[i])); - } + for j in (i+1)..new_current.len() { + new_pairs.insert((&new_current[i], &new_current[j])); + new_pairs.insert((&new_current[j], &new_current[i])); } } - + // new × all pairs for new_ind in &new_current { for all_ind in &all_current { - next_round.extend(self.sg3_contraction(new_ind, all_ind)); - next_round.extend(self.sg3_contraction(all_ind, new_ind)); + new_pairs.insert((new_ind, all_ind)); + new_pairs.insert((all_ind, new_ind)); } } + // Add current new independencies to the complete set + all_independencies.extend(new_inds.iter().cloned()); + + // Apply the Binary axiom + for (ind1, ind2) in new_pairs { + next_round.extend(self.sg3_contraction(ind1, ind2)); + } + // Remove already known assertions - next_round.retain(|ind| !all_independencies.contains(ind)); + // After applying axioms + next_round.retain(|ind| !ind.event1.is_empty() && !ind.event2.is_empty() && !all_independencies.contains(ind)); new_inds = next_round; } Self::from_assertions(all_independencies.into_iter().collect()) } + + + fn sg0(&self, ind: &IndependenceAssertion) -> IndependenceAssertion { + IndependenceAssertion::new( + ind.event2.clone(), + ind.event1.clone(), + if ind.event3.is_empty() { None } else { Some(ind.event3.clone()) }, + ).unwrap() // Assuming it succeeds + } /// Decomposition rule: 'X ⊥ Y,W | Z' -> 'X ⊥ Y | Z', 'X ⊥ W | Z' fn sg1_decomposition(&self, ind: &IndependenceAssertion) -> Vec { - if ind.event2.len() <= 1 { - return vec![]; - } + let mut results = Vec::new(); - let mut results: Vec = Vec::new(); - for elem in &ind.event2 { - let mut new_event2: HashSet = ind.event2.clone(); - new_event2.remove(elem); - - if let Ok(assertion) = IndependenceAssertion::new( - ind.event1.clone(), - new_event2, - Some(ind.event3.clone()), - ) { - results.push(assertion); + // Decompose event2 if it has multiple elements + if ind.event2.len() > 1 { + for elem in &ind.event2 { + // Create single-element set for this variable + let single_var: HashSet = [elem.clone()].into_iter().collect(); + + if let Ok(assertion) = IndependenceAssertion::new( + ind.event1.clone(), + single_var, + Some(ind.event3.clone()), + ) { + results.push(assertion); + } } } - // Apply symmetry - for elem in &ind.event1 { - let mut new_event1 = ind.event1.clone(); - new_event1.remove(elem); - - if let Ok(assertion) = IndependenceAssertion::new( - new_event1, - ind.event2.clone(), - Some(ind.event3.clone()), - ) { - results.push(assertion); + // Decompose event1 if it has multiple elements (symmetry) + if ind.event1.len() > 1 { + for elem in &ind.event1 { + // Create single-element set for this variable + let single_var: HashSet = [elem.clone()].into_iter().collect(); + + if let Ok(assertion) = IndependenceAssertion::new( + single_var, + ind.event2.clone(), + Some(ind.event3.clone()), + ) { + results.push(assertion); + } } } @@ -238,39 +254,41 @@ impl Independencies { /// Weak Union rule: 'X ⊥ Y,W | Z' -> 'X ⊥ Y | W,Z', 'X ⊥ W | Y,Z' fn sg2_weak_union(&self, ind: &IndependenceAssertion) -> Vec { - if ind.event2.len() <= 1 { - return vec![]; - } - let mut results = Vec::new(); - for elem in &ind.event2 { - let mut new_event2 = ind.event2.clone(); - new_event2.remove(elem); - let mut new_event3 = ind.event3.clone(); - new_event3.insert(elem.clone()); - - if let Ok(assertion) = IndependenceAssertion::new( - ind.event1.clone(), - new_event2, - Some(new_event3), - ) { - results.push(assertion); + + // For each variable in event2, move it to the conditioning set + if ind.event2.len() > 1 { + for elem in &ind.event2 { + let mut new_event2 = ind.event2.clone(); + new_event2.remove(elem); + let mut new_event3 = ind.event3.clone(); + new_event3.insert(elem.clone()); + + if let Ok(assertion) = IndependenceAssertion::new( + ind.event1.clone(), + new_event2, + Some(new_event3), + ) { + results.push(assertion); + } } } - // Apply symmetry for event1 - for elem in &ind.event1 { - let mut new_event1 = ind.event1.clone(); - new_event1.remove(elem); - let mut new_event3 = ind.event3.clone(); - new_event3.insert(elem.clone()); - - if let Ok(assertion) = IndependenceAssertion::new( - new_event1, - ind.event2.clone(), - Some(new_event3), - ) { - results.push(assertion); + // For each variable in event1, move it to the conditioning set (symmetry) + if ind.event1.len() > 1 { + for elem in &ind.event1 { + let mut new_event1 = ind.event1.clone(); + new_event1.remove(elem); + let mut new_event3 = ind.event3.clone(); + new_event3.insert(elem.clone()); + + if let Ok(assertion) = IndependenceAssertion::new( + new_event1, + ind.event2.clone(), + Some(new_event3), + ) { + results.push(assertion); + } } } @@ -280,24 +298,50 @@ impl Independencies { /// Contraction rule: 'X ⊥ W | Y,Z' & 'X ⊥ Y | Z' -> 'X ⊥ W,Y | Z' fn sg3_contraction(&self, ind1: &IndependenceAssertion, ind2: &IndependenceAssertion) -> Vec { let mut results = Vec::new(); - - // Standard contraction rule cases (try both directions) - results.extend(self.try_standard_contraction(ind1, ind2)); - results.extend(self.try_standard_contraction(ind2, ind1)); - - results + + // Original pair + results.extend(self.try_contraction_all_sym(ind1, ind2)); + + // ind1 original, ind2 symmetric + let ind2_sym = self.sg0(ind2); + results.extend(self.try_contraction_all_sym(ind1, &ind2_sym)); + + // ind1 symmetric, ind2 original + let ind1_sym = self.sg0(ind1); + results.extend(self.try_contraction_all_sym(&ind1_sym, ind2)); + + // Both symmetric + results.extend(self.try_contraction_all_sym(&ind1_sym, &ind2_sym)); + + let unique_results: HashSet = results.into_iter().collect(); + unique_results.into_iter().collect() } - fn try_standard_contraction(&self, larger: &IndependenceAssertion, smaller: &IndependenceAssertion) -> Vec { - let y = &smaller.event2; // Variables we're independent from in smaller assertion - let z = &smaller.event3; // What we condition on in smaller assertion - let y_z = &larger.event3; // What we condition on in larger assertion + fn try_contraction_all_sym(&self, larger: &IndependenceAssertion, smaller: &IndependenceAssertion) -> Vec { + let mut res = Vec::new(); + if let Some(contracted) = self.try_contraction(larger, smaller) { + res.push(contracted); + } + if let Some(contracted) = self.try_contraction(smaller, larger) { // Try both orders + res.push(contracted); + } + res + } + + fn try_contraction(&self, larger: &IndependenceAssertion, smaller: &IndependenceAssertion) -> Option { + if larger.event1 != smaller.event1 { + return None; + } + + let y: &HashSet = &smaller.event2; + let z: &HashSet = &smaller.event3; + let y_z: &HashSet = &larger.event3; - // Check if larger conditions on exactly Y∪Z and Y∩Z = ∅ - // Example: smaller = {A ⊥ C | D}, larger = {A ⊥ B | C,D} - // Here: y={C}, z={D}, y_z={C,D}, y∪z={C,D} ✓, y∩z=∅ ✓ - let y_union_z: HashSet = y.union(z).cloned().collect(); - if y_union_z == *y_z && y.is_disjoint(z) { + // Use proper subset: subset and not equal + if y.is_subset(y_z) && *y != *y_z && + z.is_subset(y_z) && *z != *y_z && + y.is_disjoint(z) && !y.is_empty() && !z.is_empty() { + // Create new assertion: X ⊥ W,Y | Z let mut new_event2 = larger.event2.clone(); new_event2.extend(y.iter().cloned()); @@ -306,13 +350,15 @@ impl Independencies { new_event2, if z.is_empty() { None } else { Some(z.clone()) }, ) { - return vec![assertion]; + return Some(assertion); } } - vec![] + None } + + pub fn reduce(&self) -> Self { let mut unique_assertions: Vec = self.assertions.iter().cloned().collect::>() @@ -321,7 +367,7 @@ impl Independencies { // Sort by event2 size (descending) to process more general assertions first unique_assertions.sort_by(|a, b| b.event2.len().cmp(&a.event2.len())); - let mut reduced_assertions = Vec::new(); + let mut reduced_assertions: Vec = Vec::new(); for assertion in unique_assertions { let temp_independencies: Independencies = Self::from_assertions(reduced_assertions.clone()); @@ -330,7 +376,7 @@ impl Independencies { // Only add if not entailed by current reduced set if !temp_independencies.entails(&new_assertion) { // Remove any existing assertions that are entailed by the new assertion - reduced_assertions.retain(|existing| { + reduced_assertions.retain(|existing: &IndependenceAssertion| { let existing_temp = Self::from_assertions(vec![existing.clone()]); !new_assertion.entails(&existing_temp) }); @@ -365,4 +411,4 @@ impl PartialEq for Independencies { let other_set: std::collections::HashSet<_> = other.assertions.iter().collect(); self_set == other_set } -} +} \ No newline at end of file diff --git a/rust_core/tests/independence_tests.rs b/rust_core/tests/independence_tests.rs index 1400666..6b11223 100644 --- a/rust_core/tests/independence_tests.rs +++ b/rust_core/tests/independence_tests.rs @@ -536,6 +536,27 @@ mod independence_tests { assert!(ind_y.is_equivalent(&ind_x), "Equivalence should be symmetric"); } } + + #[test] + fn test_pgmpy_closure_large_case() { + use rust_core::Independencies; + + let mut ind3 = Independencies::new(); + ind3.add_assertions_from_tuples(vec![ + (vec!["c".to_string()], vec!["a".to_string()], Some(vec!["b".to_string(), "e".to_string(), "d".to_string()])), + (vec!["e".to_string(), "c".to_string()], vec!["b".to_string()], Some(vec!["a".to_string(), "d".to_string()])), + (vec!["b".to_string(), "d".to_string()], vec!["e".to_string()], Some(vec!["a".to_string()])), + (vec!["e".to_string()], vec!["b".to_string(), "d".to_string()], Some(vec!["c".to_string()])), + (vec!["e".to_string()], vec!["b".to_string(), "c".to_string()], Some(vec!["d".to_string()])), + (vec!["e".to_string(), "c".to_string()], vec!["a".to_string()], Some(vec!["b".to_string()])), + ]).unwrap(); + + let closure = ind3.closure(); + for assertion in closure.get_assertions() { + println!("{} ", assertion.to_string()); + } + assert_eq!(closure.get_assertions().len(), 78); + } } } \ No newline at end of file From d3827b7f77c786dc241fb5df7c7c135310e923dd Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 26 Jul 2025 18:45:34 +0530 Subject: [PATCH 57/62] minor s3contraction fix + test refactors --- .../src/independencies/independencies.rs | 8 +- rust_core/tests/independence_tests.rs | 194 +++++++++++------- 2 files changed, 125 insertions(+), 77 deletions(-) diff --git a/rust_core/src/independencies/independencies.rs b/rust_core/src/independencies/independencies.rs index d46ad79..009b0ba 100644 --- a/rust_core/src/independencies/independencies.rs +++ b/rust_core/src/independencies/independencies.rs @@ -210,7 +210,7 @@ impl Independencies { ind.event2.clone(), ind.event1.clone(), if ind.event3.is_empty() { None } else { Some(ind.event3.clone()) }, - ).unwrap() // Assuming it succeeds + ).unwrap() } /// Decomposition rule: 'X ⊥ Y,W | Z' -> 'X ⊥ Y | Z', 'X ⊥ W | Z' @@ -338,9 +338,9 @@ impl Independencies { let y_z: &HashSet = &larger.event3; // Use proper subset: subset and not equal - if y.is_subset(y_z) && *y != *y_z && - z.is_subset(y_z) && *z != *y_z && - y.is_disjoint(z) && !y.is_empty() && !z.is_empty() { + if y.is_subset(y_z) && y != y_z && + z.is_subset(y_z) && z != y_z && + y.is_disjoint(z) { // Create new assertion: X ⊥ W,Y | Z let mut new_event2 = larger.event2.clone(); new_event2.extend(y.iter().cloned()); diff --git a/rust_core/tests/independence_tests.rs b/rust_core/tests/independence_tests.rs index 6b11223..9cba0e3 100644 --- a/rust_core/tests/independence_tests.rs +++ b/rust_core/tests/independence_tests.rs @@ -373,32 +373,50 @@ mod independence_tests { use super::*; use rust_core::Independencies; - // Helper to create complex Independencies matching PGMPY patterns - fn create_independencies_3() -> Independencies { + + // Helper function to create a HashSet from a Vec<&str> + fn s_set(vars: Vec<&str>) -> HashSet { + vars.into_iter().map(|s| s.to_string()).collect() + } + + // Helper function to create an IndependenceAssertion from string slices + fn create_ia(e1: Vec<&str>, e2: Vec<&str>, e3: Option>) -> IndependenceAssertion { + IndependenceAssertion::new( + s_set(e1), + s_set(e2), + e3.map(s_set), + ).unwrap() // Using unwrap() here since test data is controlled and expected to be valid + } + + // Helper function to create Independencies from a list of IndependenceAssertion + fn create_independencies(assertions: Vec) -> Independencies { let mut ind = Independencies::new(); - ind.add_assertions_from_tuples(vec![ - (vec!["a".to_string()], vec!["b".to_string(), "c".to_string(), "d".to_string()], Some(vec!["e".to_string(), "f".to_string(), "g".to_string()])), - (vec!["c".to_string()], vec!["d".to_string(), "e".to_string(), "f".to_string()], Some(vec!["g".to_string(), "h".to_string()])) - ]).unwrap(); + for assertion in assertions { + ind.add_assertion(assertion); + } ind } + // Helper to create complex Independencies matching PGMPY patterns + fn create_independencies_3() -> Independencies { + create_independencies(vec![ + create_ia(vec!["a"], vec!["b", "c", "d"], Some(vec!["e", "f", "g"])), + create_ia(vec!["c"], vec!["d", "e", "f"], Some(vec!["g", "h"])), + ]) + } + fn create_independencies_4() -> Independencies { - let mut ind = Independencies::new(); - ind.add_assertions_from_tuples(vec![ - (vec!["f".to_string(), "d".to_string(), "e".to_string()], vec!["c".to_string()], Some(vec!["h".to_string(), "g".to_string()])), - (vec!["b".to_string(), "c".to_string(), "d".to_string()], vec!["a".to_string()], Some(vec!["f".to_string(), "g".to_string(), "e".to_string()])) - ]).unwrap(); - ind + create_independencies(vec![ + create_ia(vec!["f", "d", "e"], vec!["c"], Some(vec!["h", "g"])), + create_ia(vec!["b", "c", "d"], vec!["a"], Some(vec!["f", "g", "e"])), + ]) } fn create_independencies_5() -> Independencies { - let mut ind = Independencies::new(); - ind.add_assertions_from_tuples(vec![ - (vec!["a".to_string()], vec!["b".to_string(), "c".to_string(), "d".to_string()], Some(vec!["e".to_string(), "f".to_string(), "g".to_string()])), - (vec!["c".to_string()], vec!["d".to_string(), "e".to_string(), "f".to_string()], Some(vec!["g".to_string()])) - ]).unwrap(); - ind + create_independencies(vec![ + create_ia(vec!["a"], vec!["b", "c", "d"], Some(vec!["e", "f", "g"])), + create_ia(vec!["c"], vec!["d", "e", "f"], Some(vec!["g"])), + ]) } #[test] @@ -419,27 +437,24 @@ mod independence_tests { #[test] fn test_pgmpy_complex_equivalence_scenarios() { // Test case 1: ind1 vs ind2 (should NOT be equivalent) - let mut ind1 = Independencies::new(); - ind1.add_assertions_from_tuples(vec![ - (vec!["X".to_string()], vec!["Y".to_string(), "W".to_string()], Some(vec!["Z".to_string()])) - ]).unwrap(); + let ind1 = create_independencies(vec![ + create_ia(vec!["X"], vec!["Y", "W"], Some(vec!["Z"])) + ]); - let mut ind2 = Independencies::new(); - ind2.add_assertions_from_tuples(vec![ - (vec!["X".to_string()], vec!["Y".to_string()], Some(vec!["Z".to_string()])), - (vec!["X".to_string()], vec!["W".to_string()], Some(vec!["Z".to_string()])) - ]).unwrap(); + let ind2 = create_independencies(vec![ + create_ia(vec!["X"], vec!["Y"], Some(vec!["Z"])), + create_ia(vec!["X"], vec!["W"], Some(vec!["Z"])) + ]); // This should be FALSE - ind1 should NOT be equivalent to ind2 assert!(!ind1.is_equivalent(&ind2), "ind1 should NOT be equivalent to ind2"); // Test case 2: ind1 vs ind3 (should be equivalent) - let mut ind3 = Independencies::new(); - ind3.add_assertions_from_tuples(vec![ - (vec!["X".to_string()], vec!["Y".to_string()], Some(vec!["Z".to_string()])), - (vec!["X".to_string()], vec!["W".to_string()], Some(vec!["Z".to_string()])), - (vec!["X".to_string()], vec!["Y".to_string()], Some(vec!["W".to_string(), "Z".to_string()])) - ]).unwrap(); + let ind3 = create_independencies(vec![ + create_ia(vec!["X"], vec!["Y"], Some(vec!["Z"])), + create_ia(vec!["X"], vec!["W"], Some(vec!["Z"])), + create_ia(vec!["X"], vec!["Y"], Some(vec!["W", "Z"])) + ]); // This should be TRUE - ind1 should be equivalent to ind3 assert!(ind1.is_equivalent(&ind3), "ind1 should be equivalent to ind3"); @@ -449,10 +464,9 @@ mod independence_tests { fn test_comprehensive_equality_edge_cases() { let empty_ind = Independencies::new(); - let mut non_empty_ind = Independencies::new(); - non_empty_ind.add_assertions_from_tuples(vec![ - (vec!["A".to_string()], vec!["B".to_string()], Some(vec!["C".to_string()])) - ]).unwrap(); + let non_empty_ind = create_independencies(vec![ + create_ia(vec!["A"], vec!["B"], Some(vec!["C"])) + ]); // Empty vs non-empty should be false assert_ne!(empty_ind, non_empty_ind, "Empty and non-empty independencies should not be equal"); @@ -472,17 +486,15 @@ mod independence_tests { #[test] fn test_complex_symmetric_equivalence() { // Create complex assertions that test symmetry at multiple levels - let mut ind_a = Independencies::new(); - ind_a.add_assertions_from_tuples(vec![ - (vec!["X".to_string(), "Y".to_string()], vec!["A".to_string(), "B".to_string()], Some(vec!["Z".to_string()])), - (vec!["P".to_string()], vec!["Q".to_string(), "R".to_string(), "S".to_string()], Some(vec!["T".to_string(), "U".to_string()])) - ]).unwrap(); - - let mut ind_b = Independencies::new(); - ind_b.add_assertions_from_tuples(vec![ - (vec!["A".to_string(), "B".to_string()], vec!["X".to_string(), "Y".to_string()], Some(vec!["Z".to_string()])), - (vec!["P".to_string()], vec!["S".to_string(), "Q".to_string(), "R".to_string()], Some(vec!["U".to_string(), "T".to_string()])) - ]).unwrap(); + let ind_a = create_independencies(vec![ + create_ia(vec!["X", "Y"], vec!["A", "B"], Some(vec!["Z"])), + create_ia(vec!["P"], vec!["Q", "R", "S"], Some(vec!["T", "U"])) + ]); + + let ind_b = create_independencies(vec![ + create_ia(vec!["A", "B"], vec!["X", "Y"], Some(vec!["Z"])), + create_ia(vec!["P"], vec!["S", "Q", "R"], Some(vec!["U", "T"])) + ]); // These should be equal due to symmetric equivalence and set ordering assert_eq!(ind_a, ind_b, "Symmetric complex independencies should be equal"); @@ -500,29 +512,27 @@ mod independence_tests { let vars5 = ind5.get_all_variables(); // All should contain the same variables - let expected_vars: HashSet = set_from_vec(vec!["a", "b", "c", "d", "e", "f", "g", "h"]); + let expected_vars: HashSet = s_set(vec!["a", "b", "c", "d", "e", "f", "g", "h"]); assert_eq!(vars3, expected_vars, "Independencies3 should have all expected variables"); assert_eq!(vars4, expected_vars, "Independencies4 should have all expected variables"); - let expected_vars5: HashSet = set_from_vec(vec!["a", "b", "c", "d", "e", "f", "g"]); + let expected_vars5: HashSet = s_set(vec!["a", "b", "c", "d", "e", "f", "g"]); assert_eq!(vars5, expected_vars5, "Independencies5 should have subset of variables"); } #[test] fn test_bidirectional_equivalence_complex() { // Test that equivalence is truly bidirectional in complex scenarios - let mut ind_x = Independencies::new(); - ind_x.add_assertions_from_tuples(vec![ - (vec!["A".to_string()], vec!["B".to_string(), "C".to_string()], Some(vec!["D".to_string()])), - (vec!["E".to_string()], vec!["F".to_string()], Some(vec!["G".to_string(), "H".to_string()])) - ]).unwrap(); - - let mut ind_y = Independencies::new(); - ind_y.add_assertions_from_tuples(vec![ - (vec!["A".to_string()], vec!["B".to_string()], Some(vec!["D".to_string()])), - (vec!["A".to_string()], vec!["C".to_string()], Some(vec!["D".to_string()])), - (vec!["E".to_string()], vec!["F".to_string()], Some(vec!["G".to_string(), "H".to_string()])) - ]).unwrap(); + let ind_x = create_independencies(vec![ + create_ia(vec!["A"], vec!["B", "C"], Some(vec!["D"])), + create_ia(vec!["E"], vec!["F"], Some(vec!["G", "H"])) + ]); + + let ind_y = create_independencies(vec![ + create_ia(vec!["A"], vec!["B"], Some(vec!["D"])), + create_ia(vec!["A"], vec!["C"], Some(vec!["D"])), + create_ia(vec!["E"], vec!["F"], Some(vec!["G", "H"])) + ]); // Test that decomposition creates equivalence assert!(ind_x.entails(&ind_y), "Complex independencies should entail their decomposition"); @@ -539,17 +549,14 @@ mod independence_tests { #[test] fn test_pgmpy_closure_large_case() { - use rust_core::Independencies; - - let mut ind3 = Independencies::new(); - ind3.add_assertions_from_tuples(vec![ - (vec!["c".to_string()], vec!["a".to_string()], Some(vec!["b".to_string(), "e".to_string(), "d".to_string()])), - (vec!["e".to_string(), "c".to_string()], vec!["b".to_string()], Some(vec!["a".to_string(), "d".to_string()])), - (vec!["b".to_string(), "d".to_string()], vec!["e".to_string()], Some(vec!["a".to_string()])), - (vec!["e".to_string()], vec!["b".to_string(), "d".to_string()], Some(vec!["c".to_string()])), - (vec!["e".to_string()], vec!["b".to_string(), "c".to_string()], Some(vec!["d".to_string()])), - (vec!["e".to_string(), "c".to_string()], vec!["a".to_string()], Some(vec!["b".to_string()])), - ]).unwrap(); + let ind3 = create_independencies(vec![ + create_ia(vec!["c"], vec!["a"], Some(vec!["b", "e", "d"])), + create_ia(vec!["e", "c"], vec!["b"], Some(vec!["a", "d"])), + create_ia(vec!["b", "d"], vec!["e"], Some(vec!["a"])), + create_ia(vec!["e"], vec!["b", "d"], Some(vec!["c"])), + create_ia(vec!["e"], vec!["b", "c"], Some(vec!["d"])), + create_ia(vec!["e", "c"], vec!["a"], Some(vec!["b"])), + ]); let closure = ind3.closure(); for assertion in closure.get_assertions() { @@ -557,6 +564,47 @@ mod independence_tests { } assert_eq!(closure.get_assertions().len(), 78); } + + + #[test] + fn test_pgmpy_closure_w_xyz() { + // This corresponds to Independencies(('W', ['X', 'Y', 'Z'])) in pgmpy + let ind2 = create_independencies(vec![ + create_ia(vec!["W"], vec!["X", "Y", "Z"], None), + ]); + + let actual_closure = ind2.closure(); + let actual_assertions_set: HashSet<_> = actual_closure.get_assertions().iter().cloned().collect(); + + let expected_assertions_vec = vec![ + // The order here doesn't matter for the final comparison with HashSet + create_ia(vec!["W"], vec!["Y"], None), + create_ia(vec!["W"], vec!["Y"], Some(vec!["X"])), + create_ia(vec!["W"], vec!["Y"], Some(vec!["Z"])), + create_ia(vec!["W"], vec!["Y"], Some(vec!["X", "Z"])), + create_ia(vec!["W"], vec!["X", "Y"], None), + create_ia(vec!["W"], vec!["X"], Some(vec!["Y", "Z"])), + create_ia(vec!["W"], vec!["X", "Z"], Some(vec!["Y"])), + create_ia(vec!["W"], vec!["X"], None), + create_ia(vec!["W"], vec!["X", "Z"], None), + create_ia(vec!["W"], vec!["Y", "Z"], Some(vec!["X"])), + create_ia(vec!["W"], vec!["X", "Y", "Z"], None), + create_ia(vec!["W"], vec!["X"], Some(vec!["Z"])), + create_ia(vec!["W"], vec!["Y", "Z"], None), + create_ia(vec!["W"], vec!["Z"], Some(vec!["X"])), + create_ia(vec!["W"], vec!["Z"], None), + create_ia(vec!["W"], vec!["X", "Y"], Some(vec!["Z"])), + create_ia(vec!["W"], vec!["X"], Some(vec!["Y"])), + create_ia(vec!["W"], vec!["Z"], Some(vec!["X", "Y"])), + create_ia(vec!["W"], vec!["Z"], Some(vec!["Y"])), + ]; + let expected_assertions_set: HashSet<_> = expected_assertions_vec.into_iter().collect(); + + // Assert that the two sets are equal + assert_eq!(actual_assertions_set.len(), expected_assertions_set.len(), "Mismatch in number of assertions"); + assert_eq!(actual_assertions_set, expected_assertions_set, "Mismatch in closure results"); + } + } } \ No newline at end of file From 2c6f315c9a8dacd7f3df7c458f3ca458ad8b4bd8 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sat, 26 Jul 2025 23:35:16 +0530 Subject: [PATCH 58/62] add initial python comp --- python_bindings/src/lib.rs | 201 ++++++++++- python_bindings/tests/test_basic.py | 20 -- python_bindings/tests/test_dag.py | 65 ++++ python_bindings/tests/test_independencies.py | 312 ++++++++++++++++++ .../src/independencies/independencies.rs | 28 +- 5 files changed, 599 insertions(+), 27 deletions(-) delete mode 100644 python_bindings/tests/test_basic.py create mode 100644 python_bindings/tests/test_dag.py create mode 100644 python_bindings/tests/test_independencies.py diff --git a/python_bindings/src/lib.rs b/python_bindings/src/lib.rs index b523d34..48d6124 100644 --- a/python_bindings/src/lib.rs +++ b/python_bindings/src/lib.rs @@ -1,6 +1,6 @@ use pyo3::prelude::*; use pyo3::exceptions::{PyKeyError, PyValueError}; -use rust_core::RustDAG; +use rust_core::{RustDAG, IndependenceAssertion, Independencies}; use std::collections::HashSet; #[pyclass(name = "RustDAG")] @@ -31,6 +31,15 @@ impl PyRustDAG { .map_err(PyValueError::new_err) } + pub fn add_edges_from( + &mut self, + ebunch: Vec<(String, String)>, + weights: Option>, + ) -> PyResult<()> { + self.inner.add_edges_from(ebunch, weights) + .map_err(PyValueError::new_err) + } + pub fn get_parents(&self, node: String) -> PyResult> { self.inner.get_parents(&node) .map_err(PyKeyError::new_err) @@ -61,11 +70,201 @@ impl PyRustDAG { pub fn edge_count(&self) -> usize { self.inner.edge_count() } + + #[pyo3(signature = (variables, observed = None, include_latents = false))] + pub fn active_trail_nodes( + &self, + variables: Vec, + observed: Option>, + include_latents: bool, + ) -> PyResult>> { + self.inner.active_trail_nodes(variables, observed, include_latents) + .map_err(PyValueError::new_err) + } + + #[pyo3(signature = (start, end, observed = None, include_latents = false))] + pub fn is_dconnected( + &self, + start: String, + end: String, + observed: Option>, + include_latents: bool, + ) -> PyResult { + self.inner.is_dconnected(&start, &end, observed, include_latents) + .map_err(PyValueError::new_err) + } + + pub fn are_neighbors(&self, start: String, end: String) -> PyResult { + self.inner.are_neighbors(&start, &end) + .map_err(PyValueError::new_err) + } + + pub fn get_ancestral_graph(&self, nodes: Vec) -> PyResult { + self.inner.get_ancestral_graph(nodes) + .map(|dag| PyRustDAG { inner: dag }) + .map_err(PyValueError::new_err) + } + + #[pyo3(signature = (start, end, include_latents=false))] + pub fn minimal_dseparator( + &self, + start: String, + end: String, + include_latents: bool, + ) -> PyResult>> { + self.inner.minimal_dseparator(&start, &end, include_latents) + .map_err(PyValueError::new_err) + } +} + +#[pyclass(name = "IndependenceAssertion")] +#[derive(Clone)] +pub struct PyIndependenceAssertion { + inner: IndependenceAssertion, +} + +#[pymethods] +impl PyIndependenceAssertion { + #[new] + pub fn new(event1: Vec, event2: Vec, event3: Option>) -> PyResult { + let e1: HashSet = event1.into_iter().collect(); + let e2: HashSet = event2.into_iter().collect(); + let e3: Option> = event3.map(|v| v.into_iter().collect()); + let assertion = IndependenceAssertion::new(e1, e2, e3) + .map_err(PyValueError::new_err)?; + Ok(PyIndependenceAssertion { inner: assertion }) + } + + #[getter] + pub fn event1(&self) -> Vec { + let mut result: Vec = self.inner.event1.iter().cloned().collect(); + result.sort(); // Ensure deterministic order + result + } + + #[getter] + pub fn event2(&self) -> Vec { + let mut result: Vec = self.inner.event2.iter().cloned().collect(); + result.sort(); + result + } + + #[getter] + pub fn event3(&self) -> Vec { + let mut result: Vec = self.inner.event3.iter().cloned().collect(); + result.sort(); + result + } + + #[getter] + pub fn all_vars(&self) -> Vec { + let mut result: Vec = self.inner.all_vars.iter().cloned().collect(); + result.sort(); + result + } + + pub fn is_unconditional(&self) -> bool { + self.inner.is_unconditional() + } + + pub fn to_latex(&self) -> String { + self.inner.to_latex() + } + + fn __str__(&self) -> String { + format!("{}", self.inner) + } + + pub fn __eq__(&self, other: &PyIndependenceAssertion) -> bool { + self.inner == other.inner + } + + pub fn __ne__(&self, other: &PyIndependenceAssertion) -> bool { + self.inner != other.inner + } +} + +#[pyclass(name = "Independencies")] +#[derive(Clone)] +pub struct PyIndependencies { + inner: Independencies, +} + +#[pymethods] +impl PyIndependencies { + #[new] + pub fn new() -> Self { + PyIndependencies { inner: Independencies::new() } + } + + pub fn add_assertion(&mut self, assertion: &PyIndependenceAssertion) { + self.inner.add_assertion(assertion.inner.clone()); + } + + pub fn add_assertions_from_tuples(&mut self, tuples: Vec<(Vec, Vec, Option>)>) -> PyResult<()> { + self.inner.add_assertions_from_tuples(tuples) + .map_err(PyValueError::new_err) + } + + pub fn get_assertions(&self) -> Vec { + self.inner.get_assertions() + .iter() + .map(|a| PyIndependenceAssertion { inner: a.clone() }) + .collect() + } + + #[getter(independencies)] + pub fn get_independencies(&self) -> Vec { + self.inner.get_assertions() + .iter() + .map(|a| PyIndependenceAssertion { inner: a.clone() }) + .collect() + } + + pub fn get_all_variables(&self) -> Vec { + self.inner.get_all_variables().into_iter().collect() + } + + pub fn contains(&self, assertion: &PyIndependenceAssertion) -> bool { + self.inner.contains(&assertion.inner) + } + + pub fn closure(&self) -> PyIndependencies { + PyIndependencies { inner: self.inner.closure() } + } + + #[pyo3(signature = (inplace = false))] + pub fn reduce(&mut self, inplace: bool) -> PyResult> { + if inplace { + self.inner.reduce_inplace(); + Ok(None) + } else { + Ok(Some(PyIndependencies { inner: self.inner.reduce() })) + } + } + + pub fn entails(&self, other: &PyIndependencies) -> bool { + self.inner.entails(&other.inner) + } + + pub fn is_equivalent(&self, other: &PyIndependencies) -> bool { + self.inner.is_equivalent(&other.inner) + } + + pub fn __eq__(&self, other: &PyIndependencies) -> bool { + self.inner == other.inner + } + + pub fn __ne__(&self, other: &PyIndependencies) -> bool { + self.inner != other.inner + } } #[pymodule] fn causalgraphs(_py: Python, m: &Bound) -> PyResult<()> { m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/python_bindings/tests/test_basic.py b/python_bindings/tests/test_basic.py deleted file mode 100644 index 37d2bd0..0000000 --- a/python_bindings/tests/test_basic.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest -from causalgraphs import RustDAG - -@pytest.fixture -def dag(): - d = RustDAG() - d.add_node("X") - d.add_node("Y") - d.add_edge("X", "Y") - return d - -def test_nodes_and_edges(dag): - assert set(dag.nodes()) == {"X", "Y"} - assert dag.node_count() == 2 - assert dag.edge_count() == 1 - assert dag.edges() == [("X", "Y")] - -def test_parents_children(dag): - assert dag.get_parents("Y") == ["X"] - assert dag.get_children("X") == ["Y"] diff --git a/python_bindings/tests/test_dag.py b/python_bindings/tests/test_dag.py new file mode 100644 index 0000000..3f6c8e1 --- /dev/null +++ b/python_bindings/tests/test_dag.py @@ -0,0 +1,65 @@ +import pytest +from causalgraphs import RustDAG + +class TestDAG: + @pytest.fixture + def dag(self): + d = RustDAG() + d.add_node("X") + d.add_node("Y") + d.add_edge("X", "Y") + return d + + def test_nodes_and_edges(self, dag): + assert set(dag.nodes()) == {"X", "Y"} + assert dag.node_count() == 2 + assert dag.edge_count() == 1 + assert dag.edges() == [("X", "Y")] + + def test_parents_children(self, dag): + assert dag.get_parents("Y") == ["X"] + assert dag.get_children("X") == ["Y"] + + def test_minimal_dseparator(self): + # Test case: A → B ← C + dag1 = RustDAG() + dag1.add_edges_from([("A", "B"), ("B", "C")]) + assert dag1.minimal_dseparator("A", "C") == {"B"} + + # Test case: A → B ← C, B → D, A → E, E → D + dag2 = RustDAG() + dag2.add_edges_from([("A", "B"), ("B", "C"), ("C", "D"), ("A", "E"), ("E", "D")]) + assert dag2.minimal_dseparator("A", "D") == {"C", "E"} + + # Test case: B → A, B → C, A → D, D → C, A → E, C → E + dag3 = RustDAG() + dag3.add_edges_from([("B", "A"), ("B", "C"), ("A", "D"), ("D", "C"), ("A", "E"), ("C", "E")]) + assert dag3.minimal_dseparator("A", "C") == {"B", "D"} + + # Test with latents + dag_lat1 = RustDAG() + dag_lat1.add_nodes_from(["A", "B", "C"], latent=[False, True, False]) + dag_lat1.add_edges_from([("A", "B"), ("B", "C")]) + assert dag_lat1.minimal_dseparator("A", "C") is None + # assert dag_lat1.minimal_dseparator("A", "C", include_latents=True) == {"B"} + + dag_lat2 = RustDAG() + dag_lat2.add_nodes_from(["A", "B", "C", "D"], latent=[False, True, False, False]) + dag_lat2.add_edges_from([("A", "D"), ("D", "B"), ("B", "C")]) + assert dag_lat2.minimal_dseparator("A", "C") == {"D"} + + dag_lat3 = RustDAG() + dag_lat3.add_nodes_from(["A", "B", "C", "D"], latent=[False, True, False, False]) + dag_lat3.add_edges_from([("A", "B"), ("B", "D"), ("D", "C")]) + assert dag_lat3.minimal_dseparator("A", "C") == {"D"} + + dag_lat4 = RustDAG() + dag_lat4.add_nodes_from(["A", "B", "C", "D"], latent=[False, False, False, True]) + dag_lat4.add_edges_from([("A", "B"), ("B", "C"), ("A", "D"), ("D", "C")]) + assert dag_lat4.minimal_dseparator("A", "C") is None + + # Test adjacent nodes (should raise error) + dag5 = RustDAG() + dag5.add_edges_from([("A", "B")]) + with pytest.raises(ValueError, match="No possible separators because start and end are adjacent"): + dag5.minimal_dseparator("A", "B") diff --git a/python_bindings/tests/test_independencies.py b/python_bindings/tests/test_independencies.py new file mode 100644 index 0000000..974a102 --- /dev/null +++ b/python_bindings/tests/test_independencies.py @@ -0,0 +1,312 @@ +import pytest +from causalgraphs import IndependenceAssertion, Independencies, RustDAG +from typing import List, Optional, Set, Dict +from collections import defaultdict + + +class TestIndependenceAssertion: + def setup_method(self): + self.assertion = IndependenceAssertion(["U"], ["V"], ["Z"]) + + def test_init(self): + # Test basic initialization with single elements + assertion = IndependenceAssertion(["U"], ["V"], ["Z"]) + print(assertion.event1, assertion.event2, assertion.event3, assertion.all_vars) + assert set(assertion.event1) == {"U"} + assert set(assertion.event2) == {"V"} + assert set(assertion.event3) == {"Z"} + assert set(assertion.all_vars) == {"U", "V", "Z"} + + # Test initialization with multiple elements + assertion = IndependenceAssertion(["U", "V"], ["Y", "Z"], ["A", "B"]) + assert set(assertion.event1) == {"U", "V"} + assert set(assertion.event2) == {"Y", "Z"} + assert set(assertion.event3) == {"A", "B"} + assert set(assertion.all_vars) == {"U", "V", "Y", "Z", "A", "B"} + + # Test unconditional assertion + assertion = IndependenceAssertion(["U"], ["V"], None) + assert set(assertion.event1) == {"U"} + assert set(assertion.event2) == {"V"} + assert set(assertion.event3) == set() + assert set(assertion.all_vars) == {"U", "V"} + + def test_init_exceptions(self): + # Test missing event1 + with pytest.raises(ValueError, match="event1 needs to be specified"): + IndependenceAssertion([], ["U"], ["V"]) + + # Test missing event2 + with pytest.raises(ValueError, match="event2 needs to be specified"): + IndependenceAssertion(["U"], [], ["V"]) + + def test_is_unconditional(self): + assert not IndependenceAssertion(["U"], ["V"], ["Z"]).is_unconditional() + assert IndependenceAssertion(["U"], ["V"], None).is_unconditional() + + def test_to_latex(self): + assert IndependenceAssertion(["U"], ["V"], ["Z"]).to_latex() == "U \\perp V \\mid Z" + assert IndependenceAssertion(["U"], ["V"], None).to_latex() == "U \\perp V" + assert ( + IndependenceAssertion(["U", "V"], ["Y", "Z"], ["A", "B"]).to_latex() + == "U, V \\perp Y, Z \\mid A, B" + ) + + def test_to_string(self): + assert str(IndependenceAssertion(["U"], ["V"], ["Z"])) == "(U ⊥ V | Z)" + assert str(IndependenceAssertion(["U"], ["V"], None)) == "(U ⊥ V)" + assert ( + str(IndependenceAssertion(["U", "V"], ["Y", "Z"], ["A", "B"])) + == "(U, V ⊥ Y, Z | A, B)" + ) + + def test_eq(self): + i1 = IndependenceAssertion(["a"], ["b"], ["c"]) + i2 = IndependenceAssertion(["a"], ["b"], None) + i3 = IndependenceAssertion(["a"], ["b", "c", "d"]) + i4 = IndependenceAssertion(["a"], ["b", "c", "d"], ["e"]) + i5 = IndependenceAssertion(["a"], ["d", "c", "b"], ["e"]) + i6 = IndependenceAssertion(["a"], ["c", "d"], ["e", "b"]) + i7 = IndependenceAssertion(["a"], ["d", "c"], ["b", "e"]) + i8 = IndependenceAssertion(["a"], ["f", "d"], ["b", "e"]) + i9 = IndependenceAssertion(["a"], ["d", "k", "b"], ["e"]) + i10 = IndependenceAssertion(["k", "b", "d"], ["a"], ["e"]) + + # Test inequality with non-assertion types + assert i1 != "a" + assert i2 != 1 + assert i4 != [2, "a"] + assert i6 != "c" + + # Test inequality between different assertions + assert i1 != i2 + assert i1 != i3 + assert i2 != i4 + assert i3 != i6 + + # Test equality with symmetric and reordered assertions + assert i4 == i5 + assert i6 == i7 + assert i7 != i8 + assert i4 != i9 + assert i5 != i9 + assert i10 == i9 + assert i10 != i8 + + +class TestIndependencies: + def setup_method(self): + self.independencies = Independencies() + self.ind3 = Independencies() + self.ind3.add_assertions_from_tuples( + [ + (["a"], ["b", "c", "d"], ["e", "f", "g"]), + (["c"], ["d", "e", "f"], ["g", "h"]), + ] + ) + self.ind4 = Independencies() + self.ind4.add_assertions_from_tuples( + [ + (["f", "d", "e"], ["c"], ["h", "g"]), + (["b", "c", "d"], ["a"], ["f", "g", "e"]), + ] + ) + self.ind5 = Independencies() + self.ind5.add_assertions_from_tuples( + [ + (["a"], ["b", "c", "d"], ["e", "f", "g"]), + (["c"], ["d", "e", "f"], ["g"]), + ] + ) + + def test_init(self): + ind1 = Independencies() + ind1.add_assertions_from_tuples([(["X"], ["Y"], ["Z"])]) + + ind2 = Independencies() + ind2.add_assertions_from_tuples([(["X"], ["Y"], ["Z"])]) + + assert ind1 == ind2 # Compare two equivalent objects + + ind3 = Independencies() + assert ind3 == Independencies() + + def test_get_assertions(self): + ind1 = Independencies() + ind1.add_assertions_from_tuples([(["X"], ["Y"], ["Z"])]) + assertions = ind1.get_assertions() + assert len(assertions) == 1 + assert set(assertions[0].event1) == {"X"} + assert set(assertions[0].event2) == {"Y"} + assert set(assertions[0].event3) == {"Z"} + + ind2 = Independencies() + ind2.add_assertions_from_tuples([(["A"], ["B"], ["C"]), (["D"], ["E"], ["F"])]) + assertions = ind2.get_assertions() + assert len(assertions) == 2 + assert set(assertions[0].event1) == {"A"} + assert set(assertions[0].event2) == {"B"} + assert set(assertions[0].event3) == {"C"} + assert set(assertions[1].event1) == {"D"} + assert set(assertions[1].event2) == {"E"} + assert set(assertions[1].event3) == {"F"} + + def test_get_all_variables(self): + assert set(self.ind3.get_all_variables()) == {"a", "b", "c", "d", "e", "f", "g", "h"} + assert set(self.ind4.get_all_variables()) == {"a", "b", "c", "d", "e", "f", "g", "h"} + assert set(self.ind5.get_all_variables()) == {"a", "b", "c", "d", "e", "f", "g"} + + def test_closure(self): + ind1 = Independencies() + ind1.add_assertions_from_tuples([(["A"], ["B", "C"], ["D"])]) + closure = ind1.closure() + expected = Independencies() + expected.add_assertions_from_tuples( + [ + (["A"], ["B", "C"], ["D"]), + (["A"], ["B"], ["C", "D"]), + (["A"], ["C"], ["B", "D"]), + (["A"], ["B"], ["D"]), + (["A"], ["C"], ["D"]), + ] + ) + assert closure == expected + + ind2 = Independencies() + ind2.add_assertions_from_tuples([(["W"], ["X", "Y", "Z"], None)]) + closure = ind2.closure() + expected = Independencies() + expected.add_assertions_from_tuples( + [ + (["W"], ["Y"], None), + (["W"], ["Y"], ["X"]), + (["W"], ["Y"], ["Z"]), + (["W"], ["Y"], ["X", "Z"]), + (["W"], ["X", "Y"], None), + (["W"], ["X"], ["Y", "Z"]), + (["W"], ["X", "Z"], ["Y"]), + (["W"], ["X"], None), + (["W"], ["X", "Z"], None), + (["W"], ["Y", "Z"], ["X"]), + (["W"], ["X", "Y", "Z"], None), + (["W"], ["X"], ["Z"]), + (["W"], ["Y", "Z"], None), + (["W"], ["Z"], ["X"]), + (["W"], ["Z"], None), + (["W"], ["Y", "X"], ["Z"]), + (["W"], ["X"], ["Y"]), + (["W"], ["Z"], ["Y", "X"]), + (["W"], ["Z"], ["Y"]), + ] + ) + assert closure == expected + + ind3 = Independencies() + ind3.add_assertions_from_tuples( + [ + (["c"], ["a"], ["b", "e", "d"]), + (["e", "c"], ["b"], ["a", "d"]), + (["b", "d"], ["e"], ["a"]), + (["e"], ["b", "d"], ["c"]), + (["e"], ["b", "c"], ["d"]), + (["e", "c"], ["a"], ["b"]), + ] + ) + assert len(ind3.closure().get_assertions()) == 78 + + def test_entails(self): + ind1 = Independencies() + ind1.add_assertions_from_tuples([(["A", "B"], ["C", "D"], ["E"])]) + ind2 = Independencies() + ind2.add_assertions_from_tuples([(["A"], ["C"], ["E"])]) + assert ind1.entails(ind2) + assert not ind2.entails(ind1) + + ind3 = Independencies() + ind3.add_assertions_from_tuples([(["W"], ["X", "Y", "Z"], None)]) + assert ind3.entails(ind3.closure()) + assert ind3.closure().entails(ind3) + + def test_is_equivalent(self): + ind1 = Independencies() + ind1.add_assertions_from_tuples([(["X"], ["Y", "W"], ["Z"])]) + ind2 = Independencies() + ind2.add_assertions_from_tuples([(["X"], ["Y"], ["Z"]), (["X"], ["W"], ["Z"])]) + ind3 = Independencies() + ind3.add_assertions_from_tuples( + [(["X"], ["Y"], ["Z"]), (["X"], ["W"], ["Z"]), (["X"], ["Y"], ["W", "Z"])] + ) + assert not ind1.is_equivalent(ind2) + assert ind1.is_equivalent(ind3) + + def test_eq(self): + assert self.ind3 == self.ind4 + assert not (self.ind3 != self.ind4) + assert self.ind3 != self.ind5 + assert self.ind4 != self.ind5 + assert Independencies() != Independencies().add_assertions_from_tuples( + [(["A"], ["B"], ["C"])] + ) + assert Independencies().add_assertions_from_tuples([(["A"], ["B"], ["C"])]) != Independencies() + assert Independencies() == Independencies() + + def test_reduce(self): + ind1 = Independencies() + ind1.add_assertions_from_tuples([(["X"], ["Y"], ["Z"]), (["X"], ["Y"], ["Z"])]) + reduced = ind1.reduce() + assert len(reduced.get_assertions()) == 1 + assert reduced.get_assertions() == reduced.independencies + assert set(reduced.get_assertions()[0].event1) == {"X"} + assert set(reduced.get_assertions()[0].event2) == {"Y"} + assert set(reduced.get_assertions()[0].event3) == {"Z"} + + ind2 = Independencies() + ind2.add_assertions_from_tuples([(["A"], ["B"], ["C"]), (["D"], ["E"], ["F"])]) + reduced = ind2.reduce() + assertions = reduced.get_assertions() + assert len(assertions) == 2 + assert IndependenceAssertion(["A"], ["B"], ["C"]) in assertions + assert IndependenceAssertion(["D"], ["E"], ["F"]) in assertions + + ind3 = Independencies() + ind3.add_assertions_from_tuples([(["W"], ["X", "Y", "Z"], None), (["W"], ["X"], ["Y"])]) + reduced = ind3.reduce() + assertions = reduced.get_assertions() + assert len(assertions) == 1 + assert IndependenceAssertion(["W"], ["X", "Y", "Z"], None) in assertions + + ind4 = Independencies() + ind4.add_assertions_from_tuples( + [ + (["A"], ["B", "C"], ["D"]), + (["A"], ["B"], ["D"]), + (["A"], ["C"], ["D"]), + (["E"], ["F"], ["G"]), + ] + ) + reduced = ind4.reduce() + assert len(ind4.get_assertions()) == 4 + assertions = reduced.get_assertions() + assert len(assertions) == 2 + assert IndependenceAssertion(["A"], ["B", "C"], ["D"]) in assertions + assert IndependenceAssertion(["E"], ["F"], ["G"]) in assertions + + ind5 = Independencies() + ind5.add_assertions_from_tuples([(["X"], ["Y"], ["Z"]), (["X"], ["Y"], ["Z"]), (["A"], ["B"], ["C"])]) + original_len = len(ind5.get_assertions()) + ind5.reduce(inplace=True) + assert len(ind5.get_assertions()) == 2 + assert original_len != len(ind5.get_assertions()) + assert IndependenceAssertion(["X"], ["Y"], ["Z"]) in ind5.get_assertions() + assert IndependenceAssertion(["A"], ["B"], ["C"]) in ind5.get_assertions() + + ind6 = Independencies() + reduced = ind6.reduce() + assert len(reduced.get_assertions()) == 0 + + ind7 = Independencies() + ind7.add_assertions_from_tuples([(["X"], ["Y"], ["Z"])]) + reduced = ind7.reduce() + assertions = reduced.get_assertions() + assert len(assertions) == 1 + assert IndependenceAssertion(["X"], ["Y"], ["Z"]) in assertions \ No newline at end of file diff --git a/rust_core/src/independencies/independencies.rs b/rust_core/src/independencies/independencies.rs index 009b0ba..4ff4e97 100644 --- a/rust_core/src/independencies/independencies.rs +++ b/rust_core/src/independencies/independencies.rs @@ -42,13 +42,21 @@ impl IndependenceAssertion { } pub fn to_latex(&self) -> String { - let e1_str = self.event1.iter().cloned().collect::>().join(", "); - let e2_str = self.event2.iter().cloned().collect::>().join(", "); + // Sort the elements to ensure deterministic output + let mut e1_vec: Vec = self.event1.iter().cloned().collect(); + let mut e2_vec: Vec = self.event2.iter().cloned().collect(); + e1_vec.sort(); + e2_vec.sort(); + + let e1_str = e1_vec.join(", "); + let e2_str = e2_vec.join(", "); if self.event3.is_empty() { format!("{} \\perp {}", e1_str, e2_str) } else { - let e3_str = self.event3.iter().cloned().collect::>().join(", "); + let mut e3_vec: Vec = self.event3.iter().cloned().collect(); + e3_vec.sort(); + let e3_str = e3_vec.join(", "); format!("{} \\perp {} \\mid {}", e1_str, e2_str, e3_str) } } @@ -56,13 +64,21 @@ impl IndependenceAssertion { impl std::fmt::Display for IndependenceAssertion { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let e1_str = self.event1.iter().cloned().collect::>().join(", "); - let e2_str = self.event2.iter().cloned().collect::>().join(", "); + // Sort the elements to ensure deterministic output + let mut e1_vec: Vec = self.event1.iter().cloned().collect(); + let mut e2_vec: Vec = self.event2.iter().cloned().collect(); + e1_vec.sort(); + e2_vec.sort(); + + let e1_str = e1_vec.join(", "); + let e2_str = e2_vec.join(", "); if self.event3.is_empty() { write!(f, "({} ⊥ {})", e1_str, e2_str) } else { - let e3_str = self.event3.iter().cloned().collect::>().join(", "); + let mut e3_vec: Vec = self.event3.iter().cloned().collect(); + e3_vec.sort(); + let e3_str = e3_vec.join(", "); write!(f, "({} ⊥ {} | {})", e1_str, e2_str, e3_str) } } From 15d035211d78f65d93c201144c5da5ab0ccf6312 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 27 Jul 2025 12:28:57 +0530 Subject: [PATCH 59/62] rename to DAG python bindings --- python_bindings/src/lib.rs | 2 +- python_bindings/tests/test_dag.py | 20 ++++++++++---------- python_bindings/tests/test_independencies.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/python_bindings/src/lib.rs b/python_bindings/src/lib.rs index 48d6124..43ea0bd 100644 --- a/python_bindings/src/lib.rs +++ b/python_bindings/src/lib.rs @@ -3,7 +3,7 @@ use pyo3::exceptions::{PyKeyError, PyValueError}; use rust_core::{RustDAG, IndependenceAssertion, Independencies}; use std::collections::HashSet; -#[pyclass(name = "RustDAG")] +#[pyclass(name = "DAG")] #[derive(Clone)] pub struct PyRustDAG { inner: RustDAG, diff --git a/python_bindings/tests/test_dag.py b/python_bindings/tests/test_dag.py index 3f6c8e1..6312abd 100644 --- a/python_bindings/tests/test_dag.py +++ b/python_bindings/tests/test_dag.py @@ -1,10 +1,10 @@ import pytest -from causalgraphs import RustDAG +from causalgraphs import DAG class TestDAG: @pytest.fixture def dag(self): - d = RustDAG() + d = DAG() d.add_node("X") d.add_node("Y") d.add_edge("X", "Y") @@ -22,44 +22,44 @@ def test_parents_children(self, dag): def test_minimal_dseparator(self): # Test case: A → B ← C - dag1 = RustDAG() + dag1 = DAG() dag1.add_edges_from([("A", "B"), ("B", "C")]) assert dag1.minimal_dseparator("A", "C") == {"B"} # Test case: A → B ← C, B → D, A → E, E → D - dag2 = RustDAG() + dag2 = DAG() dag2.add_edges_from([("A", "B"), ("B", "C"), ("C", "D"), ("A", "E"), ("E", "D")]) assert dag2.minimal_dseparator("A", "D") == {"C", "E"} # Test case: B → A, B → C, A → D, D → C, A → E, C → E - dag3 = RustDAG() + dag3 = DAG() dag3.add_edges_from([("B", "A"), ("B", "C"), ("A", "D"), ("D", "C"), ("A", "E"), ("C", "E")]) assert dag3.minimal_dseparator("A", "C") == {"B", "D"} # Test with latents - dag_lat1 = RustDAG() + dag_lat1 = DAG() dag_lat1.add_nodes_from(["A", "B", "C"], latent=[False, True, False]) dag_lat1.add_edges_from([("A", "B"), ("B", "C")]) assert dag_lat1.minimal_dseparator("A", "C") is None # assert dag_lat1.minimal_dseparator("A", "C", include_latents=True) == {"B"} - dag_lat2 = RustDAG() + dag_lat2 = DAG() dag_lat2.add_nodes_from(["A", "B", "C", "D"], latent=[False, True, False, False]) dag_lat2.add_edges_from([("A", "D"), ("D", "B"), ("B", "C")]) assert dag_lat2.minimal_dseparator("A", "C") == {"D"} - dag_lat3 = RustDAG() + dag_lat3 = DAG() dag_lat3.add_nodes_from(["A", "B", "C", "D"], latent=[False, True, False, False]) dag_lat3.add_edges_from([("A", "B"), ("B", "D"), ("D", "C")]) assert dag_lat3.minimal_dseparator("A", "C") == {"D"} - dag_lat4 = RustDAG() + dag_lat4 = DAG() dag_lat4.add_nodes_from(["A", "B", "C", "D"], latent=[False, False, False, True]) dag_lat4.add_edges_from([("A", "B"), ("B", "C"), ("A", "D"), ("D", "C")]) assert dag_lat4.minimal_dseparator("A", "C") is None # Test adjacent nodes (should raise error) - dag5 = RustDAG() + dag5 = DAG() dag5.add_edges_from([("A", "B")]) with pytest.raises(ValueError, match="No possible separators because start and end are adjacent"): dag5.minimal_dseparator("A", "B") diff --git a/python_bindings/tests/test_independencies.py b/python_bindings/tests/test_independencies.py index 974a102..5d03528 100644 --- a/python_bindings/tests/test_independencies.py +++ b/python_bindings/tests/test_independencies.py @@ -1,5 +1,5 @@ import pytest -from causalgraphs import IndependenceAssertion, Independencies, RustDAG +from causalgraphs import IndependenceAssertion, Independencies from typing import List, Optional, Set, Dict from collections import defaultdict From 641518f00c91899e84aa09c7ea164f7e634c3890 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Sun, 27 Jul 2025 18:07:29 +0530 Subject: [PATCH 60/62] wasm changes --- python_bindings/.gitignore | 1 + wasm_bindings/js/tests/test-dag.js | 92 ++++ wasm_bindings/js/tests/test-independencies.js | 462 ++++++++++++++++++ wasm_bindings/js/tests/test-wasm.js | 14 - wasm_bindings/src/lib.rs | 241 ++++++++- 5 files changed, 791 insertions(+), 19 deletions(-) create mode 100644 wasm_bindings/js/tests/test-dag.js create mode 100644 wasm_bindings/js/tests/test-independencies.js delete mode 100644 wasm_bindings/js/tests/test-wasm.js diff --git a/python_bindings/.gitignore b/python_bindings/.gitignore index c8f0442..7f13ffe 100644 --- a/python_bindings/.gitignore +++ b/python_bindings/.gitignore @@ -70,3 +70,4 @@ docs/_build/ # Pyenv .python-version +*pvt_tests* diff --git a/wasm_bindings/js/tests/test-dag.js b/wasm_bindings/js/tests/test-dag.js new file mode 100644 index 0000000..dffee2e --- /dev/null +++ b/wasm_bindings/js/tests/test-dag.js @@ -0,0 +1,92 @@ +const cg = require("../pkg-node/causalgraphs_wasm.js"); + +describe("DAG wasm (CJS)", () => { + it("should add nodes & edges", () => { + const dag = new cg.DAG(); + dag.addNode("U"); + dag.addNode("V"); + dag.addEdge("U","V"); + expect(dag.nodes()).toEqual(["U","V"]); + expect(dag.nodeCount).toBe(2); + expect(dag.edges()).toEqual([["U","V"]]); + expect(dag.edgeCount).toBe(1); + }); + + it("should check if nodes are d-connected (basic, connected)", () => { + const dag = new cg.DAG(); + dag.addEdge("A", "B"); + dag.addEdge("B", "C"); + const connected = dag.isDconnected("A", "C"); + expect(connected).toBe(true); // A -> B -> C is d-connected + }); + + it("should check if nodes are d-connected (with observed, disconnected)", () => { + const dag = new cg.DAG(); + dag.addEdge("A", "B"); + dag.addEdge("B", "C"); + const connected = dag.isDconnected("A", "C", ["B"]); // Observed B blocks the path + expect(connected).toBe(false); + }); + + it("should check if nodes are neighbors (adjacent)", () => { + const dag = new cg.DAG(); + dag.addEdge("A", "B"); + const areNeighbors = dag.areNeighbors("A", "B"); + expect(areNeighbors).toBe(true); + }); + + it("should check if nodes are neighbors (non-adjacent)", () => { + const dag = new cg.DAG(); + dag.addEdge("A", "B"); + dag.addEdge("B", "C"); + const areNeighbors = dag.areNeighbors("A", "C"); + expect(areNeighbors).toBe(false); + }); + + it("should compute minimal d-separator (simple)", () => { + const dag = new cg.DAG(); + dag.addEdge("A", "B"); + dag.addEdge("B", "C"); + const sep = dag.minimalDseparator("A", "C"); + expect(sep.sort()).toEqual(["B"]); + }); + + it("should compute minimal d-separator (complex)", () => { + const dag = new cg.DAG(); + dag.addEdge("A", "B"); + dag.addEdge("B", "C"); + dag.addEdge("C", "D"); + dag.addEdge("A", "E"); + dag.addEdge("E", "D"); + const sep = dag.minimalDseparator("A", "D"); + expect(sep.sort()).toEqual(["C", "E"]); + }); + + it("should return null for minimal d-separator if none exists (latent)", () => { + const dag = new cg.DAG(); + dag.addNode("A", false); + dag.addNode("B", true); // latent + dag.addNode("C", false); + dag.addEdge("A", "B"); + dag.addEdge("B", "C"); + const sep = dag.minimalDseparator("A", "C"); + expect(sep).toBeNull(); + }); + + it("should compute active trail nodes (basic)", () => { + const dag = new cg.DAG(); + dag.addEdge("diff", "grades"); + dag.addEdge("intel", "grades"); + const result = dag.activeTrailNodes(["diff"]); + expect(result["diff"]).toEqual(["diff", "grades"].sort()); + }); + + it("should compute active trail nodes with observed", () => { + const dag = new cg.DAG(); + dag.addEdge("diff", "grades"); + dag.addEdge("intel", "grades"); + const result = dag.activeTrailNodes(["diff", "intel"], ["grades"]); + expect(result["diff"].sort()).toEqual(["diff", "intel"].sort()); + expect(result["intel"].sort()).toEqual(["diff", "intel"].sort()); + }); +}); diff --git a/wasm_bindings/js/tests/test-independencies.js b/wasm_bindings/js/tests/test-independencies.js new file mode 100644 index 0000000..5fc98fd --- /dev/null +++ b/wasm_bindings/js/tests/test-independencies.js @@ -0,0 +1,462 @@ +const cg = require("../pkg-node/causalgraphs_wasm.js"); + +describe("IndependenceAssertion WASM", () => { + describe("Basic functionality", () => { + it("should create assertion with single elements", () => { + const assertion = new cg.JsIndependenceAssertion(["U"], ["V"], ["Z"]); + expect(assertion.event1()).toEqual(["U"]); + expect(assertion.event2()).toEqual(["V"]); + expect(assertion.event3()).toEqual(["Z"]); + expect(assertion.allVars()).toEqual(["U", "V", "Z"]); + }); + + it("should create assertion with multiple elements", () => { + const assertion = new cg.JsIndependenceAssertion(["U", "V"], ["Y", "Z"], ["A", "B"]); + expect(assertion.event1()).toEqual(["U", "V"]); + expect(assertion.event2()).toEqual(["Y", "Z"]); + expect(assertion.event3()).toEqual(["A", "B"]); + expect(assertion.allVars()).toEqual(["U", "V", "Y", "Z", "A", "B"]); + }); + + it("should create unconditional assertion", () => { + const assertion = new cg.JsIndependenceAssertion(["U"], ["V"], null); + expect(assertion.event1()).toEqual(["U"]); + expect(assertion.event2()).toEqual(["V"]); + expect(assertion.event3()).toEqual([]); + expect(assertion.allVars()).toEqual(["U", "V"]); + expect(assertion.isUnconditional()).toBe(true); + }); + + it("should handle conditional assertion", () => { + const assertion = new cg.JsIndependenceAssertion(["U"], ["V"], ["Z"]); + expect(assertion.isUnconditional()).toBe(false); + }); + }); + + describe("Validation", () => { + it("should throw error for empty event1", () => { + expect(() => { + new cg.JsIndependenceAssertion([], ["V"], ["Z"]); + }).toThrow("event1 needs to be specified"); + }); + + it("should throw error for empty event2", () => { + expect(() => { + new cg.JsIndependenceAssertion(["U"], [], ["Z"]); + }).toThrow("event2 needs to be specified"); + }); + }); + + describe("String formatting", () => { + it("should format conditional assertion correctly", () => { + const assertion = new cg.JsIndependenceAssertion(["U"], ["V"], ["Z"]); + expect(assertion.toLatex()).toBe("U \\perp V \\mid Z"); + expect(assertion.toString()).toBe("(U ⊥ V | Z)"); + }); + + it("should format unconditional assertion correctly", () => { + const assertion = new cg.JsIndependenceAssertion(["U"], ["V"], null); + expect(assertion.toLatex()).toBe("U \\perp V"); + expect(assertion.toString()).toBe("(U ⊥ V)"); + }); + + it("should format multi-element assertion correctly", () => { + const assertion = new cg.JsIndependenceAssertion(["U", "V"], ["Y", "Z"], ["A", "B"]); + expect(assertion.toLatex()).toBe("U, V \\perp Y, Z \\mid A, B"); + expect(assertion.toString()).toBe("(U, V ⊥ Y, Z | A, B)"); + }); + }); + + describe("Equality", () => { + it("should handle basic equality", () => { + const i1 = new cg.JsIndependenceAssertion(["a"], ["b"], ["c"]); + const i2 = new cg.JsIndependenceAssertion(["a"], ["b"], null); + const i3 = new cg.JsIndependenceAssertion(["a"], ["b", "c", "d"], null); + + expect(i1.toString()).not.toBe(i2.toString()); + expect(i1.toString()).not.toBe(i3.toString()); + expect(i2.toString()).not.toBe(i3.toString()); + }); + + it("should handle symmetry", () => { + const i4 = new cg.JsIndependenceAssertion(["a"], ["b", "c", "d"], ["e"]); + const i5 = new cg.JsIndependenceAssertion(["a"], ["d", "c", "b"], ["e"]); + + // Order shouldn't matter for sets + expect(i4.toString()).toBe(i5.toString()); + }); + + it("should handle swapped events", () => { + const i9 = new cg.JsIndependenceAssertion(["a"], ["d", "k", "b"], ["e"]); + const i10 = new cg.JsIndependenceAssertion(["k", "b", "d"], ["a"], ["e"]); + + // Should be equal due to symmetry + expect(i9.toString()).toBe(i10.toString()); + }); + }); +}); + +describe("Independencies WASM", () => { + describe("Basic functionality", () => { + it("should create empty independencies", () => { + const ind = new cg.JsIndependencies(); + expect(ind.getAssertions()).toEqual([]); + expect(ind.getAllVariables()).toEqual([]); + }); + + it("should add assertion", () => { + const ind = new cg.JsIndependencies(); + const assertion = new cg.JsIndependenceAssertion(["X"], ["Y"], ["Z"]); + ind.addAssertion(assertion); + expect(ind.getAssertions()).toHaveLength(1); + expect(ind.contains(assertion)).toBe(true); + }); + + it("should add assertions from tuples", () => { + const ind = new cg.JsIndependencies(); + const tuples = [ + [["X"], ["Y"], ["Z"]], + [["A"], ["B"], ["C"]] + ]; + ind.addAssertionsFromTuples(tuples); + expect(ind.getAssertions()).toHaveLength(2); + }); + + it("should get all variables", () => { + const ind = new cg.JsIndependencies(); + ind.addAssertionsFromTuples([ + [["a"], ["b", "c", "d"], ["e", "f", "g"]], + [["c"], ["d", "e", "f"], ["g", "h"]] + ]); + const vars = ind.getAllVariables(); + expect(vars).toContain("a"); + expect(vars).toContain("b"); + expect(vars).toContain("c"); + expect(vars).toContain("d"); + expect(vars).toContain("e"); + expect(vars).toContain("f"); + expect(vars).toContain("g"); + expect(vars).toContain("h"); + expect(vars).toHaveLength(8); + }); + }); + + describe("Closure", () => { + it("should compute simple closure", () => { + const ind = new cg.JsIndependencies(); + ind.addAssertionsFromTuples([ + [["A"], ["B", "C"], ["D"]] + ]); + + const closure = ind.closure(); + const assertions = closure.getAssertions(); + + // Should contain original assertion and decompositions + expect(assertions.length).toBeGreaterThanOrEqual(1); + + // Check for decompositions: A ⊥ B | D and A ⊥ C | D + const assertionStrings = assertions.map(a => a.toString()); + expect(assertionStrings.some(s => s.includes("(A ⊥ B | D)"))).toBe(true); + expect(assertionStrings.some(s => s.includes("(A ⊥ C | D)"))).toBe(true); + }); + + it("should compute complex closure", () => { + const ind = new cg.JsIndependencies(); + ind.addAssertionsFromTuples([ + [["A"], ["B", "C", "D"], ["E"]] + ]); + + const closure = ind.closure(); + const assertions = closure.getAssertions(); + + // Should generate multiple assertions through semi-graphoid axioms + expect(assertions.length).toBeGreaterThan(1); + }); + + it("should compute closure for unconditional assertion", () => { + const ind = new cg.JsIndependencies(); + ind.addAssertionsFromTuples([ + [["W"], ["X", "Y", "Z"], null] + ]); + + const closure = ind.closure(); + const assertions = closure.getAssertions(); + + // Should generate multiple assertions + expect(assertions.length).toBeGreaterThan(1); + + // Check for specific expected assertions + const assertionStrings = assertions.map(a => a.toString()); + expect(assertionStrings.some(s => s.includes("(W ⊥ X)"))).toBe(true); + expect(assertionStrings.some(s => s.includes("(W ⊥ Y)"))).toBe(true); + expect(assertionStrings.some(s => s.includes("(W ⊥ Z)"))).toBe(true); + }); + }); + + describe("Entailment", () => { + it("should test entailment", () => { + const ind1 = new cg.JsIndependencies(); + ind1.addAssertionsFromTuples([ + [["W"], ["X", "Y", "Z"], null] + ]); + + const ind2 = new cg.JsIndependencies(); + ind2.addAssertionsFromTuples([ + [["W"], ["X"], null] + ]); + + // W ⊥ X,Y,Z should entail W ⊥ X + expect(ind1.entails(ind2)).toBe(true); + expect(ind2.entails(ind1)).toBe(false); + }); + + it("should test self-entailment", () => { + const ind = new cg.JsIndependencies(); + ind.addAssertionsFromTuples([ + [["W"], ["X", "Y", "Z"], null] + ]); + + const closure = ind.closure(); + expect(ind.entails(closure)).toBe(true); + expect(closure.entails(ind)).toBe(true); + }); + }); + + describe("Equivalence", () => { + it("should test equivalence", () => { + const ind1 = new cg.JsIndependencies(); + ind1.addAssertionsFromTuples([ + [["X"], ["Y", "W"], ["Z"]] + ]); + + const ind2 = new cg.JsIndependencies(); + ind2.addAssertionsFromTuples([ + [["X"], ["Y"], ["Z"]], + [["X"], ["W"], ["Z"]] + ]); + + const ind3 = new cg.JsIndependencies(); + ind3.addAssertionsFromTuples([ + [["X"], ["Y"], ["Z"]], + [["X"], ["W"], ["Z"]], + [["X"], ["Y"], ["W", "Z"]] + ]); + + // ind1 should NOT be equivalent to ind2 + expect(ind1.isEquivalent(ind2)).toBe(false); + + // ind1 should be equivalent to ind3 + expect(ind1.isEquivalent(ind3)).toBe(true); + }); + + it("should test symmetric equivalence", () => { + const indA = new cg.JsIndependencies(); + indA.addAssertionsFromTuples([ + [["X", "Y"], ["A", "B"], ["Z"]], + [["P"], ["Q", "R", "S"], ["T", "U"]] + ]); + + const indB = new cg.JsIndependencies(); + indB.addAssertionsFromTuples([ + [["A", "B"], ["X", "Y"], ["Z"]], + [["P"], ["S", "Q", "R"], ["U", "T"]] + ]); + + // These should be equal due to symmetric equivalence and set ordering + expect(indA.isEquivalent(indB)).toBe(true); + }); + }); + + describe("Reduce", () => { + it("should reduce duplicates", () => { + const ind = new cg.JsIndependencies(); + const assertion = new cg.JsIndependenceAssertion(["X"], ["Y"], ["Z"]); + + // Add the same assertion twice + ind.addAssertion(assertion); + ind.addAssertion(assertion); + + const reduced = ind.reduce(); + expect(reduced.getAssertions()).toHaveLength(1); + }); + + it("should reduce entailment", () => { + const ind = new cg.JsIndependencies(); + + // More general assertion + ind.addAssertionsFromTuples([ + [["W"], ["X", "Y", "Z"], null] + ]); + // More specific assertion (should be removed) + ind.addAssertionsFromTuples([ + [["W"], ["X"], null] + ]); + + const reduced = ind.reduce(); + expect(reduced.getAssertions()).toHaveLength(1); + + // Should keep the more general assertion + const general = new cg.JsIndependenceAssertion(["W"], ["X", "Y", "Z"], null); + expect(reduced.contains(general)).toBe(true); + }); + + it("should reduce independent assertions", () => { + const ind = new cg.JsIndependencies(); + ind.addAssertionsFromTuples([ + [["A"], ["B"], ["C"]], + [["D"], ["B"], ["F"]] + ]); + + const reduced = ind.reduce(); + expect(reduced.getAssertions()).toHaveLength(2); + }); + + it("should reduce complex case", () => { + const ind = new cg.JsIndependencies(); + + // General assertion that entails the specific ones + ind.addAssertionsFromTuples([ + [["A"], ["B", "C"], ["D"]] + ]); + // Specific assertions that should be removed + ind.addAssertionsFromTuples([ + [["A"], ["B"], ["D"]], + [["A"], ["C"], ["D"]] + ]); + // Independent assertion + ind.addAssertionsFromTuples([ + [["E"], ["F"], ["G"]] + ]); + + const reduced = ind.reduce(); + expect(reduced.getAssertions()).toHaveLength(2); + + const general = new cg.JsIndependenceAssertion(["A"], ["B", "C"], ["D"]); + const independent = new cg.JsIndependenceAssertion(["E"], ["F"], ["G"]); + + expect(reduced.contains(general)).toBe(true); + expect(reduced.contains(independent)).toBe(true); + }); + + it("should reduce empty independencies", () => { + const ind = new cg.JsIndependencies(); + const reduced = ind.reduce(); + expect(reduced.getAssertions()).toHaveLength(0); + }); + }); + + describe("Complex scenarios", () => { + it("should handle complex multi-assertion equality", () => { + const ind3 = new cg.JsIndependencies(); + ind3.addAssertionsFromTuples([ + [["a"], ["b", "c", "d"], ["e", "f", "g"]], + [["c"], ["d", "e", "f"], ["g", "h"]] + ]); + + const ind4 = new cg.JsIndependencies(); + ind4.addAssertionsFromTuples([ + [["f", "d", "e"], ["c"], ["h", "g"]], + [["b", "c", "d"], ["a"], ["f", "g", "e"]] + ]); + + const ind5 = new cg.JsIndependencies(); + ind5.addAssertionsFromTuples([ + [["a"], ["b", "c", "d"], ["e", "f", "g"]], + [["c"], ["d", "e", "f"], ["g"]] + ]); + + // These should be equal due to symmetric equivalence + expect(ind3.isEquivalent(ind4)).toBe(true); + + // These should not be equal + expect(ind3.isEquivalent(ind5)).toBe(false); + expect(ind4.isEquivalent(ind5)).toBe(false); + }); + + it("should handle large closure case", () => { + const ind = new cg.JsIndependencies(); + ind.addAssertionsFromTuples([ + [["c"], ["a"], ["b", "e", "d"]], + [["e", "c"], ["b"], ["a", "d"]], + [["b", "d"], ["e"], ["a"]], + [["e"], ["b", "d"], ["c"]], + [["e"], ["b", "c"], ["d"]], + [["e", "c"], ["a"], ["b"]] + ]); + + const closure = ind.closure(); + const assertions = closure.getAssertions(); + + // Should generate many assertions + expect(assertions.length).toBeGreaterThan(50); + }); + + it("should handle WXYZ closure case", () => { + const ind = new cg.JsIndependencies(); + ind.addAssertionsFromTuples([ + [["W"], ["X", "Y", "Z"], null] + ]); + + const closure = ind.closure(); + const assertions = closure.getAssertions(); + + // Should generate exactly 19 assertions for this case + expect(assertions).toHaveLength(19); + + // Check for specific expected assertions + const assertionStrings = assertions.map(a => a.toString()); + expect(assertionStrings).toContain("(W ⊥ X)"); + expect(assertionStrings).toContain("(W ⊥ Y)"); + expect(assertionStrings).toContain("(W ⊥ Z)"); + expect(assertionStrings).toContain("(W ⊥ X, Y)"); + expect(assertionStrings).toContain("(W ⊥ X, Z)"); + expect(assertionStrings).toContain("(W ⊥ Y, Z)"); + expect(assertionStrings).toContain("(W ⊥ X, Y, Z)"); + }); + }); + + describe("Edge cases", () => { + it("should handle empty independencies comparison", () => { + const empty1 = new cg.JsIndependencies(); + const empty2 = new cg.JsIndependencies(); + const nonEmpty = new cg.JsIndependencies(); + nonEmpty.addAssertionsFromTuples([ + [["A"], ["B"], ["C"]] + ]); + + // Empty vs non-empty should be false + expect(empty1.isEquivalent(nonEmpty)).toBe(false); + + // Non-empty vs empty should be false + expect(nonEmpty.isEquivalent(empty1)).toBe(false); + + // Empty vs empty should be true + expect(empty1.isEquivalent(empty2)).toBe(true); + }); + + it("should handle bidirectional equivalence", () => { + const indX = new cg.JsIndependencies(); + indX.addAssertionsFromTuples([ + [["A"], ["B", "C"], ["D"]], + [["E"], ["F"], ["G", "H"]] + ]); + + const indY = new cg.JsIndependencies(); + indY.addAssertionsFromTuples([ + [["A"], ["B"], ["D"]], + [["A"], ["C"], ["D"]], + [["E"], ["F"], ["G", "H"]] + ]); + + // Test that decomposition creates equivalence + expect(indX.entails(indY)).toBe(true); + + // Test bidirectional equivalence + const reverseEntailment = indY.entails(indX); + if (reverseEntailment) { + expect(indX.isEquivalent(indY)).toBe(true); + expect(indY.isEquivalent(indX)).toBe(true); + } + }); + }); +}); \ No newline at end of file diff --git a/wasm_bindings/js/tests/test-wasm.js b/wasm_bindings/js/tests/test-wasm.js deleted file mode 100644 index 10a140e..0000000 --- a/wasm_bindings/js/tests/test-wasm.js +++ /dev/null @@ -1,14 +0,0 @@ -const cg = require("../pkg-node/causalgraphs_wasm.js"); - -describe("RustDAG wasm (CJS)", () => { - it("should add nodes & edges", () => { - const dag = new cg.RustDAG(); - dag.addNode("U"); - dag.addNode("V"); - dag.addEdge("U","V"); - expect(dag.nodes()).toEqual(["U","V"]); - expect(dag.nodeCount).toBe(2); - expect(dag.edges()).toEqual([["U","V"]]); - expect(dag.edgeCount).toBe(1); - }); -}); diff --git a/wasm_bindings/src/lib.rs b/wasm_bindings/src/lib.rs index e13a6e0..3884a81 100644 --- a/wasm_bindings/src/lib.rs +++ b/wasm_bindings/src/lib.rs @@ -1,18 +1,20 @@ use wasm_bindgen::prelude::*; use serde::{Deserialize, Serialize}; use std::collections::HashSet; +use rust_core::{IndependenceAssertion, Independencies}; +use js_sys::{Object, Array}; -#[wasm_bindgen(js_name = RustDAG)] +#[wasm_bindgen(js_name = DAG)] #[derive(Clone)] -pub struct RustDAG { +pub struct DAG { inner: rust_core::RustDAG, } #[wasm_bindgen] -impl RustDAG { +impl DAG { #[wasm_bindgen(constructor)] - pub fn new() -> RustDAG { - RustDAG { inner: rust_core::RustDAG::new() } + pub fn new() -> DAG { + DAG { inner: rust_core::RustDAG::new() } } #[wasm_bindgen(js_name = addNode, catch)] @@ -78,8 +80,237 @@ impl RustDAG { pub fn latents(&self) -> JsValue { serde_wasm_bindgen::to_value(&self.inner.latents).unwrap_or_else(|_| JsValue::from_str("Failed to serialize latents")) } + + // In RustDAG impl + #[wasm_bindgen(js_name = minimalDseparator, catch)] + pub fn minimal_dseparator(&self, start: String, end: String, include_latents: Option) -> Result { + let result = self.inner.minimal_dseparator(&start, &end, include_latents.unwrap_or(false)) + .map_err(|e| JsValue::from_str(&e))?; + + match result { + Some(mut set) => { + let mut vec: Vec = set.drain().collect(); + vec.sort(); + let js_array = Array::new(); + for item in vec { + js_array.push(&JsValue::from_str(&item)); + } + Ok(js_array.into()) // Return JS Array + } + None => Ok(JsValue::NULL), + } + } + + #[wasm_bindgen(js_name = activeTrailNodes, catch)] + pub fn active_trail_nodes(&self, variables: Vec, observed: Option>, include_latents: Option) -> Result { + let result = self.inner.active_trail_nodes(variables, observed, include_latents.unwrap_or(false)) + .map_err(|e| JsValue::from_str(&e))?; + + // Create a plain JS Object + let js_object = Object::new(); + + for (key, mut set) in result { + let mut vec: Vec = set.drain().collect(); + vec.sort(); + + let js_array = Array::new(); + for item in vec { + js_array.push(&JsValue::from_str(&item)); + } + + // Set property on object (key: array) + js_sys::Reflect::set(&js_object, &JsValue::from_str(&key), &js_array.into()) + .map_err(|_| JsValue::from_str("Failed to set property"))?; + } + + Ok(js_object.into()) + } + + #[wasm_bindgen(js_name = isDconnected, catch)] + pub fn is_dconnected( + &self, + start: String, + end: String, + observed: Option>, + include_latents: Option, + ) -> Result { + self.inner.is_dconnected(&start, &end, observed, include_latents.unwrap_or(false)) + .map_err(|e| JsValue::from_str(&e)) + } + + #[wasm_bindgen(js_name = areNeighbors, catch)] + pub fn are_neighbors(&self, start: String, end: String) -> Result { + self.inner.are_neighbors(&start, &end) + .map_err(|e| JsValue::from_str(&e)) + } +} + + +#[wasm_bindgen] +#[derive(Clone)] +pub struct JsIndependenceAssertion { + inner: IndependenceAssertion, } +#[wasm_bindgen] +impl JsIndependenceAssertion { + #[wasm_bindgen(constructor)] + pub fn new(event1: Vec, event2: Vec, event3: Option>) -> Result { + let e1: HashSet = event1.into_iter().collect(); + let e2: HashSet = event2.into_iter().collect(); + let e3: Option> = event3.map(|v| v.into_iter().collect()); + let assertion = IndependenceAssertion::new(e1, e2, e3) + .map_err(|e| JsValue::from_str(&e))?; + Ok(JsIndependenceAssertion { inner: assertion }) + } + + #[wasm_bindgen(js_name = event1)] + pub fn event1(&self) -> Vec { + self.inner.event1.iter().cloned().collect() + } + + #[wasm_bindgen(js_name = event2)] + pub fn event2(&self) -> Vec { + self.inner.event2.iter().cloned().collect() + } + + #[wasm_bindgen(js_name = event3)] + pub fn event3(&self) -> Vec { + let mut e3_vec: Vec = self.inner.event3.iter().cloned().collect(); + e3_vec.sort(); + e3_vec + } + + #[wasm_bindgen(js_name = allVars)] + pub fn all_vars(&self) -> Vec { + // Return variables in the order: event1, event2, event3 + // Sort within each set for consistency + let mut all_vars_vec = Vec::new(); + + // Add event1 variables (sorted) + let mut e1_vec: Vec = self.inner.event1.iter().cloned().collect(); + e1_vec.sort(); + all_vars_vec.extend(e1_vec); + + // Add event2 variables (sorted) + let mut e2_vec: Vec = self.inner.event2.iter().cloned().collect(); + e2_vec.sort(); + all_vars_vec.extend(e2_vec); + + // Add event3 variables (sorted) + let mut e3_vec: Vec = self.inner.event3.iter().cloned().collect(); + e3_vec.sort(); + all_vars_vec.extend(e3_vec); + + all_vars_vec + } + + #[wasm_bindgen(js_name = isUnconditional)] + pub fn is_unconditional(&self) -> bool { + self.inner.is_unconditional() + } + + #[wasm_bindgen(js_name = toLatex)] + pub fn to_latex(&self) -> String { + self.inner.to_latex() + } + + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> String { + // Create a canonical representation that handles symmetry + let mut e1_vec: Vec = self.inner.event1.iter().cloned().collect(); + let mut e2_vec: Vec = self.inner.event2.iter().cloned().collect(); + e1_vec.sort(); + e2_vec.sort(); + + // For symmetry, ensure consistent ordering: put the lexicographically smaller set first + let (first, second) = if e1_vec < e2_vec { + (e1_vec, e2_vec) + } else { + (e2_vec, e1_vec) + }; + + let first_str = first.join(", "); + let second_str = second.join(", "); + + if self.inner.event3.is_empty() { + format!("({} ⊥ {})", first_str, second_str) + } else { + let mut e3_vec: Vec = self.inner.event3.iter().cloned().collect(); + e3_vec.sort(); + let e3_str = e3_vec.join(", "); + format!("({} ⊥ {} | {})", first_str, second_str, e3_str) + } + } +} + +#[wasm_bindgen] +#[derive(Clone)] +pub struct JsIndependencies { + inner: Independencies, +} + +#[wasm_bindgen] +impl JsIndependencies { + #[wasm_bindgen(constructor)] + pub fn new() -> JsIndependencies { + JsIndependencies { inner: Independencies::new() } + } + + #[wasm_bindgen(js_name = addAssertion)] + pub fn add_assertion(&mut self, assertion: &JsIndependenceAssertion) { + self.inner.add_assertion(assertion.inner.clone()); + } + + #[wasm_bindgen(js_name = addAssertionsFromTuples)] + pub fn add_assertions_from_tuples(&mut self, tuples: JsValue) -> Result<(), JsValue> { + let tuples: Vec<(Vec, Vec, Option>)> = + serde_wasm_bindgen::from_value(tuples) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + self.inner.add_assertions_from_tuples(tuples) + .map_err(|e| JsValue::from_str(&e)) + } + + #[wasm_bindgen(js_name = getAssertions)] + pub fn get_assertions(&self) -> Vec { + self.inner.get_assertions() + .iter() + .map(|a| JsIndependenceAssertion { inner: a.clone() }) + .collect() + } + + #[wasm_bindgen(js_name = getAllVariables)] + pub fn get_all_variables(&self) -> Vec { + self.inner.get_all_variables().into_iter().collect() + } + + #[wasm_bindgen(js_name = contains)] + pub fn contains(&self, assertion: &JsIndependenceAssertion) -> bool { + self.inner.contains(&assertion.inner) + } + + #[wasm_bindgen(js_name = closure)] + pub fn closure(&self) -> JsIndependencies { + JsIndependencies { inner: self.inner.closure() } + } + + #[wasm_bindgen(js_name = reduce)] + pub fn reduce(&self) -> JsIndependencies { + JsIndependencies { inner: self.inner.reduce() } + } + + #[wasm_bindgen(js_name = entails)] + pub fn entails(&self, other: &JsIndependencies) -> bool { + self.inner.entails(&other.inner) + } + + #[wasm_bindgen(js_name = isEquivalent)] + pub fn is_equivalent(&self, other: &JsIndependencies) -> bool { + self.inner.is_equivalent(&other.inner) + } +} + + // Optional: Add a start function for debugging or initialization #[wasm_bindgen(start)] pub fn main_js() -> Result<(), JsValue> { From b4e393fb83777ca2624a23589bf8a6ed41dd4bd9 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Fri, 1 Aug 2025 21:25:13 +0530 Subject: [PATCH 61/62] R binding changes --- r_bindings/causalgraphs/NAMESPACE | 4 + r_bindings/causalgraphs/R/extendr-wrappers.R | 64 ++++ r_bindings/causalgraphs/src/rust/src/lib.rs | 339 ++++++++++++++++-- r_bindings/causalgraphs/tests/testthat/test.R | 91 ++++- .../src/independencies/independencies.rs | 2 +- 5 files changed, 475 insertions(+), 25 deletions(-) diff --git a/r_bindings/causalgraphs/NAMESPACE b/r_bindings/causalgraphs/NAMESPACE index e16e0fa..8b85938 100644 --- a/r_bindings/causalgraphs/NAMESPACE +++ b/r_bindings/causalgraphs/NAMESPACE @@ -1,5 +1,9 @@ # Generated by roxygen2: do not edit by hand S3method("$",RDAG) +S3method("$",RIndependenceAssertion) +S3method("$",RIndependencies) S3method("[[",RDAG) +S3method("[[",RIndependenceAssertion) +S3method("[[",RIndependencies) useDynLib(causalgraphs, .registration = TRUE) diff --git a/r_bindings/causalgraphs/R/extendr-wrappers.R b/r_bindings/causalgraphs/R/extendr-wrappers.R index 64ec308..37bd8cb 100644 --- a/r_bindings/causalgraphs/R/extendr-wrappers.R +++ b/r_bindings/causalgraphs/R/extendr-wrappers.R @@ -36,11 +36,75 @@ RDAG$edge_count <- function() .Call(wrap__RDAG__edge_count, self) RDAG$latents <- function() .Call(wrap__RDAG__latents, self) +RDAG$add_edges_from <- function(ebunch, weights) .Call(wrap__RDAG__add_edges_from, self, ebunch, weights) + +RDAG$active_trail_nodes <- function(variables, observed, include_latents) .Call(wrap__RDAG__active_trail_nodes, self, variables, observed, include_latents) + +RDAG$is_dconnected <- function(start, end, observed, include_latents) .Call(wrap__RDAG__is_dconnected, self, start, end, observed, include_latents) + +RDAG$are_neighbors <- function(start, end) .Call(wrap__RDAG__are_neighbors, self, start, end) + +RDAG$get_ancestral_graph <- function(nodes) .Call(wrap__RDAG__get_ancestral_graph, self, nodes) + +RDAG$minimal_dseparator <- function(start, end, include_latents) .Call(wrap__RDAG__minimal_dseparator, self, start, end, include_latents) + #' @export `$.RDAG` <- function (self, name) { func <- RDAG[[name]]; environment(func) <- environment(); func } #' @export `[[.RDAG` <- `$.RDAG` +RIndependenceAssertion <- new.env(parent = emptyenv()) + +RIndependenceAssertion$new <- function(event1, event2, event3) .Call(wrap__RIndependenceAssertion__new, event1, event2, event3) + +RIndependenceAssertion$event1 <- function() .Call(wrap__RIndependenceAssertion__event1, self) + +RIndependenceAssertion$event2 <- function() .Call(wrap__RIndependenceAssertion__event2, self) + +RIndependenceAssertion$event3 <- function() .Call(wrap__RIndependenceAssertion__event3, self) + +RIndependenceAssertion$all_vars <- function() .Call(wrap__RIndependenceAssertion__all_vars, self) + +RIndependenceAssertion$is_unconditional <- function() .Call(wrap__RIndependenceAssertion__is_unconditional, self) + +RIndependenceAssertion$to_latex <- function() .Call(wrap__RIndependenceAssertion__to_latex, self) + +RIndependenceAssertion$to_string <- function() .Call(wrap__RIndependenceAssertion__to_string, self) + +#' @export +`$.RIndependenceAssertion` <- function (self, name) { func <- RIndependenceAssertion[[name]]; environment(func) <- environment(); func } + +#' @export +`[[.RIndependenceAssertion` <- `$.RIndependenceAssertion` + +RIndependencies <- new.env(parent = emptyenv()) + +RIndependencies$new <- function() .Call(wrap__RIndependencies__new) + +RIndependencies$add_assertion <- function(assertion) invisible(.Call(wrap__RIndependencies__add_assertion, self, assertion)) + +RIndependencies$add_assertions_from_tuples <- function(tuples) .Call(wrap__RIndependencies__add_assertions_from_tuples, self, tuples) + +RIndependencies$get_assertions <- function() .Call(wrap__RIndependencies__get_assertions, self) + +RIndependencies$get_all_variables <- function() .Call(wrap__RIndependencies__get_all_variables, self) + +RIndependencies$contains <- function(assertion) .Call(wrap__RIndependencies__contains, self, assertion) + +RIndependencies$closure <- function() .Call(wrap__RIndependencies__closure, self) + +RIndependencies$reduce <- function(inplace) .Call(wrap__RIndependencies__reduce, self, inplace) + +RIndependencies$entails <- function(other) .Call(wrap__RIndependencies__entails, self, other) + +RIndependencies$is_equivalent <- function(other) .Call(wrap__RIndependencies__is_equivalent, self, other) + +#' @export +`$.RIndependencies` <- function (self, name) { func <- RIndependencies[[name]]; environment(func) <- environment(); func } + +#' @export +`[[.RIndependencies` <- `$.RIndependencies` + # nolint end diff --git a/r_bindings/causalgraphs/src/rust/src/lib.rs b/r_bindings/causalgraphs/src/rust/src/lib.rs index f55fcbf..213caec 100644 --- a/r_bindings/causalgraphs/src/rust/src/lib.rs +++ b/r_bindings/causalgraphs/src/rust/src/lib.rs @@ -1,5 +1,17 @@ use extendr_api::prelude::*; -use rust_core::RustDAG; +use rust_core::{RustDAG, IndependenceAssertion, Independencies}; +use std::collections::HashSet; +use std::panic; + + +#[extendr] +fn on_load() { + panic::set_hook(Box::new(|info| { + eprintln!("Panic: {:?}", info); + })); +} + + #[extendr] #[derive(Debug, Clone)] @@ -20,8 +32,7 @@ impl RDAG { /// @param latent Whether the node is latent (default: FALSE) /// @export fn add_node(&mut self, node: String, latent: Option) -> extendr_api::Result<()> { - self.inner.add_node(node, latent.unwrap_or(false)) - .map_err(Error::from) + self.inner.add_node(node, latent.unwrap_or(false)).map_err(|e| Error::Other(e.to_string())) } @@ -32,37 +43,31 @@ impl RDAG { fn add_nodes_from(&mut self, nodes: Strings, latent: Nullable) -> extendr_api::Result<()> { let node_vec: Vec = nodes.iter().map(|s| s.to_string()).collect(); let latent_opt: Option> = latent.into_option().map(|v| v.iter().map(|x| x.is_true()).collect()); - - self.inner.add_nodes_from(node_vec, latent_opt) - .map_err(|e| Error::Other(e)) + self.inner.add_nodes_from(node_vec, latent_opt).map_err(|e| Error::Other(e.to_string())) } /// Add an edge between two nodes /// @param u Source node - /// @param v Target node - /// @param weight Optional edge weight + /// @param v Target node + /// @param weight Optional edge weight (default: NULL) /// @export fn add_edge(&mut self, u: String, v: String, weight: Nullable) -> extendr_api::Result<()> { let w = weight.into_option(); - self.inner.add_edge(u, v, w) - .map_err(|e| Error::Other(e)) + self.inner.add_edge(u, v, w).map_err(|e| Error::Other(e.to_string())) } /// Get parents of a node /// @param node The node name /// @export fn get_parents(&self, node: String) -> extendr_api::Result { - let parents = self.inner.get_parents(&node) - .map_err(|e| Error::Other(e))?; + let parents = self.inner.get_parents(&node).map_err(|e| Error::Other(e.to_string()))?; Ok(parents.iter().map(|s| s.as_str()).collect::()) } - /// Get children of a node /// @param node The node name /// @export fn get_children(&self, node: String) -> extendr_api::Result { - let children = self.inner.get_children(&node) - .map_err(|e| Error::Other(e))?; + let children = self.inner.get_children(&node).map_err(|e| Error::Other(e.to_string()))?; Ok(children.iter().map(|s| s.as_str()).collect::()) } @@ -71,8 +76,7 @@ impl RDAG { /// @export fn get_ancestors_of(&self, nodes: Strings) -> extendr_api::Result { let node_vec: Vec = nodes.iter().map(|s| s.to_string()).collect(); - let ancestors = self.inner.get_ancestors_of(node_vec) - .map_err(|e| Error::Other(e))?; + let ancestors = self.inner.get_ancestors_of(node_vec).map_err(|e| Error::Other(e.to_string()))?; Ok(ancestors.iter().map(|s| s.as_str()).collect::()) } @@ -82,7 +86,6 @@ impl RDAG { let nodes = self.inner.nodes(); nodes.iter().map(|s| s.as_str()).collect::() } - /// Get all edges in the DAG /// @export fn edges(&self) -> List { @@ -108,12 +111,306 @@ impl RDAG { fn latents(&self) -> Strings { self.inner.latents.iter().map(|s| s.as_str()).collect::() } + + /// Add multiple edges to the DAG + /// @param ebunch List of (u, v) pairs (each pair as a character vector of length 2) + /// @param weights Optional vector of weights (must match ebunch length) + /// @export + fn add_edges_from(&mut self, ebunch: List, weights: Nullable) -> extendr_api::Result<()> { + let mut edge_vec: Vec<(String, String)> = Vec::with_capacity(ebunch.len()); + let weight_opt: Option> = weights.into_option().map(|v| v.iter().map(|x| x.inner()).collect()); + + if let Some(ref w) = weight_opt { + if w.len() != ebunch.len() { + return Err(Error::Other("Weights length must match ebunch".to_string())); + } + } + + for (i, pair) in ebunch.values().enumerate() { + let pair_vec: Strings = pair.try_into() + .map_err(|_| Error::Other(format!("tuples[{}] must be a list", i)))?; // Changed error message + if pair_vec.len() != 2 { + return Err(Error::Other(format!("ebunch[{}] must have exactly 2 elements", i))); // Removed "(u, v)" part + } + edge_vec.push((pair_vec[0].to_string(), pair_vec[1].to_string())); + } + + self.inner.add_edges_from(edge_vec, weight_opt).map_err(|e| Error::Other(e.to_string())) + } + + /// Get active trail nodes + /// @param variables Vector of starting variables + /// @param observed Optional vector of observed nodes + /// @param include_latents Whether to include latents (default: FALSE) + /// @export + fn active_trail_nodes(&self, variables: Strings, observed: Nullable, include_latents: Option) -> extendr_api::Result { + let var_vec: Vec = variables.iter().map(|s| s.to_string()).collect(); + if var_vec.is_empty() { + return Err(Error::Other("variables cannot be empty".to_string())); + } + let obs_opt: Option> = observed.into_option().map(|v| v.iter().map(|s| s.to_string()).collect()); + + let result = self.inner.active_trail_nodes(var_vec, obs_opt, include_latents.unwrap_or(false)) + .map_err(|e| Error::Other(e.to_string()))?; + + let result_clone = result.clone(); + + let r_list = List::from_names_and_values( + result.keys().map(|k| k.as_str()), + result_clone.into_values().map(|set| { + let vec: Vec = set.into_iter().collect(); + let strings: Strings = vec.iter().map(|s| s.as_str()).collect(); + Into::::into(strings) + }) + )?; + Ok(r_list) + } + + + /// Check if two nodes are d-connected + /// @param start Starting node + /// @param end Ending node + /// @param observed Optional vector of observed nodes + /// @param include_latents Whether to include latents (default: FALSE) + /// @export + fn is_dconnected(&self, start: String, end: String, observed: Nullable, include_latents: Option) -> extendr_api::Result { + let obs_opt: Option> = observed.into_option().map(|v| v.iter().map(|s| s.to_string()).collect()); + self.inner.is_dconnected(&start, &end, obs_opt, include_latents.unwrap_or(false)) + .map_err(|e| Error::Other(e.to_string())) + } + + + /// Check if two nodes are neighbors + /// @param start First node + /// @param end Second node + /// @export + fn are_neighbors(&self, start: String, end: String) -> extendr_api::Result { + self.inner.are_neighbors(&start, &end).map_err(|e| Error::Other(e.to_string())) + } + + /// Get ancestral graph for given nodes + /// @param nodes Vector of nodes + /// @export + fn get_ancestral_graph(&self, nodes: Strings) -> extendr_api::Result { + let node_vec: Vec = nodes.iter().map(|s| s.to_string()).collect(); + self.inner.get_ancestral_graph(node_vec) + .map(|dag| RDAG { inner: dag }) + .map_err(|e| Error::Other(e.to_string())) + } + + /// Get minimal d-separator between two nodes + /// @param start Starting node + /// @param end Ending node + /// @param include_latents Whether to include latents (default: FALSE) + /// @export + fn minimal_dseparator(&self, start: String, end: String, include_latents: Option) -> extendr_api::Result> { + let result = self.inner.minimal_dseparator(&start, &end, include_latents.unwrap_or(false)) + .map_err(|e| Error::Other(e.to_string()))?; + match result { + Some(set) => { + let vec: Vec = set.into_iter().collect(); + Ok(Nullable::NotNull(vec.iter().map(|s| s.as_str()).collect::())) + } + None => Ok(Nullable::Null), + } + } +} + +#[extendr] +#[derive(Debug, Clone)] +pub struct RIndependenceAssertion { + inner: IndependenceAssertion, +} + +#[extendr] +impl RIndependenceAssertion { + /// Create a new IndependenceAssertion + /// @param event1 Vector of event1 variables + /// @param event2 Vector of event2 variables + /// @param event3 Optional vector of event3 variables + /// @export + fn new(event1: Strings, event2: Strings, event3: Nullable) -> extendr_api::Result { + let e1: HashSet = event1.iter().map(|s| s.to_string()).collect(); + let e2: HashSet = event2.iter().map(|s| s.to_string()).collect(); + let e3_opt: Option> = event3.into_option().map(|v| v.iter().map(|s| s.to_string()).collect()); + let inner = IndependenceAssertion::new(e1, e2, e3_opt) + .map_err(|e| Error::Other(e.to_string()))?; + Ok(RIndependenceAssertion { inner }) + } + + /// Get event1 variables + /// @export + fn event1(&self) -> Strings { + let mut result: Vec = self.inner.event1.iter().cloned().collect(); + result.sort(); + result.iter().map(|s| s.as_str()).collect::() + } + + /// Get event2 variables + /// @export + fn event2(&self) -> Strings { + let mut result: Vec = self.inner.event2.iter().cloned().collect(); + result.sort(); + result.iter().map(|s| s.as_str()).collect::() + } + + /// Get event3 variables + /// @export + fn event3(&self) -> Strings { + let mut result: Vec = self.inner.event3.iter().cloned().collect(); + result.sort(); + result.iter().map(|s| s.as_str()).collect::() + } + + /// Get all variables + /// @export + fn all_vars(&self) -> Strings { + let mut result: Vec = self.inner.all_vars.iter().cloned().collect(); + result.sort(); + result.iter().map(|s| s.as_str()).collect::() + } + + /// Check if unconditional + /// @export + fn is_unconditional(&self) -> bool { + self.inner.is_unconditional() + } + + /// Get LaTeX representation + /// @export + fn to_latex(&self) -> String { + self.inner.to_latex() + } + + /// Get string representation + /// @export + fn to_string(&self) -> String { + format!("{}", self.inner) + } } -// Macro to generate exports. -// This ensures exported functions are registered with R. -// See corresponding C code in `entrypoint.c` +#[extendr] +#[derive(Debug, Clone)] +pub struct RIndependencies { + inner: Independencies, +} + +#[extendr] +impl RIndependencies { + /// Create a new Independencies + /// @export + fn new() -> Self { + RIndependencies { inner: Independencies::new() } + } + + /// Add a single assertion + /// @param assertion An RIndependenceAssertion object + /// @export + fn add_assertion(&mut self, assertion: &RIndependenceAssertion) { + self.inner.add_assertion(assertion.inner.clone()); + } + + /// Add multiple assertions from R tuples + /// @param tuples A list of 2- or 3-tuples `(event1, event2, event3)` + /// @export + fn add_assertions_from_tuples(&mut self, tuples: List) -> extendr_api::Result<()> { + let mut rust_tuples: Vec<(Vec, Vec, Option>)> = Vec::with_capacity(tuples.len()); + + for (i, pair) in tuples.values().enumerate() { + if pair.is_null() { + continue; // Skip NULL items if any + } + let inner = pair.as_list().ok_or_else(|| Error::Other(format!("tuples[{}] must be a list", i)))?; + if inner.len() < 2 || inner.len() > 3 { + return Err(Error::Other(format!("tuples[{}] must have 2 or 3 elements", i))); + } + + let e1: Strings = inner.elt(0)?.try_into().map_err(|_| Error::Other(format!("tuples[{}][0] must be character vector", i)))?; + let e1_vec = e1.iter().map(|s| s.to_string()).collect::>(); + let e2: Strings = inner.elt(1)?.try_into().map_err(|_| Error::Other(format!("tuples[{}][1] must be character vector", i)))?; + let e2_vec = e2.iter().map(|s| s.to_string()).collect::>(); + + let e3_opt = if inner.len() == 3 { + let e3_robj = inner.elt(2)?; + if e3_robj.is_null() { + None + } else { + let e3: Strings = e3_robj.try_into().map_err(|_| Error::Other(format!("tuples[{}][2] must be character vector", i)))?; + Some(e3.iter().map(|s| s.to_string()).collect::>()) + } + } else { + None + }; + rust_tuples.push((e1_vec, e2_vec, e3_opt)); + } + + self.inner.add_assertions_from_tuples(rust_tuples).map_err(|e| Error::Other(e.to_string())) + } + + /// Get all assertions + /// @export + fn get_assertions(&self) -> List { + let assertions = self.inner.get_assertions(); + let mut r_list = List::new(assertions.len()); + for (i, a) in assertions.iter().enumerate() { + let r_assertion = RIndependenceAssertion { inner: a.clone() }; + r_list.set_elt(i, r_assertion.into()).unwrap(); + } + r_list + } + + /// Get all variables + /// @export + fn get_all_variables(&self) -> Strings { + let mut result: Vec = self.inner.get_all_variables().into_iter().collect(); + result.sort(); + result.iter().map(|s| s.as_str()).collect::() + } + + /// Check if contains assertion + /// @param assertion An RIndependenceAssertion object + /// @export + fn contains(&self, assertion: &RIndependenceAssertion) -> bool { + self.inner.contains(&assertion.inner) + } + + /// Compute closure + /// @export + fn closure(&self) -> RIndependencies { + RIndependencies { inner: self.inner.closure() } + } + + /// Reduce independencies + /// @param inplace Whether to modify in place (default: FALSE) + /// @export + fn reduce(&mut self, inplace: Option) -> Nullable { + if inplace.unwrap_or(false) { + self.inner.reduce_inplace(); + Nullable::Null + } else { + Nullable::NotNull(RIndependencies { inner: self.inner.reduce() }) + } + } + + /// Check if entails another set + /// @param other Another RIndependencies object + /// @export + fn entails(&self, other: &RIndependencies) -> bool { + self.inner.entails(&other.inner) + } + + /// Check if equivalent to another set + /// @param other Another RIndependencies object + /// @export + fn is_equivalent(&self, other: &RIndependencies) -> bool { + self.inner.is_equivalent(&other.inner) + } +} + + extendr_module! { mod causalgraphs; impl RDAG; + impl RIndependenceAssertion; + impl RIndependencies; } \ No newline at end of file diff --git a/r_bindings/causalgraphs/tests/testthat/test.R b/r_bindings/causalgraphs/tests/testthat/test.R index 749c454..72f4189 100644 --- a/r_bindings/causalgraphs/tests/testthat/test.R +++ b/r_bindings/causalgraphs/tests/testthat/test.R @@ -1,14 +1,99 @@ -library(causalgraphs) library(testthat) +library(causalgraphs) test_that("basic DAG operations", { dag <- RDAG$new() dag$add_node("A", FALSE) dag$add_node("B", FALSE) - dag$add_edge("A", "B", 20) + dag$add_edge("A", "B", NULL) expect_setequal(dag$nodes(), c("A", "B")) expect_equal(dag$node_count(), 2) expect_equal(dag$edge_count(), 1) expect_equal(dag$get_parents("B"), "A") expect_equal(dag$get_children("A"), "B") -}) \ No newline at end of file +}) + +test_that("add_edges_from adds multiple edges correctly", { + dag <- RDAG$new() + dag$add_nodes_from(c("A", "B", "C", "D"), NULL) + ebunch <- list(c("A", "B"), c("C", "D")) + weights <- c(1.5, 2.0) + dag$add_edges_from(ebunch, weights) + expect_equal(dag$edge_count(), 2) + expect_setequal(dag$nodes(), c("A", "B", "C", "D")) + + # Test with no weights + dag2 <- RDAG$new() + dag2$add_nodes_from(c("A", "B", "C", "D"), NULL) + dag2$add_edges_from(ebunch, NULL) + expect_equal(dag2$edge_count(), 2) +}) + +test_that("active_trail_nodes returns correct trails", { + dag <- RDAG$new() + dag$add_nodes_from(c("A", "B", "C"), NULL) + dag$add_edges_from(list(c("A", "B"), c("B", "C")), NULL) + result <- dag$active_trail_nodes(c("A"), NULL, FALSE) + expect_equal(sort(result$A), sort(c("A", "B", "C"))) + + result_observed <- dag$active_trail_nodes(c("A"), c("B"), FALSE) + expect_equal(result_observed$A, "A") + + result_multi <- dag$active_trail_nodes(c("A", "C"), NULL, FALSE) + expect_equal(sort(result_multi$A), sort(c("A", "B", "C"))) + expect_equal(sort(result_multi$C), sort(c("C", "B", "A"))) +}) + +# ... (add similar fixes for other tests: add nodes before calling methods, expect specific error strings) + +test_that("RIndependencies creation and methods", { + ind <- RIndependencies$new() + asser1 <- RIndependenceAssertion$new(c("X"), c("Y"), c("Z")) + ind$add_assertion(asser1) + assertions <- ind$get_assertions() + expect_length(assertions, 1) + expect_equal(assertions[[1]]$event1(), "X") + + ind$add_assertions_from_tuples(list( + list(c("A", "B"), c("C"), c("D")), + list(c("E"), c("F"), NULL), + list(c("X"), c("Y"), c("Z")) # Duplicate + )) + expect_length(ind$get_assertions(), 4) + expect_true(all(c("X", "Y", "Z", "A", "B", "C", "D", "E", "F") %in% ind$get_all_variables())) + + expect_true(ind$contains(asser1)) + + closure <- ind$closure() + expect_s3_class(closure, "RIndependencies") + expect_gte(length(closure$get_assertions()), length(ind$get_assertions())) + + reduced <- ind$reduce(FALSE) + expect_s3_class(reduced, "RIndependencies") + expect_lte(length(reduced$get_assertions()), length(ind$get_assertions())) + + ind$reduce(TRUE) + expect_lte(length(ind$get_assertions()), 3) + + expect_true(ind$entails(reduced)) + expect_true(ind$is_equivalent(ind)) + + ind_pgmpy <- RIndependencies$new() + ind_pgmpy$add_assertions_from_tuples(list( + list(c("c"), c("a"), c("b", "e", "d")), + list(c("e", "c"), c("b"), c("a", "d")), + list(c("b", "d"), c("e"), c("a")) + )) + expect_equal(length(ind_pgmpy$closure()$get_assertions()), 14) + + ind_large <- RIndependencies$new() + ind_large$add_assertions_from_tuples(list( + list(c("c"), c("a"), c("b", "e", "d")), + list(c("e", "c"), c("b"), c("a", "d")), + list(c("b", "d"), c("e"), c("a")), + list(c("e"), c("b", "d"), c("c")), + list(c("e"), c("b", "c"), c("d")), + list(c("e", "c"), c("a"), c("b")) + )) + expect_equal(length(ind_large$closure()$get_assertions()), 78) +}) diff --git a/rust_core/src/independencies/independencies.rs b/rust_core/src/independencies/independencies.rs index 4ff4e97..9f4e69d 100644 --- a/rust_core/src/independencies/independencies.rs +++ b/rust_core/src/independencies/independencies.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeSet, HashMap, HashSet}; +use std::collections::{BTreeSet, HashSet}; use std::hash::{Hash, Hasher}; #[derive(Debug, Clone, Eq)] From ff4a6a99e98ac5608feb4ac48ff1bcd8400b2c26 Mon Sep 17 00:00:00 2001 From: Mohammed Razak Date: Fri, 1 Aug 2025 21:38:28 +0530 Subject: [PATCH 62/62] temp R build fix --- Cargo.lock | 17 +++-------------- r_bindings/causalgraphs/src/rust/Cargo.toml | 4 ++-- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f84888..d64d185 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,7 +50,7 @@ name = "causalgraphs" version = "0.1.0" dependencies = [ "pyo3", - "rust_core 0.1.0", + "rust_core", ] [[package]] @@ -61,7 +61,7 @@ dependencies = [ "getrandom 0.2.16", "getrandom 0.3.3", "js-sys", - "rust_core 0.1.0", + "rust_core", "serde", "serde-wasm-bindgen", "uuid", @@ -563,7 +563,7 @@ name = "rcausalgraphs" version = "0.1.0" dependencies = [ "extendr-api", - "rust_core 0.1.0 (git+https://github.com/pgmpy/causalgraphs.git?branch=main)", + "rust_core", ] [[package]] @@ -586,17 +586,6 @@ dependencies = [ "rustworkx-core", ] -[[package]] -name = "rust_core" -version = "0.1.0" -source = "git+https://github.com/pgmpy/causalgraphs.git?branch=main#e8625b5a019ba8a6f67e53a9d1df7c9e925bac84" -dependencies = [ - "ahash", - "indexmap 2.10.0", - "petgraph", - "rustworkx-core", -] - [[package]] name = "rustversion" version = "1.0.21" diff --git a/r_bindings/causalgraphs/src/rust/Cargo.toml b/r_bindings/causalgraphs/src/rust/Cargo.toml index 14ec67e..860d497 100644 --- a/r_bindings/causalgraphs/src/rust/Cargo.toml +++ b/r_bindings/causalgraphs/src/rust/Cargo.toml @@ -11,9 +11,9 @@ name = 'rcausalgraphs' [dependencies] -rust_core = { git = "https://github.com/pgmpy/causalgraphs.git", branch = "main", package = "rust_core" } +# rust_core = { git = "https://github.com/pgmpy/causalgraphs.git", branch = "main", package = "rust_core" } # For local development, comment out the Git line above and uncomment this: -# rust_core = { path = "../../../../rust_core" } +rust_core = { path = "../../../../rust_core" } extendr-api = '*'