diff --git a/r_bindings/causalgraphs/NAMESPACE b/r_bindings/causalgraphs/NAMESPACE index 8b85938..5d78744 100644 --- a/r_bindings/causalgraphs/NAMESPACE +++ b/r_bindings/causalgraphs/NAMESPACE @@ -1,8 +1,10 @@ # Generated by roxygen2: do not edit by hand +S3method("$",PDAG) S3method("$",RDAG) S3method("$",RIndependenceAssertion) S3method("$",RIndependencies) +S3method("[[",PDAG) S3method("[[",RDAG) S3method("[[",RIndependenceAssertion) S3method("[[",RIndependencies) diff --git a/r_bindings/causalgraphs/R/extendr-wrappers.R b/r_bindings/causalgraphs/R/extendr-wrappers.R index 37bd8cb..8431564 100644 --- a/r_bindings/causalgraphs/R/extendr-wrappers.R +++ b/r_bindings/causalgraphs/R/extendr-wrappers.R @@ -106,5 +106,59 @@ RIndependencies$is_equivalent <- function(other) .Call(wrap__RIndependencies__is #' @export `[[.RIndependencies` <- `$.RIndependencies` +PDAG <- new.env(parent = emptyenv()) + +PDAG$new <- function() .Call(wrap__PDAG__new) + +PDAG$add_node <- function(node, latent) .Call(wrap__PDAG__add_node, self, node, latent) + +PDAG$add_nodes_from <- function(nodes, latent) .Call(wrap__PDAG__add_nodes_from, self, nodes, latent) + +PDAG$add_edge <- function(u, v, weight, directed) .Call(wrap__PDAG__add_edge, self, u, v, weight, directed) + +PDAG$add_edges_from <- function(ebunch, weights, directed) .Call(wrap__PDAG__add_edges_from, self, ebunch, weights, directed) + +PDAG$edges <- function() .Call(wrap__PDAG__edges, self) + +PDAG$nodes <- function() .Call(wrap__PDAG__nodes, self) + +PDAG$node_count <- function() .Call(wrap__PDAG__node_count, self) + +PDAG$edge_count <- function() .Call(wrap__PDAG__edge_count, self) + +PDAG$latents <- function() .Call(wrap__PDAG__latents, self) + +PDAG$directed_edges <- function() .Call(wrap__PDAG__directed_edges, self) + +PDAG$undirected_edges <- function() .Call(wrap__PDAG__undirected_edges, self) + +PDAG$all_neighbors <- function(node) .Call(wrap__PDAG__all_neighbors, self, node) + +PDAG$directed_children <- function(node) .Call(wrap__PDAG__directed_children, self, node) + +PDAG$directed_parents <- function(node) .Call(wrap__PDAG__directed_parents, self, node) + +PDAG$has_directed_edge <- function(u, v) .Call(wrap__PDAG__has_directed_edge, self, u, v) + +PDAG$has_undirected_edge <- function(u, v) .Call(wrap__PDAG__has_undirected_edge, self, u, v) + +PDAG$undirected_neighbors <- function(node) .Call(wrap__PDAG__undirected_neighbors, self, node) + +PDAG$is_adjacent <- function(u, v) .Call(wrap__PDAG__is_adjacent, self, u, v) + +PDAG$copy <- function() .Call(wrap__PDAG__copy, self) + +PDAG$orient_undirected_edge <- function(u, v, inplace) .Call(wrap__PDAG__orient_undirected_edge, self, u, v, inplace) + +PDAG$apply_meeks_rules <- function(apply_r4, inplace) .Call(wrap__PDAG__apply_meeks_rules, self, apply_r4, inplace) + +PDAG$to_dag <- function() .Call(wrap__PDAG__to_dag, self) + +#' @export +`$.PDAG` <- function (self, name) { func <- PDAG[[name]]; environment(func) <- environment(); func } + +#' @export +`[[.PDAG` <- `$.PDAG` + # nolint end diff --git a/r_bindings/causalgraphs/src/rust/src/lib.rs b/r_bindings/causalgraphs/src/rust/src/lib.rs index e345661..bc092d7 100644 --- a/r_bindings/causalgraphs/src/rust/src/lib.rs +++ b/r_bindings/causalgraphs/src/rust/src/lib.rs @@ -12,6 +12,7 @@ fn on_load() { } +use rust_core::RustPDAG; #[extendr] #[derive(Debug, Clone)] @@ -416,10 +417,233 @@ impl RIndependencies { } } +#[extendr] +#[derive(Debug, Clone)] +pub struct PDAG { + inner: RustPDAG, +} + + +#[extendr] +impl PDAG { + /// Create a new PDAG + /// @export + fn new() -> Self { + PDAG { inner: RustPDAG::new() } + } + + /// Add a single node + /// @param node Node name + /// @param latent Whether 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(|e| Error::Other(e.to_string())) + } + + /// Add nodes from vector with optional latent mask (NULL means all false) + /// @param nodes character vector + /// @param latent NULL or logical vector + /// @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.to_string())) + } + + /// Add single edge (directed or undirected) + /// @param u source + /// @param v target + /// @param weight optional numeric (NULL) + /// @param directed bool (TRUE: directed, FALSE: undirected) + /// @export + fn add_edge(&mut self, u: String, v: String, weight: Nullable, directed: Option) -> extendr_api::Result<()> { + let w = weight.into_option(); + let d = directed.unwrap_or(true); + self.inner.add_edge(u, v, w, d).map_err(|e| Error::Other(e.to_string())) + } + + /// Add multiple edges from an R list of pairs: list(c("A","B"), c("C","D")) + /// @param ebunch list of character vectors length 2 + /// @param weights NULL or numeric vector + /// @param directed bool + /// @export + fn add_edges_from(&mut self, ebunch: List, weights: Nullable, directed: Option) -> extendr_api::Result<()> { + // convert ebunch (List) -> Vec<(String,String)> + let mut edges: Vec<(String,String)> = Vec::with_capacity(ebunch.len()); + for (i, item) in ebunch.values().enumerate() { + // Each item must be a character vector of length 2 + let pair: Strings = item.try_into().map_err(|_| Error::Other(format!("ebunch[{}] must be a character vector of length 2", i)))?; + if pair.len() != 2 { + return Err(Error::Other(format!("ebunch[{}] must have exactly 2 elements", i))); + } + edges.push((pair[0].to_string(), pair[1].to_string())); + } + let weight_opt: Option> = weights.into_option().map(|v| v.iter().map(|d| d.inner()).collect()); + let directed = directed.unwrap_or(true); + self.inner.add_edges_from(Some(edges), weight_opt, directed).map_err(|e| Error::Other(e.to_string())) + } + + /// Return all edges. For PDAG this includes both directed and undirected (both directions placed into graph). + /// Return as list(from = ..., to = ...) same as RDAG$edges() + /// @export + fn edges(&self) -> List { + let edges = self.inner.edges(); + let (from, to): (Vec<_>, Vec<_>) = edges.into_iter().unzip(); + list!(from = from, to = to) + } + + /// Return nodes + /// @export + fn nodes(&self) -> Strings { + self.inner.nodes().iter().map(|s| s.as_str()).collect::() + } + + /// Number of nodes + /// @export + fn node_count(&self) -> i32 { + self.inner.node_map.len() as i32 + } + + /// Number of edges (count unique graph edges) + /// @export + fn edge_count(&self) -> i32 { + self.inner.edges().len() as i32 + } + + /// Latent nodes + /// @export + fn latents(&self) -> Strings { + let mut v: Vec = self.inner.latents.iter().cloned().collect(); + v.sort(); + v.iter().map(|s| s.as_str()).collect::() + } + + /// Directed edges as a list of 2-element character vectors + /// @export + fn directed_edges(&self) -> List { + let mut vec = self.inner.directed_edges.iter().cloned().collect::>(); + vec.sort(); + let mut out = List::new(vec.len()); + for (i, (u, v)) in vec.into_iter().enumerate() { + let pair = vec![u.as_str(), v.as_str()].iter().map(|s| *s).collect::(); + out.set_elt(i, Into::::into(pair)).unwrap(); + } + out + } + + /// Undirected edges reported as stored (u, v) for each undirected pair (original insertion) + /// @export + fn undirected_edges(&self) -> List { + let mut vec = self.inner.undirected_edges.iter().cloned().collect::>(); + vec.sort(); + let mut out = List::new(vec.len()); + for (i, (u, v)) in vec.into_iter().enumerate() { + let pair = vec![u.as_str(), v.as_str()].iter().map(|s| *s).collect::(); + out.set_elt(i, Into::::into(pair)).unwrap(); + } + out + } + + /// All neighbors (directed or undirected) as character vector + /// @export + fn all_neighbors(&self, node: String) -> extendr_api::Result { + let s = self.inner.all_neighbors(&node).map_err(|e| Error::Other(e))?; + let mut v: Vec = s.into_iter().collect(); + v.sort(); + Ok(v.iter().map(|x| x.as_str()).collect::()) + } + + /// Directed children + /// @export + fn directed_children(&self, node: String) -> extendr_api::Result { + let s = self.inner.directed_children(&node).map_err(|e| Error::Other(e))?; + let mut v: Vec = s.into_iter().collect(); + v.sort(); + Ok(v.iter().map(|x| x.as_str()).collect::()) + } + + /// Directed parents + /// @export + fn directed_parents(&self, node: String) -> extendr_api::Result { + let s = self.inner.directed_parents(&node).map_err(|e| Error::Other(e))?; + let mut v: Vec = s.into_iter().collect(); + v.sort(); + Ok(v.iter().map(|x| x.as_str()).collect::()) + } + + /// has_directed_edge + /// @export + fn has_directed_edge(&self, u: String, v: String) -> bool { + self.inner.has_directed_edge(&u, &v) + } + + /// has_undirected_edge + /// @export + fn has_undirected_edge(&self, u: String, v: String) -> bool { + self.inner.has_undirected_edge(&u, &v) + } + + /// undirected_neighbors + /// @export + fn undirected_neighbors(&self, node: String) -> extendr_api::Result { + let s = self.inner.undirected_neighbors(&node).map_err(|e| Error::Other(e))?; + let mut v: Vec = s.into_iter().collect(); + v.sort(); + Ok(v.iter().map(|x| x.as_str()).collect::()) + } + + /// is_adjacent + /// @export + fn is_adjacent(&self, u: String, v: String) -> bool { + self.inner.is_adjacent(&u, &v) + } + + /// copy + /// @export + fn copy(&self) -> PDAG { + PDAG { inner: self.inner.copy() } + } + + /// orient_undirected_edge (returns NULL if inplace = TRUE, otherwise returns new PDAG) + /// @param u + /// @param v + /// @param inplace default TRUE + /// @export + fn orient_undirected_edge(&mut self, u: String, v: String, inplace: Option) -> extendr_api::Result> { + let in_place = inplace.unwrap_or(true); + match self.inner.orient_undirected_edge(&u, &v, in_place) { + Ok(None) => Ok(Nullable::Null), + Ok(Some(pdag)) => Ok(Nullable::NotNull(PDAG { inner: pdag })), + Err(e) => Err(Error::Other(e)), + } + } + + /// apply_meeks_rules (apply_r4 bool, inplace bool) + /// @export + fn apply_meeks_rules(&mut self, apply_r4: Option, inplace: Option) -> extendr_api::Result> { + let apply_r4 = apply_r4.unwrap_or(true); + let inplace = inplace.unwrap_or(false); + match self.inner.apply_meeks_rules(apply_r4, inplace) { + Ok(None) => Ok(Nullable::Null), + Ok(Some(pdag)) => Ok(Nullable::NotNull(PDAG { inner: pdag })), + Err(e) => Err(Error::Other(e)), + } + } + + /// to_dag -> RDAG + /// @export + fn to_dag(&self) -> extendr_api::Result { + let dag = self.inner.to_dag().map_err(|e| Error::Other(e))?; + Ok(RDAG { inner: dag }) + } +} + extendr_module! { mod causalgraphs; impl RDAG; impl RIndependenceAssertion; impl RIndependencies; + impl PDAG; } diff --git a/r_bindings/causalgraphs/tests/testthat/test.R b/r_bindings/causalgraphs/tests/testthat/test_dag.R similarity index 100% rename from r_bindings/causalgraphs/tests/testthat/test.R rename to r_bindings/causalgraphs/tests/testthat/test_dag.R diff --git a/r_bindings/causalgraphs/tests/testthat/test_pdag.R b/r_bindings/causalgraphs/tests/testthat/test_pdag.R new file mode 100644 index 0000000..fc24d11 --- /dev/null +++ b/r_bindings/causalgraphs/tests/testthat/test_pdag.R @@ -0,0 +1,135 @@ +library(causalgraphs) +library(testthat) + + +test_that("basic PDAG operations and properties", { + pdag <- PDAG$new() + pdag$add_edges_from(list(c("A", "C"), c("D", "C")), weights = NULL, directed = TRUE) + pdag$add_edges_from(list(c("B", "A"), c("B", "D")), weights = NULL, directed = FALSE) + + expect_setequal(pdag$nodes(), c("A", "B", "C", "D")) + expect_equal(pdag$node_count(), 4L) + expect_equal(pdag$edge_count(), 6L) + + # Check directed edges + dir_edges <- pdag$directed_edges() + expect_length(dir_edges, 2) + expect_setequal(sapply(dir_edges, paste, collapse="->"), c("A->C", "D->C")) + + # Check undirected edges + undir_edges <- pdag$undirected_edges() + expect_length(undir_edges, 2) + # Sorting to ensure consistent comparison + undir_pairs <- sapply(undir_edges, function(x) paste(sort(x), collapse="-")) + expect_setequal(undir_pairs, c("A-B", "B-D")) + + # Check all edges in the representation + all_edges <- pdag$edges() + all_edges_str <- paste0(all_edges$from, "->", all_edges$to) + expect_setequal(all_edges_str, c("A->C", "D->C", "A->B", "B->A", "B->D", "D->B")) +}) + +test_that("PDAG neighbor and parent/child queries work correctly", { + pdag <- PDAG$new() + pdag$add_edges_from(list(c("A", "C"), c("D", "C")), weights = NULL, directed = TRUE) + pdag$add_edges_from(list(c("B", "A"), c("B", "D")), weights = NULL, directed = FALSE) + + expect_setequal(pdag$all_neighbors("A"), c("B", "C")) + expect_setequal(pdag$all_neighbors("B"), c("A", "D")) + expect_setequal(pdag$all_neighbors("C"), c("A", "D")) + expect_setequal(pdag$all_neighbors("D"), c("B", "C")) + + expect_setequal(pdag$directed_children("A"), "C") + expect_length(pdag$directed_children("B"), 0) + expect_setequal(pdag$directed_parents("C"), c("A", "D")) + + expect_setequal(pdag$undirected_neighbors("A"), "B") + expect_setequal(pdag$undirected_neighbors("B"), c("A", "D")) + expect_length(pdag$undirected_neighbors("C"), 0) +}) + + +test_that("PDAG edge existence checks work", { + pdag <- PDAG$new() + pdag$add_edges_from(list(c("A", "C"), c("D", "C")), weights = NULL, directed = TRUE) + pdag$add_edges_from(list(c("B", "A"), c("B", "D")), weights = NULL, directed = FALSE) + + expect_true(pdag$has_directed_edge("A", "C")) + expect_false(pdag$has_directed_edge("C", "A")) + expect_false(pdag$has_directed_edge("A", "B")) + + expect_true(pdag$has_undirected_edge("A", "B")) + expect_true(pdag$has_undirected_edge("B", "A")) + expect_false(pdag$has_undirected_edge("A", "C")) + + expect_true(pdag$is_adjacent("A", "B")) + expect_true(pdag$is_adjacent("A", "C")) + expect_false(pdag$is_adjacent("A", "D")) +}) + + +test_that("PDAG copy and orient_undirected_edge work", { + pdag <- PDAG$new() + pdag$add_edges_from(list(c("A", "C"), c("D", "C")), weights = NULL, directed = TRUE) + pdag$add_edges_from(list(c("B", "A"), c("B", "D")), weights = NULL, directed = FALSE) + + # Test copy + pdag_copy <- pdag$copy() + expect_equal(pdag$nodes(), pdag_copy$nodes()) + expect_equal(pdag$directed_edges(), pdag_copy$directed_edges()) + expect_equal(pdag$undirected_edges(), pdag_copy$undirected_edges()) + + # Test orient_undirected_edge (not in-place) + mod_pdag <- pdag$orient_undirected_edge("B", "A", inplace = FALSE) + expect_false(is.null(mod_pdag)) + expect_setequal(sapply(mod_pdag$directed_edges(), paste, collapse="->"), c("A->C", "D->C", "B->A")) + expect_setequal(sapply(mod_pdag$undirected_edges(), function(x) paste(sort(x), collapse="-")), "B-D") + + # Test orient_undirected_edge (in-place) + pdag$orient_undirected_edge("B", "A", inplace = TRUE) + expect_setequal(sapply(pdag$directed_edges(), paste, collapse="->"), c("A->C", "D->C", "B->A")) + expect_setequal(sapply(pdag$undirected_edges(), function(x) paste(sort(x), collapse="-")), "B-D") + + # Orienting an already directed edge should fail + expect_error(pdag$orient_undirected_edge("B", "A", inplace = TRUE)) +}) + +test_that("PDAG to_dag conversion works", { + pdag <- PDAG$new() + pdag$add_edges_from(list(c("A", "B"), c("C", "B")), weights = NULL, directed = TRUE) + pdag$add_edges_from(list(c("C", "D"), c("D", "A")), weights = NULL, directed = FALSE) + + dag <- pdag$to_dag() + expect_s3_class(dag, "RDAG") + expect_equal(dag$edge_count(), 4L) + + e <- dag$edges() + edges_str <- paste0(e$from, "->", e$to) + expect_true("A->B" %in% edges_str) + expect_true("C->B" %in% edges_str) + # Should not create a v-structure at D + expect_false(all(c("A->D", "C->D") %in% edges_str)) +}) + + +test_that("PDAG apply_meeks_rules works", { + # Test case 1: A -> B - C => A -> B -> C + pdag <- PDAG$new() + pdag$add_edge("A", "B", weight = NULL, directed = TRUE) + pdag$add_edge("B", "C", weight = NULL, directed = FALSE) + cpdag <- pdag$apply_meeks_rules(apply_r4 = TRUE, inplace = FALSE) + + e <- cpdag$edges() + edges_str <- paste0(e$from, "->", e$to) + expect_setequal(edges_str, c("A->B", "B->C")) + + # Test case 2: A -> B, D -> C, B - C => No change (potential v-structure) + pdag2 <- PDAG$new() + pdag2$add_edges_from(list(c("A", "B"), c("D", "C")), weights = NULL, directed = TRUE) + pdag2$add_edge("B", "C", weight = NULL, directed = FALSE) + cpdag2 <- pdag2$apply_meeks_rules(apply_r4 = TRUE, inplace = FALSE) + + e2 <- cpdag2$edges() + edges_str2 <- paste0(e2$from, "->", e2$to) + expect_setequal(edges_str2, c("A->B", "D->C", "B->C", "C->B")) +}) \ No newline at end of file diff --git a/wasm_bindings/js/tests/test-pdag.js b/wasm_bindings/js/tests/test-pdag.js new file mode 100644 index 0000000..0841413 --- /dev/null +++ b/wasm_bindings/js/tests/test-pdag.js @@ -0,0 +1,81 @@ +const cg = require("../pkg-node/causalgraphs_wasm.js"); + + +describe('cg.PDAG', () => { + it('can be instantiated', () => { + const pdag = new cg.PDAG(); + expect(pdag.nodeCount).toBe(0); + expect(pdag.edgeCount).toBe(0); + }); + + it('can add nodes and edges', () => { + const pdag = new cg.PDAG(); + pdag.addNode('A'); + pdag.addNode('B'); + pdag.addNode('C'); + pdag.addEdge('A', 'B', null, true); // A -> B + pdag.addEdge('B', 'C', null, false); // B - C + + expect(pdag.nodeCount).toBe(3); + expect(pdag.edgeCount).toBe(2); + + const directedEdges = new Set(pdag.directedEdges().map(e => e.join(','))); + expect(directedEdges).toEqual(new Set(['A,B'])); + + const undirectedEdges = new Set(pdag.undirectedEdges().map(e => e.sort().join(','))); + expect(undirectedEdges).toEqual(new Set(['B,C'])); + }); + + it('can add multiple edges from a list', () => { + const pdag = new cg.PDAG(); + const directed = [['A', 'B'], ['D', 'C']]; + const undirected = [['B', 'C']]; + pdag.addEdgesFrom(directed, null, true); + pdag.addEdgesFrom(undirected, null, false); + + expect(pdag.nodeCount).toBe(4); + expect(pdag.edgeCount).toBe(3); + expect(pdag.nodes().sort()).toEqual(['A', 'B', 'C', 'D']); + }); + + it("applies Meek's rules correctly (basic case)", () => { + const pdag = new cg.PDAG(); + pdag.addEdge('A', 'B', null, true); // A -> B + pdag.addEdge('B', 'C', null, false); // B - C + + const cpdag = pdag.applyMeeksRules(true, false); + const expectedEdges = new Set(['A,B', 'B,C']); + const actualEdges = new Set(cpdag.edges().map(e => e.join(','))); + + expect(actualEdges).toEqual(expectedEdges); + }); + + it("applies Meek's rules correctly (no change)", () => { + const pdag = new cg.PDAG(); + pdag.addEdgesFrom([['A', 'B'], ['D', 'C']], null, true); + pdag.addEdgesFrom([['B', 'C']], null, false); + + const cpdag = pdag.applyMeeksRules(true, false); + + // Expect B-C to remain undirected + const directed = new Set(cpdag.directedEdges().map(e => e.join(','))); + const undirected = new Set(cpdag.undirectedEdges().map(e => e.sort().join(','))); + + expect(directed).toEqual(new Set(['A,B', 'D,C'])); + expect(undirected).toEqual(new Set(['B,C'])); + }); + + it('converts to a DAG', () => { + const pdag = new cg.PDAG(); + pdag.addEdge('A', 'B', null, true); + pdag.addEdge('B', 'C', null, false); + + const dag = pdag.toDag(); + expect(dag.constructor.name).toBe('DAG'); + + const dagEdges = new Set(dag.edges().map(e => e.join(','))); + // to_dag is consistent, so B-C will be oriented B->C in this case + const expectedEdges = new Set(['A,B', 'B,C']); + expect(dagEdges).toEqual(expectedEdges); + }); +}); \ No newline at end of file diff --git a/wasm_bindings/src/lib.rs b/wasm_bindings/src/lib.rs index 81f03cf..e1155f2 100644 --- a/wasm_bindings/src/lib.rs +++ b/wasm_bindings/src/lib.rs @@ -326,6 +326,128 @@ impl JsIndependencies { } +#[wasm_bindgen(js_name = PDAG)] +pub struct PDAG { + inner: rust_core::RustPDAG, +} + +#[wasm_bindgen] +impl PDAG { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + inner: rust_core::RustPDAG::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(js_name = addNodesFrom, catch)] + 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)) + } + + #[wasm_bindgen(js_name = addEdge, catch)] + pub fn add_edge( + &mut self, + u: String, + v: String, + weight: Option, + directed: bool, + ) -> Result<(), JsValue> { + self.inner + .add_edge(u, v, weight, directed) + .map_err(|e| JsValue::from_str(&e)) + } + + #[wasm_bindgen(js_name = addEdgesFrom, catch)] + pub fn add_edges_from( + &mut self, + ebunch: JsValue, + weights: Option>, + directed: bool, + ) -> Result<(), JsValue> { + let ebunch_vec: Vec<(String, String)> = serde_wasm_bindgen::from_value(ebunch)?; + self.inner + .add_edges_from(Some(ebunch_vec), weights, directed) + .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 { + serde_wasm_bindgen::to_value(&self.inner.edges()).unwrap() + } + + #[wasm_bindgen(js_name = directedEdges)] + pub fn directed_edges(&self) -> JsValue { + serde_wasm_bindgen::to_value(&self.inner.directed_edges).unwrap() + } + + #[wasm_bindgen(js_name = undirectedEdges)] + pub fn undirected_edges(&self) -> JsValue { + serde_wasm_bindgen::to_value(&self.inner.undirected_edges).unwrap() + } + + #[wasm_bindgen(js_name = nodeCount, getter)] + pub fn node_count(&self) -> usize { + self.inner.node_map.len() + } + + #[wasm_bindgen(js_name = edgeCount, getter)] + pub fn edge_count(&self) -> usize { + self.inner.directed_edges.len() + self.inner.undirected_edges.len() + } + + #[wasm_bindgen(js_name = latents, getter)] + pub fn latents(&self) -> JsValue { + serde_wasm_bindgen::to_value(&self.inner.latents).unwrap() + } + + #[wasm_bindgen(js_name = applyMeeksRules, catch)] + pub fn apply_meeks_rules( + &mut self, + apply_r4: bool, + inplace: bool, + ) -> Result, JsValue> { + self.inner + .apply_meeks_rules(apply_r4, inplace) + .map(|opt| opt.map(|pdag| PDAG { inner: pdag })) + .map_err(|e| JsValue::from_str(&e)) + } + + #[wasm_bindgen(js_name = toDag, catch)] + pub fn to_dag(&self) -> Result { + self.inner + .to_dag() + .map(|dag| DAG { inner: dag }) + .map_err(|e| JsValue::from_str(&e)) + } + + #[wasm_bindgen(js_name = copy)] + pub fn copy(&self) -> PDAG { + PDAG { + inner: self.inner.clone(), + } + } +} + // Optional: Add a start function for debugging or initialization #[wasm_bindgen(start)] pub fn main_js() -> Result<(), JsValue> {