Skip to content

Commit 3313c25

Browse files
committed
Merge branch 'main' into feat/impl-bindings-independencies-minimal-dseparator
2 parents b80d5de + 3c44752 commit 3313c25

5 files changed

Lines changed: 228 additions & 8 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
runs-on: ${{ matrix.os }}
1313
strategy:
1414
matrix:
15-
os: [ubuntu-latest, macos-latest]
15+
os: [ubuntu-latest, macos-latest, windows-latest]
1616

1717
steps:
1818
- name: Checkout code

r_bindings/causalgraphs/src/rust/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ name = 'rcausalgraphs'
1111

1212
[dependencies]
1313

14-
# rust_core = { git = "https://github.com/pgmpy/causalgraphs.git", branch = "main", package = "rust_core" }
14+
rust_core = { git = "https://github.com/pgmpy/causalgraphs.git", branch = "main", package = "rust_core" }
1515

1616
# For local development, comment out the Git line above and uncomment this:
17-
rust_core = { path = "../../../../rust_core" }
17+
# rust_core = { path = "../../../../rust_core" }
1818

1919
extendr-api = '*'

r_bindings/causalgraphs/tests/testthat/test.R

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,108 @@ test_that("basic DAG operations", {
55
dag <- RDAG$new()
66
dag$add_node("A", FALSE)
77
dag$add_node("B", FALSE)
8-
dag$add_edge("A", "B", NULL)
8+
dag$add_edge("A", "B", 20)
9+
910
expect_setequal(dag$nodes(), c("A", "B"))
10-
expect_equal(dag$node_count(), 2)
11-
expect_equal(dag$edge_count(), 1)
11+
expect_equal(dag$node_count(), 2L)
12+
expect_equal(dag$edge_count(), 1L)
13+
1214
expect_equal(dag$get_parents("B"), "A")
1315
expect_equal(dag$get_children("A"), "B")
16+
17+
e <- dag$edges()
18+
expect_true(is.list(e) && all(c("from", "to") %in% names(e)))
19+
expect_equal(length(e$from), 1L)
20+
expect_equal(paste0(e$from, "->", e$to), "A->B")
21+
})
22+
23+
test_that("add_nodes_from with and without latent mask", {
24+
# With latent mask
25+
dag1 <- RDAG$new()
26+
dag1$add_nodes_from(c("X", "Y", "Z"), c(TRUE, FALSE, TRUE))
27+
expect_setequal(dag1$nodes(), c("X", "Y", "Z"))
28+
expect_setequal(dag1$latents(), c("X", "Z"))
29+
30+
# Without latent mask (all observed). Pass NULL explicitly.
31+
dag2 <- RDAG$new()
32+
dag2$add_nodes_from(c("A", "B", "C"), NULL)
33+
expect_setequal(dag2$nodes(), c("A", "B", "C"))
34+
expect_length(dag2$latents(), 0L)
35+
})
36+
37+
test_that("add_node defaults latent=FALSE and duplicate adds are no-ops", {
38+
dag <- RDAG$new()
39+
dag$add_node("L", FALSE)
40+
dag$add_node("L", TRUE)
41+
expect_setequal(dag$nodes(), "L")
42+
expect_length(dag$latents(), 0L)
43+
expect_equal(dag$node_count(), 1L)
44+
})
45+
46+
test_that("add_edge auto-adds missing nodes; optional weight works", {
47+
dag <- RDAG$new()
48+
49+
dag$add_edge("S", "T", NULL)
50+
expect_setequal(dag$nodes(), c("S", "T"))
51+
expect_equal(dag$node_count(), 2L)
52+
expect_equal(dag$edge_count(), 1L)
53+
expect_equal(dag$get_parents("T"), "S")
54+
expect_equal(dag$get_children("S"), "T")
55+
56+
# another edge with an explicit weight
57+
dag$add_edge("T", "U", 0.5)
58+
expect_equal(dag$edge_count(), 2L)
59+
60+
# edge list is order-insensitive for assertions
61+
e <- dag$edges()
62+
got <- paste0(e$from, "->", e$to)
63+
expect_setequal(got, c("S->T", "T->U"))
1464
})
1565

66+
test_that("get_parents / get_children errors on unknown node", {
67+
dag <- RDAG$new()
68+
dag$add_nodes_from(c("A","B"), NULL)
69+
dag$add_edge("A","B", NULL)
70+
71+
expect_error(dag$get_parents("Z"))
72+
expect_error(dag$get_children("Z"))
73+
})
74+
75+
test_that("get_ancestors_of returns nodes plus all their ancestors", {
76+
dag <- RDAG$new()
77+
dag$add_nodes_from(c("A","B","C","D"), NULL)
78+
dag$add_edge("A","B", NULL)
79+
dag$add_edge("B","C", NULL)
80+
dag$add_edge("D","C", NULL)
81+
82+
# ancestors(C) = {A, B, D, C} (includes the node itself per implementation)
83+
anc_C <- dag$get_ancestors_of(c("C"))
84+
expect_setequal(anc_C, c("A","B","C","D"))
85+
86+
# ancestors(B, D) = {A, B, D}
87+
anc_BD <- dag$get_ancestors_of(c("B","D"))
88+
expect_setequal(anc_BD, c("A","B","D"))
89+
90+
# Unknown node should error
91+
expect_error(dag$get_ancestors_of(c("C","Z")))
92+
})
93+
94+
test_that("nodes(), edges(), node_count(), edge_count(), latents() remain consistent", {
95+
dag <- RDAG$new()
96+
dag$add_nodes_from(c("L1","O1","O2"), c(TRUE, FALSE, FALSE))
97+
dag$add_edge("O1","O2", NULL)
98+
dag$add_edge("L1","O2", NULL)
99+
100+
expect_equal(dag$node_count(), 3L)
101+
expect_equal(dag$edge_count(), 2L)
102+
expect_setequal(dag$latents(), "L1")
103+
expect_setequal(dag$nodes(), c("L1","O1","O2"))
104+
105+
e <- dag$edges()
106+
expect_setequal(paste0(e$from, "->", e$to), c("O1->O2","L1->O2"))
107+
})
108+
109+
16110
test_that("add_edges_from adds multiple edges correctly", {
17111
dag <- RDAG$new()
18112
dag$add_nodes_from(c("A", "B", "C", "D"), NULL)
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
const cg = require("../pkg-node/causalgraphs_wasm.js");
2+
3+
// --- helpers ---
4+
const sortStrings = (arr) => arr.slice().sort();
5+
const sortPairs = (pairs) =>
6+
pairs
7+
.map(([a, b]) => [String(a), String(b)])
8+
.sort((p, q) => (p[0] === q[0] ? p[1].localeCompare(q[1]) : p[0].localeCompare(q[0])));
9+
10+
const latentsList = (dag) => {
11+
const v = dag.latents; // exposed as a getter property
12+
if (Array.isArray(v)) return sortStrings(v);
13+
if (v && typeof v === "object") {
14+
return sortStrings(Object.keys(v).filter((k) => v[k]));
15+
}
16+
return [];
17+
};
18+
19+
describe("DAG wasm (CJS)", () => {
20+
it("should add nodes & edges (basic)", () => {
21+
const dag = new cg.DAG();
22+
dag.addNode("U");
23+
dag.addNode("V");
24+
dag.addEdge("U", "V");
25+
expect(dag.nodes()).toEqual(["U", "V"]);
26+
expect(dag.nodeCount).toBe(2);
27+
expect(dag.edges()).toEqual([["U", "V"]]);
28+
expect(dag.edgeCount).toBe(1);
29+
});
30+
31+
it("addNode with optional latent flag; latents getter", () => {
32+
const dag = new cg.DAG();
33+
dag.addNode("A");
34+
dag.addNode("L", true);
35+
expect(sortStrings(dag.nodes())).toEqual(["A", "L"]);
36+
37+
const lats = latentsList(dag);
38+
expect(lats).toContain("L");
39+
expect(lats).not.toContain("A");
40+
});
41+
42+
it("addNodesFrom with optional latent mask (Uint8Array)", () => {
43+
const dag = new cg.DAG();
44+
dag.addNodesFrom(["X", "Y", "Z"], [true, false, true]);
45+
expect(sortStrings(dag.nodes())).toEqual(["X", "Y", "Z"]);
46+
47+
const lats = latentsList(dag);
48+
expect(lats).toEqual(["X", "Z"]);
49+
});
50+
51+
it("getParents and getChildren", () => {
52+
const dag = new cg.DAG();
53+
dag.addNodesFrom(["A", "B", "C", "D"]);
54+
dag.addEdge("A", "B");
55+
dag.addEdge("A", "C");
56+
dag.addEdge("B", "D");
57+
dag.addEdge("C", "D");
58+
59+
expect(sortStrings(dag.getParents("D"))).toEqual(["B", "C"]);
60+
expect(sortStrings(dag.getChildren("A"))).toEqual(["B", "C"]);
61+
});
62+
63+
it("getAncestorsOf for a single target", () => {
64+
const dag = new cg.DAG();
65+
dag.addNodesFrom(["A", "B", "C", "D"]);
66+
dag.addEdge("A", "B");
67+
dag.addEdge("A", "C");
68+
dag.addEdge("B", "D");
69+
dag.addEdge("C", "D");
70+
71+
const ancD = sortStrings(dag.getAncestorsOf(["D"]));
72+
expect(ancD).toEqual(["A", "B", "C", "D"]);
73+
});
74+
75+
it("getAncestorsOf for multiple targets", () => {
76+
const dag = new cg.DAG();
77+
dag.addNodesFrom(["A", "B", "C", "D", "E"]);
78+
dag.addEdge("A", "B");
79+
dag.addEdge("B", "C");
80+
dag.addEdge("A", "D");
81+
dag.addEdge("D", "E");
82+
83+
const anc = sortStrings(dag.getAncestorsOf(["C", "E"]));
84+
expect(anc).toEqual(["A", "B", "C", "D", "E"]);
85+
});
86+
87+
it("edges reflects added edges (order-insensitive)", () => {
88+
const dag = new cg.DAG();
89+
dag.addNodesFrom(["A", "B", "C"]);
90+
dag.addEdge("A", "B");
91+
dag.addEdge("B", "C");
92+
93+
const expected = sortPairs([
94+
["A", "B"],
95+
["B", "C"],
96+
]);
97+
const got = dag.edges();
98+
expect(Array.isArray(got)).toBe(true);
99+
100+
const normalized = sortPairs(got.map((e) => (Array.isArray(e) ? e.slice(0, 2) : e)));
101+
expect(normalized).toEqual(expected);
102+
});
103+
104+
it("addEdge can take an optional weight (graph relations still correct)", () => {
105+
const dag = new cg.DAG();
106+
dag.addNodesFrom(["S", "T"]);
107+
dag.addEdge("S", "T", 0.75);
108+
109+
expect(dag.getParents("T")).toEqual(["S"]);
110+
expect(dag.getChildren("S")).toEqual(["T"]);
111+
});
112+
113+
it("nodeCount / edgeCount track mutations", () => {
114+
const dag = new cg.DAG();
115+
expect(dag.nodeCount).toBe(0);
116+
expect(dag.edgeCount).toBe(0);
117+
118+
dag.addNodesFrom(["A", "B", "C"]);
119+
expect(dag.nodeCount).toBe(3);
120+
121+
dag.addEdge("A", "B");
122+
dag.addEdge("B", "C");
123+
expect(dag.edgeCount).toBe(2);
124+
});
125+
});

wasm_bindings/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use serde::{Deserialize, Serialize};
22
use std::collections::HashSet;
3+
use wasm_bindgen::prelude::*;
34
use rust_core::{IndependenceAssertion, Independencies};
45
use js_sys::{Object, Array};
56

@@ -12,8 +13,8 @@ pub struct DAG {
1213
#[wasm_bindgen]
1314
impl DAG {
1415
#[wasm_bindgen(constructor)]
15-
pub fn new() -> RustDAG {
16-
RustDAG {
16+
pub fn new() -> DAG {
17+
DAG {
1718
inner: rust_core::RustDAG::new(),
1819
}
1920
}

0 commit comments

Comments
 (0)