From 1eb13e9ae9a0e34c0fc54c5d44aeca4ba747acea Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:40:04 +0200 Subject: [PATCH 01/20] chore: initial commit --- todo.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 todo.txt diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000..e69de29 From 5ffb9d257decc1a5fbe1f92cbf911a7ca5e9c426 Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:57:00 +0200 Subject: [PATCH 02/20] docs: started on working out r bindings potential pitfalls --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4ef5d39..94d0e38 100644 --- a/.gitignore +++ b/.gitignore @@ -188,4 +188,7 @@ pyrightconfig.json .DS_Store Thumbs.db -todo.txt \ No newline at end of file +todo.txt + +# Design documents +docs/r-bindings-architecture.md \ No newline at end of file From 9f33b143e1652b44a685c4be06c1b8f230fe0895 Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:43:17 +0200 Subject: [PATCH 03/20] feat: very naive implementation --- Cargo.lock | 43 ++++++++++++++++++++++++++++++ crates/ci-core/src/ci_tests/mod.rs | 2 +- crates/ci-core/src/lib.rs | 2 +- crates/ci-r/Cargo.toml | 1 + crates/ci-r/src/lib.rs | 29 ++++++++++++++++++++ 5 files changed, 75 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e23b50e..4c80963 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,6 +218,7 @@ name = "ci_r" version = "0.1.0" dependencies = [ "ci_core", + "extendr-api", ] [[package]] @@ -544,6 +545,37 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "extendr-api" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803569de0d273b4bf281871046a7d63a23cc12776bdb5b63de5c1e81aae30728" +dependencies = [ + "extendr-ffi", + "extendr-macros", + "once_cell", + "paste", + "readonly", +] + +[[package]] +name = "extendr-ffi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba82ddd48e85202654997b81e4b1d39c0c54b5dcd7cae92705f807bf528efcf" + +[[package]] +name = "extendr-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8fad8d2a0d0651b1947042cf3a8beddc73d39cec3485b200fdfd24cb3bb6aa" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -1676,6 +1708,17 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "readonly" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2a62d85ed81ca5305dc544bd42c8804c5060b78ffa5ad3c64b0fb6a8c13d062" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "redox_syscall" version = "0.7.3" diff --git a/crates/ci-core/src/ci_tests/mod.rs b/crates/ci-core/src/ci_tests/mod.rs index 9cae276..34c1f75 100644 --- a/crates/ci-core/src/ci_tests/mod.rs +++ b/crates/ci-core/src/ci_tests/mod.rs @@ -1,4 +1,4 @@ -mod chi_squared; +pub mod chi_squared; mod cressie_read; mod freeman_tukey; mod log_likelihood; diff --git a/crates/ci-core/src/lib.rs b/crates/ci-core/src/lib.rs index 3294e97..abc87ef 100644 --- a/crates/ci-core/src/lib.rs +++ b/crates/ci-core/src/lib.rs @@ -1,4 +1,4 @@ -mod ci_tests; +pub mod ci_tests; pub mod registry; pub mod strategy; pub mod utils; diff --git a/crates/ci-r/Cargo.toml b/crates/ci-r/Cargo.toml index 13b8caf..de8bef3 100644 --- a/crates/ci-r/Cargo.toml +++ b/crates/ci-r/Cargo.toml @@ -9,6 +9,7 @@ repository = "" [dependencies] ci_core = { path = "../ci-core" } +extendr-api = "0.9.0" [lints] workspace = true diff --git a/crates/ci-r/src/lib.rs b/crates/ci-r/src/lib.rs index 8b13789..ce7340b 100644 --- a/crates/ci-r/src/lib.rs +++ b/crates/ci-r/src/lib.rs @@ -1 +1,30 @@ +use extendr_api::prelude::*; +use ci_core::strategy::CITest; +use ci_core::ci_tests::chi_squared::ChiSquared; + +#[extendr] +#[derive(Clone)] +pub struct RChiSquared { + citest: Option, +} + +#[extendr] +impl RChiSquared { + fn new( + boolean: bool, + significance_level: f64 + ) -> Self { + citest = ChiSquared::new(boolean, significance_level); + RChiSquared{citest} + } + + fn run_test( + &self, + x_values: Array1, + y_values: Array1, + z: Array2, + ) -> anyhow::Result { + self.citest?.run_test(x_values, y_values, z) + } +} \ No newline at end of file From d723de099b08667c6c96464dfc38f871df68ee4d Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Fri, 17 Apr 2026 12:03:16 +0200 Subject: [PATCH 04/20] fix: missing imports --- Cargo.lock | 1 + crates/ci-r/Cargo.toml | 1 + crates/ci-r/src/lib.rs | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c80963..f82ad5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,6 +217,7 @@ dependencies = [ name = "ci_r" version = "0.1.0" dependencies = [ + "anyhow", "ci_core", "extendr-api", ] diff --git a/crates/ci-r/Cargo.toml b/crates/ci-r/Cargo.toml index de8bef3..fea920b 100644 --- a/crates/ci-r/Cargo.toml +++ b/crates/ci-r/Cargo.toml @@ -9,6 +9,7 @@ repository = "" [dependencies] ci_core = { path = "../ci-core" } +anyhow = { workspace = true} extendr-api = "0.9.0" [lints] diff --git a/crates/ci-r/src/lib.rs b/crates/ci-r/src/lib.rs index ce7340b..d68f262 100644 --- a/crates/ci-r/src/lib.rs +++ b/crates/ci-r/src/lib.rs @@ -1,10 +1,10 @@ use extendr_api::prelude::*; -use ci_core::strategy::CITest; +use ci_core::strategy::{CITest, TestResult}; use ci_core::ci_tests::chi_squared::ChiSquared; +use anyhow; #[extendr] -#[derive(Clone)] pub struct RChiSquared { citest: Option, } From 84e6d3bc51ceaee371d038e7057e56498b247b4a Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:06:21 +0200 Subject: [PATCH 05/20] feat: downgraded ndarray to 0.16 everywhere, added testresult enum unpacking for r --- Cargo.lock | 161 ++++++++++++++++++++------------------ Cargo.toml | 2 +- crates/ci-core/Cargo.toml | 6 +- crates/ci-r/Cargo.toml | 2 +- crates/ci-r/src/lib.rs | 42 ++++++++-- 5 files changed, 123 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f82ad5e..dc51d34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,9 +94,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -139,7 +139,7 @@ checksum = "9ff11ddd2af3b5e80dd0297fee6e56ac038d9bdc549573cdb51bd6d2efe7f05e" dependencies = [ "num-complex", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "serde", ] @@ -154,9 +154,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.59" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "shlex", @@ -191,7 +191,7 @@ dependencies = [ "ndarray-linalg", "ordered-float", "proptest", - "rand 0.9.2", + "rand 0.9.4", "rand_distr 0.5.1", "statrs", ] @@ -251,9 +251,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", ] @@ -554,6 +554,7 @@ checksum = "803569de0d273b4bf281871046a7d63a23cc12776bdb5b63de5c1e81aae30728" dependencies = [ "extendr-ffi", "extendr-macros", + "ndarray", "once_cell", "paste", "readonly", @@ -726,9 +727,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -899,12 +900,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -959,9 +960,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "once_cell", "wasm-bindgen", @@ -980,18 +981,18 @@ dependencies = [ [[package]] name = "lapack-sys" -version = "0.15.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "314b879030845b68571809a6978e52d3b67ac5fba07e77b1b317b484092e2fb5" +checksum = "447f56c85fb410a7a3d36701b2153c1018b1d2b908c5fbaf01c1b04fac33bcbe" dependencies = [ "libc", ] [[package]] name = "lax" -version = "0.18.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda593cb87a3b1c06625e73710812005caf1523e45b0b898adcd7716602f8ba2" +checksum = "1048f58cdc36e726e1c5ef81ec7ac9b1be2fce6b705786514b5a4f8c63487867" dependencies = [ "cauchy", "intel-mkl-src", @@ -1016,9 +1017,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libm" @@ -1028,9 +1029,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ "bitflags", "libc", @@ -1093,7 +1094,7 @@ dependencies = [ "num-complex", "num-rational", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "rand_distr 0.4.3", "simba", "typenum", @@ -1118,9 +1119,9 @@ dependencies = [ [[package]] name = "ndarray" -version = "0.17.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" dependencies = [ "approx", "cblas-sys", @@ -1136,9 +1137,9 @@ dependencies = [ [[package]] name = "ndarray-linalg" -version = "0.18.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb0783188ff249ab498417e0477f7fade3b312d1d287314ca76de570d9a83ee0" +checksum = "820b7fdcc3f1fd48c31f4c85bdefe93ef2d0e8b69ff955c4d499ea6db2f19d44" dependencies = [ "cauchy", "katexit", @@ -1146,7 +1147,7 @@ dependencies = [ "ndarray", "num-complex", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "thiserror 2.0.18", ] @@ -1167,7 +1168,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", "serde", ] @@ -1299,9 +1300,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.76" +version = "0.10.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" dependencies = [ "bitflags", "cfg-if", @@ -1331,9 +1332,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", @@ -1379,9 +1380,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -1425,9 +1426,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] @@ -1501,7 +1502,7 @@ dependencies = [ "bit-vec", "bitflags", "num-traits", - "rand 0.9.2", + "rand 0.9.4", "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax", @@ -1597,9 +1598,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -1608,9 +1609,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -1661,7 +1662,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -1671,7 +1672,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" dependencies = [ "num-traits", - "rand 0.9.2", + "rand 0.9.4", ] [[package]] @@ -1691,9 +1692,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -1722,9 +1723,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ "bitflags", ] @@ -1815,9 +1816,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ "log", "once_cell", @@ -1839,9 +1840,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -2031,7 +2032,7 @@ dependencies = [ "approx", "nalgebra", "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -2220,9 +2221,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unarray" @@ -2321,9 +2322,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -2369,11 +2370,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -2382,14 +2383,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -2400,9 +2401,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2410,9 +2411,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -2423,9 +2424,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -2466,9 +2467,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -2476,9 +2477,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -2489,14 +2490,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.6", + "webpki-roots 1.0.7", ] [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -2745,6 +2746,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index 1b56066..d21440a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ resolver = "2" [workspace.dependencies] anyhow = "1" -ndarray = "0.17" +ndarray = "0.16" statrs = { version = "0.18.0"} diff --git a/crates/ci-core/Cargo.toml b/crates/ci-core/Cargo.toml index b8312b1..6412625 100644 --- a/crates/ci-core/Cargo.toml +++ b/crates/ci-core/Cargo.toml @@ -19,13 +19,13 @@ ordered-float = "5.3.0" rand = "0.9" [target.'cfg(target_os = "linux")'.dependencies] -ndarray-linalg = { version = "0.18.1", features = ["openblas-system"] } +ndarray-linalg = { version = "0.17", features = ["openblas-system"] } [target.'cfg(target_os = "macos")'.dependencies] -ndarray-linalg = { version = "0.18.1", features = ["openblas-system"] } +ndarray-linalg = { version = "0.17", features = ["openblas-system"] } [target.'cfg(windows)'.dependencies] -ndarray-linalg = { version = "0.18.1", features = ["intel-mkl-static"] } +ndarray-linalg = { version = "0.17", features = ["intel-mkl-static"] } [dev-dependencies] criterion = { workspace = true } diff --git a/crates/ci-r/Cargo.toml b/crates/ci-r/Cargo.toml index fea920b..d9c8023 100644 --- a/crates/ci-r/Cargo.toml +++ b/crates/ci-r/Cargo.toml @@ -10,7 +10,7 @@ repository = "" [dependencies] ci_core = { path = "../ci-core" } anyhow = { workspace = true} -extendr-api = "0.9.0" +extendr-api = { version = "0.9.0", features = ["ndarray"] } [lints] workspace = true diff --git a/crates/ci-r/src/lib.rs b/crates/ci-r/src/lib.rs index d68f262..c732cda 100644 --- a/crates/ci-r/src/lib.rs +++ b/crates/ci-r/src/lib.rs @@ -1,12 +1,13 @@ use extendr_api::prelude::*; use ci_core::strategy::{CITest, TestResult}; use ci_core::ci_tests::chi_squared::ChiSquared; +use ndarray::{ArrayView1, ArrayView2}; use anyhow; #[extendr] pub struct RChiSquared { - citest: Option, + citest: ChiSquared, } #[extendr] @@ -15,16 +16,41 @@ impl RChiSquared { boolean: bool, significance_level: f64 ) -> Self { - citest = ChiSquared::new(boolean, significance_level); - RChiSquared{citest} + let citest = ChiSquared::new(boolean, significance_level); + RChiSquared{citest: citest} } fn run_test( &self, - x_values: Array1, - y_values: Array1, - z: Array2, - ) -> anyhow::Result { - self.citest?.run_test(x_values, y_values, z) + x_values: ArrayView1, + y_values: ArrayView1, + z: ArrayView2, + ) -> anyhow::Result<()> { + + let result = self.citest.run_test( + x_values.to_owned(), + y_values.to_owned(), + z.to_owned())?; + Ok(self.test_result_to_robj(result)) + } +} + +fn test_result_to_robj(r: TestResult) -> Robj { + match r { + TestResult::PValue(p, coef) => list!( + kind = "pvalue", + p_value = p, + coefficient = coef, + ).into(), + TestResult::Statistic(stat, p, df) => list!( + kind = "statistic", + statistic = stat, + p_value = p, + df = df as i32, + ).into(), + TestResult::Boolean(b) => list!( + kind = "boolean", + independent = b, + ).into(), } } \ No newline at end of file From e7ab9d81c1348998510411e4ee350d9d9334c7e4 Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:50:25 +0200 Subject: [PATCH 06/20] feat: potentially working macro version? --- Cargo.lock | 18 ++-- Cargo.toml | 2 +- crates/ci-core/src/ci_tests/mod.rs | 12 +-- crates/ci-r/Cargo.toml | 18 ---- crates/ci-r/README.md | 1 - crates/cir/.Rbuildignore | 6 ++ crates/cir/.gitignore | 3 + crates/cir/.vscode/settings.json | 15 +++ crates/cir/DESCRIPTION | 15 +++ crates/cir/NAMESPACE | 3 + crates/cir/R/extendr-wrappers.R | 12 +++ crates/cir/README.md | 17 ++++ crates/cir/cleanup | 1 + crates/cir/cleanup.win | 1 + crates/cir/configure | 3 + crates/cir/configure.win | 2 + crates/cir/src/.gitignore | 8 ++ crates/cir/src/Makevars.in | 52 ++++++++++ crates/cir/src/Makevars.win.in | 51 ++++++++++ crates/cir/src/cir-win.def | 2 + crates/cir/src/entrypoint.c | 10 ++ crates/cir/src/rust/Cargo.toml | 23 +++++ crates/cir/src/rust/document.rs | 20 ++++ crates/{ci-r => cir/src/rust}/src/lib.rs | 16 +++- crates/cir/src/rust/src/macro_tryout.rs | 86 +++++++++++++++++ crates/cir/tools/config.R | 112 ++++++++++++++++++++++ crates/cir/tools/msrv.R | 116 +++++++++++++++++++++++ 27 files changed, 585 insertions(+), 40 deletions(-) delete mode 100644 crates/ci-r/Cargo.toml delete mode 100644 crates/ci-r/README.md create mode 100644 crates/cir/.Rbuildignore create mode 100644 crates/cir/.gitignore create mode 100644 crates/cir/.vscode/settings.json create mode 100644 crates/cir/DESCRIPTION create mode 100644 crates/cir/NAMESPACE create mode 100644 crates/cir/R/extendr-wrappers.R create mode 100644 crates/cir/README.md create mode 100644 crates/cir/cleanup create mode 100644 crates/cir/cleanup.win create mode 100755 crates/cir/configure create mode 100644 crates/cir/configure.win create mode 100644 crates/cir/src/.gitignore create mode 100644 crates/cir/src/Makevars.in create mode 100644 crates/cir/src/Makevars.win.in create mode 100644 crates/cir/src/cir-win.def create mode 100644 crates/cir/src/entrypoint.c create mode 100644 crates/cir/src/rust/Cargo.toml create mode 100644 crates/cir/src/rust/document.rs rename crates/{ci-r => cir/src/rust}/src/lib.rs (86%) create mode 100644 crates/cir/src/rust/src/macro_tryout.rs create mode 100644 crates/cir/tools/config.R create mode 100644 crates/cir/tools/msrv.R diff --git a/Cargo.lock b/Cargo.lock index dc51d34..5607ad8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -213,15 +213,6 @@ dependencies = [ "pyo3", ] -[[package]] -name = "ci_r" -version = "0.1.0" -dependencies = [ - "anyhow", - "ci_core", - "extendr-api", -] - [[package]] name = "ciborium" version = "0.2.2" @@ -249,6 +240,15 @@ dependencies = [ "half", ] +[[package]] +name = "cir" +version = "0.1.0" +dependencies = [ + "anyhow", + "ci_core", + "extendr-api", +] + [[package]] name = "clap" version = "4.6.1" diff --git a/Cargo.toml b/Cargo.toml index d21440a..8e28563 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = [ "crates/ci-core", "crates/ci-python", - "crates/ci-r", + "crates/cir/src/rust", "crates/ci-js", ] resolver = "2" diff --git a/crates/ci-core/src/ci_tests/mod.rs b/crates/ci-core/src/ci_tests/mod.rs index 34c1f75..732cb28 100644 --- a/crates/ci-core/src/ci_tests/mod.rs +++ b/crates/ci-core/src/ci_tests/mod.rs @@ -1,10 +1,10 @@ pub mod chi_squared; -mod cressie_read; -mod freeman_tukey; -mod log_likelihood; -mod modified_likelihood; -mod pearson_correlation; -mod pearson_equivalence; +pub mod cressie_read; +pub mod freeman_tukey; +pub mod log_likelihood; +pub mod modified_likelihood; +pub mod pearson_correlation; +pub mod pearson_equivalence; use chi_squared::ChiSquared; use cressie_read::CressieRead; diff --git a/crates/ci-r/Cargo.toml b/crates/ci-r/Cargo.toml deleted file mode 100644 index d9c8023..0000000 --- a/crates/ci-r/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "ci_r" -version = "0.1.0" -edition = "2021" -authors = ["Your Team "] -license = "" -description = "extendr bindings for Conditional Independence Testing" -repository = "" - -[dependencies] -ci_core = { path = "../ci-core" } -anyhow = { workspace = true} -extendr-api = { version = "0.9.0", features = ["ndarray"] } - -[lints] -workspace = true - -[lib] diff --git a/crates/ci-r/README.md b/crates/ci-r/README.md deleted file mode 100644 index 41e4a99..0000000 --- a/crates/ci-r/README.md +++ /dev/null @@ -1 +0,0 @@ -extendr bindings \ No newline at end of file diff --git a/crates/cir/.Rbuildignore b/crates/cir/.Rbuildignore new file mode 100644 index 0000000..a020d1a --- /dev/null +++ b/crates/cir/.Rbuildignore @@ -0,0 +1,6 @@ +^\.vscode$ +^src/\.cargo$ +^src/rust/vendor$ +^src/rust/target$ +^src/Makevars$ +^src/Makevars\.win$ diff --git a/crates/cir/.gitignore b/crates/cir/.gitignore new file mode 100644 index 0000000..20cff42 --- /dev/null +++ b/crates/cir/.gitignore @@ -0,0 +1,3 @@ +src/rust/vendor +src/Makevars +src/Makevars.win diff --git a/crates/cir/.vscode/settings.json b/crates/cir/.vscode/settings.json new file mode 100644 index 0000000..a0fd38b --- /dev/null +++ b/crates/cir/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "rust-analyzer.linkedProjects": [ + "${workspaceFolder}/src/rust/Cargo.toml" + ], + "files.associations": { + "Makevars": "makefile", + "Makevars.in": "makefile", + "Makevars.win": "makefile", + "Makevars.win.in": "makefile", + "configure": "shellscript", + "configure.win": "shellscript", + "cleanup": "shellscript", + "cleanup.win": "shellscript" + } +} diff --git a/crates/cir/DESCRIPTION b/crates/cir/DESCRIPTION new file mode 100644 index 0000000..bd80d1b --- /dev/null +++ b/crates/cir/DESCRIPTION @@ -0,0 +1,15 @@ +Package: cir +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.3 +Config/rextendr/version: 0.5.0 +SystemRequirements: Cargo (Rust's package manager), rustc >= 1.65.0, xz +Depends: + R (>= 4.2) diff --git a/crates/cir/NAMESPACE b/crates/cir/NAMESPACE new file mode 100644 index 0000000..5d83515 --- /dev/null +++ b/crates/cir/NAMESPACE @@ -0,0 +1,3 @@ +# Generated by roxygen2: do not edit by hand + +useDynLib(cir, .registration = TRUE) diff --git a/crates/cir/R/extendr-wrappers.R b/crates/cir/R/extendr-wrappers.R new file mode 100644 index 0000000..287fede --- /dev/null +++ b/crates/cir/R/extendr-wrappers.R @@ -0,0 +1,12 @@ +# Generated by extendr: Do not edit by hand +# nolint start + +#' @usage NULL +#' @useDynLib cir, .registration = TRUE +NULL + +#' Return string `"Hello world!"` to R. +#' @export +hello_world <- function() .Call(wrap__hello_world) + +# nolint end diff --git a/crates/cir/README.md b/crates/cir/README.md new file mode 100644 index 0000000..195eb48 --- /dev/null +++ b/crates/cir/README.md @@ -0,0 +1,17 @@ +extendr bindings + +##Usage + +Install R +In your command line open an R session by typing: R +install.packages("rextendr") (this takes 5-15 mins) + +usethis::create_package("crates/ci-r-pkg", open = FALSE) +setwd("crates/ci-r-pkg") +rextendr::use_extendr() + +to regenerate bindings -> install.packages("devtools") + +devtools::document() # generates R wrappers +devtools::load_all() # compiles Rust + loads the package + diff --git a/crates/cir/cleanup b/crates/cir/cleanup new file mode 100644 index 0000000..e346d71 --- /dev/null +++ b/crates/cir/cleanup @@ -0,0 +1 @@ +rm -f src/Makevars diff --git a/crates/cir/cleanup.win b/crates/cir/cleanup.win new file mode 100644 index 0000000..a182174 --- /dev/null +++ b/crates/cir/cleanup.win @@ -0,0 +1 @@ +rm -f src/Makevars.win diff --git a/crates/cir/configure b/crates/cir/configure new file mode 100755 index 0000000..c608b11 --- /dev/null +++ b/crates/cir/configure @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +: "${R_HOME=`R RHOME`}" +"${R_HOME}/bin/Rscript" tools/config.R diff --git a/crates/cir/configure.win b/crates/cir/configure.win new file mode 100644 index 0000000..57eb255 --- /dev/null +++ b/crates/cir/configure.win @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +"${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" tools/config.R diff --git a/crates/cir/src/.gitignore b/crates/cir/src/.gitignore new file mode 100644 index 0000000..24e51fc --- /dev/null +++ b/crates/cir/src/.gitignore @@ -0,0 +1,8 @@ +*.o +*.so +*.dll +target +.cargo +rust/vendor +Makevars +Makevars.win diff --git a/crates/cir/src/Makevars.in b/crates/cir/src/Makevars.in new file mode 100644 index 0000000..a687d41 --- /dev/null +++ b/crates/cir/src/Makevars.in @@ -0,0 +1,52 @@ +TARGET_DIR = ./rust/target +LIBDIR = $(TARGET_DIR)/@LIBDIR@ +STATLIB = $(LIBDIR)/libcir.a +PKG_LIBS = -L$(LIBDIR) -lcir + +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. +# +# If a vendor directory exists, it is used for offline compilation. Otherwise if +# vendor.tar.xz exists, it is unzipped and used for offline compilation. +$(STATLIB): + + if [ -d ./vendor ]; then \ + echo "=== Using offline vendor directory ==="; \ + mkdir -p $(CARGOTMP) && \ + cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ + elif [ -f ./rust/vendor.tar.xz ]; then \ + echo "=== Using offline vendor tarball ==="; \ + 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" && \ + @PANIC_EXPORTS@RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build @CRAN_FLAGS@ --lib @PROFILE@ --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) @TARGET@ + + export CARGO_HOME=$(CARGOTMP) && \ + export PATH="$(PATH):$(HOME)/.cargo/bin" && \ + cargo run @CRAN_FLAGS@ --bin document --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/crates/cir/src/Makevars.win.in b/crates/cir/src/Makevars.win.in new file mode 100644 index 0000000..6f72063 --- /dev/null +++ b/crates/cir/src/Makevars.win.in @@ -0,0 +1,51 @@ +TARGET = $(subst 64,x86_64,$(subst 32,i686,$(WIN)))-pc-windows-gnu + +TARGET_DIR = ./rust/target +LIBDIR = $(TARGET_DIR)/$(TARGET)/@LIBDIR@ +STATLIB = $(LIBDIR)/libcir.a +PKG_LIBS = -L$(LIBDIR) -lcir -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 a vendor directory exists, it is used for offline compilation. Otherwise if + # vendor.tar.xz exists, it is unzipped and used for offline compilation. + if [ -d ./vendor ]; then \ + echo "=== Using offline vendor directory ==="; \ + mkdir -p $(CARGOTMP) && \ + cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ + elif [ -f ./rust/vendor.tar.xz ]; then \ + echo "=== Using offline vendor tarball ==="; \ + 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) + + # Generate wrappers + export CARGO_HOME=$(CARGOTMP) && \ + export PATH="$(PATH):$(HOME)/.cargo/bin" && \ + cargo run @CRAN_FLAGS@ --bin document --target $(TARGET) --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/crates/cir/src/cir-win.def b/crates/cir/src/cir-win.def new file mode 100644 index 0000000..6fe96d6 --- /dev/null +++ b/crates/cir/src/cir-win.def @@ -0,0 +1,2 @@ +EXPORTS +R_init_cir diff --git a/crates/cir/src/entrypoint.c b/crates/cir/src/entrypoint.c new file mode 100644 index 0000000..fc87ffb --- /dev/null +++ b/crates/cir/src/entrypoint.c @@ -0,0 +1,10 @@ +// We need to forward routine registration from C to Rust +// to avoid the linker removing the static library. + +void R_init_cir_extendr(void *dll); +void register_extendr_panic_hook(void); + +void R_init_cir(void *dll) { + register_extendr_panic_hook(); + R_init_cir_extendr(dll); +} diff --git a/crates/cir/src/rust/Cargo.toml b/crates/cir/src/rust/Cargo.toml new file mode 100644 index 0000000..f0b3dbb --- /dev/null +++ b/crates/cir/src/rust/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = 'cir' +publish = false +version = '0.1.0' +edition = '2021' +rust-version = '1.65' + +[lib] +crate-type = [ 'rlib', 'staticlib' ] +name = 'cir' + +[[bin]] +name = 'document' +path = 'document.rs' + +[dependencies] +ci_core = { path = "../../../ci-core" } +anyhow = "1" +extendr-api = { version = '0.9.0', features = ['ndarray'] } + +[profile.release] +lto = true +codegen-units = 1 diff --git a/crates/cir/src/rust/document.rs b/crates/cir/src/rust/document.rs new file mode 100644 index 0000000..1dd2ff7 --- /dev/null +++ b/crates/cir/src/rust/document.rs @@ -0,0 +1,20 @@ +// Generated by extendr: Do not edit by hand +fn main() -> Result<(), Box> { + let wrapper_path = "../R/extendr-wrappers.R"; + let header = "\ + # Generated by extendr: Do not edit by hand\n\ + # nolint start\n\ + \n\ + #' @usage NULL\n\ + #' @useDynLib cir, .registration = TRUE\n\ + NULL\n\ + \n\ + "; + let footer = "# nolint end\n"; + let wrappers = cir::get_cir_metadata() + .make_r_wrappers(true, "cir") + .map_err(|e| format!("failed to generate wrappers: {e}"))?; + std::fs::write(wrapper_path, format!("{header}{wrappers}{footer}")) + .map_err(|e| format!("failed to write {wrapper_path}: {e}"))?; + Ok(()) +} \ No newline at end of file diff --git a/crates/ci-r/src/lib.rs b/crates/cir/src/rust/src/lib.rs similarity index 86% rename from crates/ci-r/src/lib.rs rename to crates/cir/src/rust/src/lib.rs index c732cda..b6806a3 100644 --- a/crates/ci-r/src/lib.rs +++ b/crates/cir/src/rust/src/lib.rs @@ -3,7 +3,7 @@ use ci_core::strategy::{CITest, TestResult}; use ci_core::ci_tests::chi_squared::ChiSquared; use ndarray::{ArrayView1, ArrayView2}; use anyhow; - +mod macro_tryout; #[extendr] pub struct RChiSquared { @@ -25,13 +25,14 @@ impl RChiSquared { x_values: ArrayView1, y_values: ArrayView1, z: ArrayView2, - ) -> anyhow::Result<()> { + ) -> anyhow::Result { let result = self.citest.run_test( x_values.to_owned(), y_values.to_owned(), - z.to_owned())?; - Ok(self.test_result_to_robj(result)) + z.to_owned()) + ?; + Ok(test_result_to_robj(result)) } } @@ -53,4 +54,9 @@ fn test_result_to_robj(r: TestResult) -> Robj { independent = b, ).into(), } -} \ No newline at end of file +} + +extendr_module! { + mod cir; + impl RChiSquared; +} diff --git a/crates/cir/src/rust/src/macro_tryout.rs b/crates/cir/src/rust/src/macro_tryout.rs new file mode 100644 index 0000000..352f870 --- /dev/null +++ b/crates/cir/src/rust/src/macro_tryout.rs @@ -0,0 +1,86 @@ +use extendr_api::prelude::*; +use ci_core::strategy::{CITest, TestResult}; +use ci_core::ci_tests::{ + chi_squared::ChiSquared, + cressie_read::CressieRead, + freeman_tukey::FreemanTukey, + log_likelihood::LogLikelihood, + modified_likelihood::ModifiedLikelihood, + pearson_correlation::PearsonCorrelation, + pearson_equivalence::PearsonEquivalence, +}; +use ndarray::{ArrayView1, ArrayView2}; + +macro_rules! r_ci_test { + ($r_name:ident, $inner:ty) => { + #[extendr] + pub struct $r_name { + citest: $inner + } + + #[extendr] + impl $r_name { + fn new( + boolean: bool, + significance_level: f64 + ) -> Self { + let citest = <$inner>::new(boolean, significance_level); + $r_name{citest: citest} + } + + fn run_test( + &self, + x_values: ArrayView1, + y_values: ArrayView1, + z: ArrayView2, + ) -> anyhow::Result { + + let result = self.citest.run_test( + x_values.to_owned(), + y_values.to_owned(), + z.to_owned())?; + Ok(test_result_to_robj(result)) + } + } + }; +} + +r_ci_test!(RChiSquared, ChiSquared); +r_ci_test!(RLogLikelihood, LogLikelihood); +r_ci_test!(RCressieRead, CressieRead); +r_ci_test!(RPearsonCorrelation, PearsonCorrelation); +r_ci_test!(RFreemanTukey, FreemanTukey); +r_ci_test!(RModifiedLikelihood, ModifiedLikelihood); +r_ci_test!(RPearsonEquivalence, PearsonEquivalence); + + +extendr_module! { + mod cir; + impl RChiSquared; + impl RLogLikelihood; + impl RCressieRead; + impl RPearsonCorrelation; + impl RFreemanTukey; + impl RModifiedLikelihood; + impl RPearsonEquivalence; +} + +fn test_result_to_robj(r: TestResult) -> Robj { + match r { + TestResult::PValue(p, coef) => list!( + kind = "pvalue", + p_value = p, + coefficient = coef, + ).into(), + TestResult::Statistic(stat, p, df) => list!( + kind = "statistic", + statistic = stat, + p_value = p, + df = df as i32, + ).into(), + TestResult::Boolean(b) => list!( + kind = "boolean", + independent = b, + ).into(), + } +} \ No newline at end of file diff --git a/crates/cir/tools/config.R b/crates/cir/tools/config.R new file mode 100644 index 0000000..06d57cd --- /dev/null +++ b/crates/cir/tools/config.R @@ -0,0 +1,112 @@ +# 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), "") + +# add panic exports only for WASM builds +.panic_exports <- ifelse( + is_wasm, + "CARGO_PROFILE_DEV_PANIC=\"abort\" CARGO_PROFILE_RELEASE_PANIC=\"abort\" ", + "" +) + +# 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/.wasm} +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 = _) |> + gsub("@PANIC_EXPORTS@", .panic_exports, 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/crates/cir/tools/msrv.R b/crates/cir/tools/msrv.R new file mode 100644 index 0000000..59a61ab --- /dev/null +++ b/crates/cir/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)) From 6995d353e9e545500e56faeccc5a6ab320dc76b9 Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:45:34 +0200 Subject: [PATCH 07/20] feat: working basic r bindings --- crates/cir/NAMESPACE | 14 +++ crates/cir/R/extendr-wrappers.R | 84 +++++++++++++- crates/cir/README.md | 3 +- crates/cir/src/rust/src/lib.rs | 107 +++++++++--------- crates/cir/src/rust/src/macro_tryout.rs | 86 -------------- crates/cir/src/rust/src/util.rs | 22 ++++ crates/cir/tests/testthat/test-chi_squared.R | 34 ++++++ crates/cir/tests/testthat/test-cressie_read.R | 0 .../cir/tests/testthat/test-freeman_tukey.R | 0 .../cir/tests/testthat/test-log_likelihood.R | 0 .../tests/testthat/test-modified_likelihood.R | 0 .../tests/testthat/test-pearson_correlation.R | 0 .../tests/testthat/test-pearson_equivalence.R | 0 13 files changed, 210 insertions(+), 140 deletions(-) delete mode 100644 crates/cir/src/rust/src/macro_tryout.rs create mode 100644 crates/cir/src/rust/src/util.rs create mode 100644 crates/cir/tests/testthat/test-chi_squared.R create mode 100644 crates/cir/tests/testthat/test-cressie_read.R create mode 100644 crates/cir/tests/testthat/test-freeman_tukey.R create mode 100644 crates/cir/tests/testthat/test-log_likelihood.R create mode 100644 crates/cir/tests/testthat/test-modified_likelihood.R create mode 100644 crates/cir/tests/testthat/test-pearson_correlation.R create mode 100644 crates/cir/tests/testthat/test-pearson_equivalence.R diff --git a/crates/cir/NAMESPACE b/crates/cir/NAMESPACE index 5d83515..3d4893f 100644 --- a/crates/cir/NAMESPACE +++ b/crates/cir/NAMESPACE @@ -1,3 +1,17 @@ # Generated by roxygen2: do not edit by hand +S3method("$",RChiSquared) +S3method("$",RCressieRead) +S3method("$",RFreemanTukey) +S3method("$",RLogLikelihood) +S3method("$",RModifiedLikelihood) +S3method("$",RPearsonCorrelation) +S3method("$",RPearsonEquivalence) +S3method("[[",RChiSquared) +S3method("[[",RCressieRead) +S3method("[[",RFreemanTukey) +S3method("[[",RLogLikelihood) +S3method("[[",RModifiedLikelihood) +S3method("[[",RPearsonCorrelation) +S3method("[[",RPearsonEquivalence) useDynLib(cir, .registration = TRUE) diff --git a/crates/cir/R/extendr-wrappers.R b/crates/cir/R/extendr-wrappers.R index 287fede..51b726c 100644 --- a/crates/cir/R/extendr-wrappers.R +++ b/crates/cir/R/extendr-wrappers.R @@ -5,8 +5,88 @@ #' @useDynLib cir, .registration = TRUE NULL -#' Return string `"Hello world!"` to R. +RChiSquared <- new.env(parent = emptyenv()) + +RChiSquared$new <- function(boolean, significance_level) .Call(wrap__RChiSquared__new, boolean, significance_level) + +RChiSquared$run_test <- function(x_values, y_values, z) .Call(wrap__RChiSquared__run_test, self, x_values, y_values, z) + +#' @export +`$.RChiSquared` <- function (self, name) { func <- RChiSquared[[name]]; environment(func) <- environment(); func } + +#' @export +`[[.RChiSquared` <- `$.RChiSquared` + +RLogLikelihood <- new.env(parent = emptyenv()) + +RLogLikelihood$new <- function(boolean, significance_level) .Call(wrap__RLogLikelihood__new, boolean, significance_level) + +RLogLikelihood$run_test <- function(x_values, y_values, z) .Call(wrap__RLogLikelihood__run_test, self, x_values, y_values, z) + +#' @export +`$.RLogLikelihood` <- function (self, name) { func <- RLogLikelihood[[name]]; environment(func) <- environment(); func } + +#' @export +`[[.RLogLikelihood` <- `$.RLogLikelihood` + +RCressieRead <- new.env(parent = emptyenv()) + +RCressieRead$new <- function(boolean, significance_level) .Call(wrap__RCressieRead__new, boolean, significance_level) + +RCressieRead$run_test <- function(x_values, y_values, z) .Call(wrap__RCressieRead__run_test, self, x_values, y_values, z) + +#' @export +`$.RCressieRead` <- function (self, name) { func <- RCressieRead[[name]]; environment(func) <- environment(); func } + +#' @export +`[[.RCressieRead` <- `$.RCressieRead` + +RPearsonCorrelation <- new.env(parent = emptyenv()) + +RPearsonCorrelation$new <- function(boolean, significance_level) .Call(wrap__RPearsonCorrelation__new, boolean, significance_level) + +RPearsonCorrelation$run_test <- function(x_values, y_values, z) .Call(wrap__RPearsonCorrelation__run_test, self, x_values, y_values, z) + +#' @export +`$.RPearsonCorrelation` <- function (self, name) { func <- RPearsonCorrelation[[name]]; environment(func) <- environment(); func } + +#' @export +`[[.RPearsonCorrelation` <- `$.RPearsonCorrelation` + +RFreemanTukey <- new.env(parent = emptyenv()) + +RFreemanTukey$new <- function(boolean, significance_level) .Call(wrap__RFreemanTukey__new, boolean, significance_level) + +RFreemanTukey$run_test <- function(x_values, y_values, z) .Call(wrap__RFreemanTukey__run_test, self, x_values, y_values, z) + +#' @export +`$.RFreemanTukey` <- function (self, name) { func <- RFreemanTukey[[name]]; environment(func) <- environment(); func } + +#' @export +`[[.RFreemanTukey` <- `$.RFreemanTukey` + +RModifiedLikelihood <- new.env(parent = emptyenv()) + +RModifiedLikelihood$new <- function(boolean, significance_level) .Call(wrap__RModifiedLikelihood__new, boolean, significance_level) + +RModifiedLikelihood$run_test <- function(x_values, y_values, z) .Call(wrap__RModifiedLikelihood__run_test, self, x_values, y_values, z) + +#' @export +`$.RModifiedLikelihood` <- function (self, name) { func <- RModifiedLikelihood[[name]]; environment(func) <- environment(); func } + +#' @export +`[[.RModifiedLikelihood` <- `$.RModifiedLikelihood` + +RPearsonEquivalence <- new.env(parent = emptyenv()) + +RPearsonEquivalence$new <- function(boolean, significance_level) .Call(wrap__RPearsonEquivalence__new, boolean, significance_level) + +RPearsonEquivalence$run_test <- function(x_values, y_values, z) .Call(wrap__RPearsonEquivalence__run_test, self, x_values, y_values, z) + +#' @export +`$.RPearsonEquivalence` <- function (self, name) { func <- RPearsonEquivalence[[name]]; environment(func) <- environment(); func } + #' @export -hello_world <- function() .Call(wrap__hello_world) +`[[.RPearsonEquivalence` <- `$.RPearsonEquivalence` # nolint end diff --git a/crates/cir/README.md b/crates/cir/README.md index 195eb48..e4c5279 100644 --- a/crates/cir/README.md +++ b/crates/cir/README.md @@ -12,6 +12,7 @@ rextendr::use_extendr() to regenerate bindings -> install.packages("devtools") -devtools::document() # generates R wrappers +devtools::document() # generates R bindings devtools::load_all() # compiles Rust + loads the package +devtools::test() # Run all tests \ No newline at end of file diff --git a/crates/cir/src/rust/src/lib.rs b/crates/cir/src/rust/src/lib.rs index b6806a3..6da4982 100644 --- a/crates/cir/src/rust/src/lib.rs +++ b/crates/cir/src/rust/src/lib.rs @@ -1,62 +1,67 @@ use extendr_api::prelude::*; -use ci_core::strategy::{CITest, TestResult}; -use ci_core::ci_tests::chi_squared::ChiSquared; +use ci_core::strategy::CITest; +use ci_core::ci_tests::{ + chi_squared::ChiSquared, + cressie_read::CressieRead, + freeman_tukey::FreemanTukey, + log_likelihood::LogLikelihood, + modified_likelihood::ModifiedLikelihood, + pearson_correlation::PearsonCorrelation, + pearson_equivalence::PearsonEquivalence, +}; use ndarray::{ArrayView1, ArrayView2}; -use anyhow; -mod macro_tryout; +mod util; -#[extendr] -pub struct RChiSquared { - citest: ChiSquared, -} - -#[extendr] -impl RChiSquared { - fn new( - boolean: bool, - significance_level: f64 - ) -> Self { - let citest = ChiSquared::new(boolean, significance_level); - RChiSquared{citest: citest} - } +macro_rules! r_ci_test { + ($r_name:ident, $inner:ty) => { + #[extendr] + pub struct $r_name { + citest: $inner + } + + #[extendr] + impl $r_name { + fn new( + boolean: bool, + significance_level: f64 + ) -> Self { + let citest = <$inner>::new(boolean, significance_level); + $r_name{citest: citest} + } - fn run_test( - &self, - x_values: ArrayView1, - y_values: ArrayView1, - z: ArrayView2, - ) -> anyhow::Result { + fn run_test( + &self, + x_values: ArrayView1, + y_values: ArrayView1, + z: ArrayView2, + ) -> anyhow::Result { - let result = self.citest.run_test( - x_values.to_owned(), - y_values.to_owned(), - z.to_owned()) - ?; - Ok(test_result_to_robj(result)) - } + let result = self.citest.run_test( + x_values.to_owned(), + y_values.to_owned(), + z.to_owned())?; + Ok(util::test_result_to_robj(result)) + } + } + }; } -fn test_result_to_robj(r: TestResult) -> Robj { - match r { - TestResult::PValue(p, coef) => list!( - kind = "pvalue", - p_value = p, - coefficient = coef, - ).into(), - TestResult::Statistic(stat, p, df) => list!( - kind = "statistic", - statistic = stat, - p_value = p, - df = df as i32, - ).into(), - TestResult::Boolean(b) => list!( - kind = "boolean", - independent = b, - ).into(), - } -} +r_ci_test!(RChiSquared, ChiSquared); +r_ci_test!(RLogLikelihood, LogLikelihood); +r_ci_test!(RCressieRead, CressieRead); +r_ci_test!(RPearsonCorrelation, PearsonCorrelation); +r_ci_test!(RFreemanTukey, FreemanTukey); +r_ci_test!(RModifiedLikelihood, ModifiedLikelihood); +r_ci_test!(RPearsonEquivalence, PearsonEquivalence); + extendr_module! { mod cir; impl RChiSquared; -} + impl RLogLikelihood; + impl RCressieRead; + impl RPearsonCorrelation; + impl RFreemanTukey; + impl RModifiedLikelihood; + impl RPearsonEquivalence; +} \ No newline at end of file diff --git a/crates/cir/src/rust/src/macro_tryout.rs b/crates/cir/src/rust/src/macro_tryout.rs deleted file mode 100644 index 352f870..0000000 --- a/crates/cir/src/rust/src/macro_tryout.rs +++ /dev/null @@ -1,86 +0,0 @@ -use extendr_api::prelude::*; -use ci_core::strategy::{CITest, TestResult}; -use ci_core::ci_tests::{ - chi_squared::ChiSquared, - cressie_read::CressieRead, - freeman_tukey::FreemanTukey, - log_likelihood::LogLikelihood, - modified_likelihood::ModifiedLikelihood, - pearson_correlation::PearsonCorrelation, - pearson_equivalence::PearsonEquivalence, -}; -use ndarray::{ArrayView1, ArrayView2}; - -macro_rules! r_ci_test { - ($r_name:ident, $inner:ty) => { - #[extendr] - pub struct $r_name { - citest: $inner - } - - #[extendr] - impl $r_name { - fn new( - boolean: bool, - significance_level: f64 - ) -> Self { - let citest = <$inner>::new(boolean, significance_level); - $r_name{citest: citest} - } - - fn run_test( - &self, - x_values: ArrayView1, - y_values: ArrayView1, - z: ArrayView2, - ) -> anyhow::Result { - - let result = self.citest.run_test( - x_values.to_owned(), - y_values.to_owned(), - z.to_owned())?; - Ok(test_result_to_robj(result)) - } - } - }; -} - -r_ci_test!(RChiSquared, ChiSquared); -r_ci_test!(RLogLikelihood, LogLikelihood); -r_ci_test!(RCressieRead, CressieRead); -r_ci_test!(RPearsonCorrelation, PearsonCorrelation); -r_ci_test!(RFreemanTukey, FreemanTukey); -r_ci_test!(RModifiedLikelihood, ModifiedLikelihood); -r_ci_test!(RPearsonEquivalence, PearsonEquivalence); - - -extendr_module! { - mod cir; - impl RChiSquared; - impl RLogLikelihood; - impl RCressieRead; - impl RPearsonCorrelation; - impl RFreemanTukey; - impl RModifiedLikelihood; - impl RPearsonEquivalence; -} - -fn test_result_to_robj(r: TestResult) -> Robj { - match r { - TestResult::PValue(p, coef) => list!( - kind = "pvalue", - p_value = p, - coefficient = coef, - ).into(), - TestResult::Statistic(stat, p, df) => list!( - kind = "statistic", - statistic = stat, - p_value = p, - df = df as i32, - ).into(), - TestResult::Boolean(b) => list!( - kind = "boolean", - independent = b, - ).into(), - } -} \ No newline at end of file diff --git a/crates/cir/src/rust/src/util.rs b/crates/cir/src/rust/src/util.rs new file mode 100644 index 0000000..9fe6c6f --- /dev/null +++ b/crates/cir/src/rust/src/util.rs @@ -0,0 +1,22 @@ +use ci_core::strategy::{TestResult}; +use extendr_api::prelude::*; + +pub fn test_result_to_robj(r: TestResult) -> Robj { + match r { + TestResult::PValue(p, coef) => list!( + kind = "pvalue", + p_value = p, + coefficient = coef, + ).into(), + TestResult::Statistic(p, stat, df) => list!( + kind = "statistic", + statistic = stat, + p_value = p, + df = df as i32, + ).into(), + TestResult::Boolean(b) => list!( + kind = "boolean", + independent = b, + ).into(), + } +} \ No newline at end of file diff --git a/crates/cir/tests/testthat/test-chi_squared.R b/crates/cir/tests/testthat/test-chi_squared.R new file mode 100644 index 0000000..5ad9504 --- /dev/null +++ b/crates/cir/tests/testthat/test-chi_squared.R @@ -0,0 +1,34 @@ +library(cir) + +test = RChiSquared$new(FALSE, 0.05) + +test_that("independent data is not rejected", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow=8, ncol=0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "statistic") + expect_true(result$statistic < 1e-9) + expect_true(result$p_value >= 0.99) + expect_equal(result$df, 1) + test = RChiSquared$new(TRUE, 0.05) + result = test$run_test(x, y, z) + expect_true(result$independent) +}) + +test_that("dependent data is rejected", { + x = c(1., 1., 1., 1., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 2., 2., 2., 2.) + z = matrix(0, nrow=8, ncol=0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "statistic") + expect_true(abs(result$statistic - 8.0) < 1e-9) + expect_true(abs(result$p_value - 0.004677734981047276) < 1e-12) + expect_equal(result$df, 1) + test = RChiSquared$new(TRUE, 0.05) + result = test$run_test(x, y, z) + expect_false(result$independent) +}) + diff --git a/crates/cir/tests/testthat/test-cressie_read.R b/crates/cir/tests/testthat/test-cressie_read.R new file mode 100644 index 0000000..e69de29 diff --git a/crates/cir/tests/testthat/test-freeman_tukey.R b/crates/cir/tests/testthat/test-freeman_tukey.R new file mode 100644 index 0000000..e69de29 diff --git a/crates/cir/tests/testthat/test-log_likelihood.R b/crates/cir/tests/testthat/test-log_likelihood.R new file mode 100644 index 0000000..e69de29 diff --git a/crates/cir/tests/testthat/test-modified_likelihood.R b/crates/cir/tests/testthat/test-modified_likelihood.R new file mode 100644 index 0000000..e69de29 diff --git a/crates/cir/tests/testthat/test-pearson_correlation.R b/crates/cir/tests/testthat/test-pearson_correlation.R new file mode 100644 index 0000000..e69de29 diff --git a/crates/cir/tests/testthat/test-pearson_equivalence.R b/crates/cir/tests/testthat/test-pearson_equivalence.R new file mode 100644 index 0000000..e69de29 From 4bafaf895123bd9009091487c54e32e68ccadf3d Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:05:49 +0200 Subject: [PATCH 08/20] feat: some broken unit tests --- crates/cir/tests/testthat/helper-citest.R | 24 ++++++++++++++ crates/cir/tests/testthat/test-chi_squared.R | 33 +------------------ crates/cir/tests/testthat/test-cressie_read.R | 16 +++++++++ 3 files changed, 41 insertions(+), 32 deletions(-) create mode 100644 crates/cir/tests/testthat/helper-citest.R diff --git a/crates/cir/tests/testthat/helper-citest.R b/crates/cir/tests/testthat/helper-citest.R new file mode 100644 index 0000000..af4639f --- /dev/null +++ b/crates/cir/tests/testthat/helper-citest.R @@ -0,0 +1,24 @@ +run_citest_standard_tests = function(test) { + test_that("independent data is not rejected", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "statistic") + expect_true(result$statistic < 1e-9) + expect_true(result$p_value >= 0.99) + expect_equal(result$df, 1) + }) + + test_that("dependent data is rejected", { + x = c(1., 1., 1., 1., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 2., 2., 2., 2.) + z = matrix(0, nrow = 8, ncol = 0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "statistic") + expect_true(abs(result$statistic - 8.0) < 1e-9) + expect_equal(result$df, 1) + }) +} diff --git a/crates/cir/tests/testthat/test-chi_squared.R b/crates/cir/tests/testthat/test-chi_squared.R index 5ad9504..74bbaeb 100644 --- a/crates/cir/tests/testthat/test-chi_squared.R +++ b/crates/cir/tests/testthat/test-chi_squared.R @@ -1,34 +1,3 @@ library(cir) - -test = RChiSquared$new(FALSE, 0.05) - -test_that("independent data is not rejected", { - x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) - y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) - z = matrix(0, nrow=8, ncol=0) - - result = test$run_test(x, y, z) - expect_equal(result$kind, "statistic") - expect_true(result$statistic < 1e-9) - expect_true(result$p_value >= 0.99) - expect_equal(result$df, 1) - test = RChiSquared$new(TRUE, 0.05) - result = test$run_test(x, y, z) - expect_true(result$independent) -}) - -test_that("dependent data is rejected", { - x = c(1., 1., 1., 1., 2., 2., 2., 2.) - y = c(1., 1., 1., 1., 2., 2., 2., 2.) - z = matrix(0, nrow=8, ncol=0) - - result = test$run_test(x, y, z) - expect_equal(result$kind, "statistic") - expect_true(abs(result$statistic - 8.0) < 1e-9) - expect_true(abs(result$p_value - 0.004677734981047276) < 1e-12) - expect_equal(result$df, 1) - test = RChiSquared$new(TRUE, 0.05) - result = test$run_test(x, y, z) - expect_false(result$independent) -}) +run_citest_standard_tests(RChiSquared$new(FALSE, 0.05)) diff --git a/crates/cir/tests/testthat/test-cressie_read.R b/crates/cir/tests/testthat/test-cressie_read.R index e69de29..8361969 100644 --- a/crates/cir/tests/testthat/test-cressie_read.R +++ b/crates/cir/tests/testthat/test-cressie_read.R @@ -0,0 +1,16 @@ +library(cir) + +citest = RCressieRead$new(FALSE, 0.05) +run_citest_standard_tests(citest) + +test_that("Conditional case is independent", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(c(0., 0., 0., 0., 1., 1., 1., 1.), nrow = 8, ncol = 1) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "statistic") + expect_true(result$statistic < 1e-9) + expect_true(result$p_value >= 0.99) + expect_equal(result$df, 2) + }) \ No newline at end of file From 72fc07b4f6e72d701b04a70bdd0408eacf87ef20 Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Wed, 6 May 2026 11:12:32 +0200 Subject: [PATCH 09/20] feat: working unit tests for R bindings --- crates/cir/tests/testthat/helper-citest.R | 24 ---- crates/cir/tests/testthat/test-chi_squared.R | 47 +++++- crates/cir/tests/testthat/test-cressie_read.R | 74 +++++++++- .../cir/tests/testthat/test-freeman_tukey.R | 38 +++++ .../cir/tests/testthat/test-log_likelihood.R | 38 +++++ .../tests/testthat/test-modified_likelihood.R | 38 +++++ .../tests/testthat/test-pearson_correlation.R | 136 ++++++++++++++++++ todo.txt | 0 8 files changed, 365 insertions(+), 30 deletions(-) delete mode 100644 crates/cir/tests/testthat/helper-citest.R delete mode 100644 todo.txt diff --git a/crates/cir/tests/testthat/helper-citest.R b/crates/cir/tests/testthat/helper-citest.R deleted file mode 100644 index af4639f..0000000 --- a/crates/cir/tests/testthat/helper-citest.R +++ /dev/null @@ -1,24 +0,0 @@ -run_citest_standard_tests = function(test) { - test_that("independent data is not rejected", { - x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) - y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) - z = matrix(0, nrow = 8, ncol = 0) - - result = test$run_test(x, y, z) - expect_equal(result$kind, "statistic") - expect_true(result$statistic < 1e-9) - expect_true(result$p_value >= 0.99) - expect_equal(result$df, 1) - }) - - test_that("dependent data is rejected", { - x = c(1., 1., 1., 1., 2., 2., 2., 2.) - y = c(1., 1., 1., 1., 2., 2., 2., 2.) - z = matrix(0, nrow = 8, ncol = 0) - - result = test$run_test(x, y, z) - expect_equal(result$kind, "statistic") - expect_true(abs(result$statistic - 8.0) < 1e-9) - expect_equal(result$df, 1) - }) -} diff --git a/crates/cir/tests/testthat/test-chi_squared.R b/crates/cir/tests/testthat/test-chi_squared.R index 74bbaeb..b2e3b67 100644 --- a/crates/cir/tests/testthat/test-chi_squared.R +++ b/crates/cir/tests/testthat/test-chi_squared.R @@ -1,3 +1,48 @@ library(cir) -run_citest_standard_tests(RChiSquared$new(FALSE, 0.05)) +test = RChiSquared$new(FALSE, 0.05) +test_that("independent data is not rejected", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "statistic") + expect_true(result$statistic < 1e-9) + expect_true(result$p_value >= 0.99) + expect_equal(result$df, 1) + }) + + test_that("dependent data is rejected", { + x = c(1., 1., 1., 1., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 2., 2., 2., 2.) + z = matrix(0, nrow = 8, ncol = 0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "statistic") + expect_true(abs(result$statistic - 8.0) < 1e-9) + expect_true(abs(result$p_value - 0.004677734981047276) < 1e-12) + expect_equal(result$df, 1) + }) + +test_bool = RChiSquared$new(TRUE, 0.05) + +test_that("boolean mode returns independent=TRUE for independent data", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = test_bool$run_test(x, y, z) + expect_equal(result$kind, "boolean") + expect_true(result$independent) + }) + +test_that("boolean mode returns independent=FALSE for dependent data", { + x = c(1., 1., 1., 1., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 2., 2., 2., 2.) + z = matrix(0, nrow = 8, ncol = 0) + + result = test_bool$run_test(x, y, z) + expect_equal(result$kind, "boolean") + expect_false(result$independent) + }) \ No newline at end of file diff --git a/crates/cir/tests/testthat/test-cressie_read.R b/crates/cir/tests/testthat/test-cressie_read.R index 8361969..2510ae3 100644 --- a/crates/cir/tests/testthat/test-cressie_read.R +++ b/crates/cir/tests/testthat/test-cressie_read.R @@ -1,9 +1,52 @@ library(cir) +test = RCressieRead$new(FALSE, 0.05) +test_bool = RCressieRead$new(TRUE, 0.05) -citest = RCressieRead$new(FALSE, 0.05) -run_citest_standard_tests(citest) +test_that("unconditional independent data is not rejected", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "statistic") + expect_true(result$statistic < 1e-9) + expect_true(result$p_value > 0.99) + expect_equal(result$df, 1) + }) + +test_that("unconditional boolean accepts independent data", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = test_bool$run_test(x, y, z) + expect_equal(result$kind, "boolean") + expect_true(result$independent) + }) + +test_that("unconditional dependent data is rejected", { + x = c(1., 1., 1., 1., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 2., 2., 2., 2.) + z = matrix(0, nrow = 8, ncol = 0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "statistic") + expect_true(result$statistic > 5.0) + expect_true(result$p_value < 0.05) + expect_equal(result$df, 1) + }) + +test_that("unconditional boolean rejects dependent data", { + x = c(1., 1., 1., 1., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 2., 2., 2., 2.) + z = matrix(0, nrow = 8, ncol = 0) -test_that("Conditional case is independent", { + result = test_bool$run_test(x, y, z) + expect_equal(result$kind, "boolean") + expect_false(result$independent) + }) + +test_that("conditional independent data is not rejected", { x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) z = matrix(c(0., 0., 0., 0., 1., 1., 1., 1.), nrow = 8, ncol = 1) @@ -11,6 +54,27 @@ test_that("Conditional case is independent", { result = test$run_test(x, y, z) expect_equal(result$kind, "statistic") expect_true(result$statistic < 1e-9) - expect_true(result$p_value >= 0.99) + expect_true(result$p_value > 0.99) expect_equal(result$df, 2) - }) \ No newline at end of file + }) + +test_that("conditional boolean accepts conditionally independent data", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(c(0., 0., 0., 0., 1., 1., 1., 1.), nrow = 8, ncol = 1) + + result = test_bool$run_test(x, y, z) + expect_equal(result$kind, "boolean") + expect_true(result$independent) + }) + +test_that("conditional dependent data is rejected", { + x = c(1., 1., 2., 2., 1., 1., 2., 2.) + y = c(1., 1., 2., 2., 1., 1., 2., 2.) + z = matrix(c(0., 0., 0., 0., 1., 1., 1., 1.), nrow = 8, ncol = 1) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "statistic") + expect_true(result$statistic > 5.0) + expect_true(result$p_value < 0.05) + }) diff --git a/crates/cir/tests/testthat/test-freeman_tukey.R b/crates/cir/tests/testthat/test-freeman_tukey.R index e69de29..19629c8 100644 --- a/crates/cir/tests/testthat/test-freeman_tukey.R +++ b/crates/cir/tests/testthat/test-freeman_tukey.R @@ -0,0 +1,38 @@ +library(cir) +test = RFreemanTukey$new(FALSE, 0.05) + +test_that("independent data is not rejected", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "statistic") + expect_true(result$statistic < 1e-9) + expect_true(result$p_value >= 0.99) + expect_equal(result$df, 1) + }) + +test_that("dependent data is rejected", { + x = c(1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.) + z = matrix(0, nrow = 12, ncol = 0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "statistic") + expect_true(abs(result$statistic - 6.319453539579289) < 1e-9) + expect_true(abs(result$p_value - 0.011942042564347121) < 1e-12) + expect_equal(result$df, 1) + }) + +test_bool = RFreemanTukey$new(TRUE, 0.05) + +test_that("boolean mode returns independent=TRUE for independent data", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = test_bool$run_test(x, y, z) + expect_equal(result$kind, "boolean") + expect_true(result$independent) + }) diff --git a/crates/cir/tests/testthat/test-log_likelihood.R b/crates/cir/tests/testthat/test-log_likelihood.R index e69de29..53dee87 100644 --- a/crates/cir/tests/testthat/test-log_likelihood.R +++ b/crates/cir/tests/testthat/test-log_likelihood.R @@ -0,0 +1,38 @@ +library(cir) +test = RLogLikelihood$new(FALSE, 0.05) + +test_that("independent data is not rejected", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "statistic") + expect_true(result$statistic < 1e-9) + expect_true(result$p_value >= 0.99) + expect_equal(result$df, 1) + }) + +test_that("dependent data is rejected", { + x = c(1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.) + z = matrix(0, nrow = 12, ncol = 0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "statistic") + expect_true(abs(result$statistic - 5.822063320647374) < 1e-9) + expect_true(abs(result$p_value - 0.015826368796540195) < 1e-12) + expect_equal(result$df, 1) + }) + +test_bool = RLogLikelihood$new(TRUE, 0.05) + +test_that("boolean mode returns independent=FALSE for dependent data", { + x = c(1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.) + z = matrix(0, nrow = 12, ncol = 0) + + result = test_bool$run_test(x, y, z) + expect_equal(result$kind, "boolean") + expect_false(result$independent) + }) diff --git a/crates/cir/tests/testthat/test-modified_likelihood.R b/crates/cir/tests/testthat/test-modified_likelihood.R index e69de29..a89cf9a 100644 --- a/crates/cir/tests/testthat/test-modified_likelihood.R +++ b/crates/cir/tests/testthat/test-modified_likelihood.R @@ -0,0 +1,38 @@ +library(cir) +test = RModifiedLikelihood$new(FALSE, 0.05) + +test_that("independent data is not rejected", { + x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) + y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) + z = matrix(0, nrow = 8, ncol = 0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "statistic") + expect_true(result$statistic < 1e-9) + expect_true(result$p_value >= 0.99) + expect_equal(result$df, 1) + }) + +test_that("dependent data is rejected", { + x = c(1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.) + z = matrix(0, nrow = 12, ncol = 0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "statistic") + expect_true(abs(result$statistic - 7.053439978825427) < 1e-9) + expect_true(abs(result$p_value - 0.007911317670556329) < 1e-12) + expect_equal(result$df, 1) + }) + +test_bool = RModifiedLikelihood$new(TRUE, 0.05) + +test_that("boolean mode returns independent=FALSE for dependent data", { + x = c(1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2.) + y = c(1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.) + z = matrix(0, nrow = 12, ncol = 0) + + result = test_bool$run_test(x, y, z) + expect_equal(result$kind, "boolean") + expect_false(result$independent) + }) diff --git a/crates/cir/tests/testthat/test-pearson_correlation.R b/crates/cir/tests/testthat/test-pearson_correlation.R index e69de29..a22becb 100644 --- a/crates/cir/tests/testthat/test-pearson_correlation.R +++ b/crates/cir/tests/testthat/test-pearson_correlation.R @@ -0,0 +1,136 @@ +library(cir) +N = 1000 +test = RPearsonCorrelation$new(FALSE, 0.05) +test_bool = RPearsonCorrelation$new(TRUE, 0.05) + +test_that("unconditional independent data is not rejected", { + set.seed(42) + x = rnorm(N) + y = rnorm(N) + z = matrix(0, nrow = N, ncol = 0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "pvalue") + expect_true(result$p_value > 0.05) + expect_true(abs(result$coefficient) < 0.1) + }) + +test_that("unconditional boolean accepts independent data", { + set.seed(42) + x = rnorm(N) + y = rnorm(N) + z = matrix(0, nrow = N, ncol = 0) + + result = test_bool$run_test(x, y, z) + expect_equal(result$kind, "boolean") + expect_true(result$independent) + }) + +test_that("unconditional dependent data is rejected", { + set.seed(42) + x = rnorm(N) + noise = rnorm(N, sd = 0.1) + y = 3 * x + noise + z = matrix(0, nrow = N, ncol = 0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "pvalue") + expect_true(result$p_value < 0.05) + expect_true(abs(result$coefficient) > 0.9) + }) + +test_that("unconditional boolean rejects dependent data", { + set.seed(42) + x = rnorm(N) + noise = rnorm(N, sd = 0.1) + y = 3 * x + noise + z = matrix(0, nrow = N, ncol = 0) + + result = test_bool$run_test(x, y, z) + expect_equal(result$kind, "boolean") + expect_false(result$independent) + }) + +test_that("conditional independent data is not rejected", { + set.seed(42) + z_col = rnorm(N) + noise_x = rnorm(N, sd = 0.1) + noise_y = rnorm(N, sd = 0.1) + x = 3 * z_col + noise_x + y = 2 * z_col + noise_y + z = matrix(z_col, nrow = N, ncol = 1) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "pvalue") + expect_true(result$p_value > 0.05) + expect_true(abs(result$coefficient) < 0.1) + }) + +test_that("conditional boolean accepts conditionally independent data", { + set.seed(42) + z_col = rnorm(N) + noise_x = rnorm(N, sd = 0.1) + noise_y = rnorm(N, sd = 0.1) + x = 3 * z_col + noise_x + y = 2 * z_col + noise_y + z = matrix(z_col, nrow = N, ncol = 1) + + result = test_bool$run_test(x, y, z) + expect_equal(result$kind, "boolean") + expect_true(result$independent) + }) + +test_that("conditional dependent data (v-structure collider) is rejected", { + set.seed(42) + x = rnorm(N) + y = rnorm(N) + noise = rnorm(N, sd = 0.1) + z_col = 2 * x + 2 * y + noise + z = matrix(z_col, nrow = N, ncol = 1) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "pvalue") + expect_true(result$p_value < 0.05) + expect_true(abs(result$coefficient) > 0.9) + }) + +test_that("conditional boolean rejects dependent data (v-structure collider)", { + set.seed(42) + x = rnorm(N) + y = rnorm(N) + noise = rnorm(N, sd = 0.1) + z_col = 2 * x + 2 * y + noise + z = matrix(z_col, nrow = N, ncol = 1) + + result = test_bool$run_test(x, y, z) + expect_equal(result$kind, "boolean") + expect_false(result$independent) + }) + +test_that("conditional independent data with multiple conditioning variables is not rejected", { + set.seed(42) + z1 = rnorm(N) + z2 = rnorm(N) + z3 = rnorm(N) + noise_x = rnorm(N, sd = 0.1) + noise_y = rnorm(N, sd = 0.1) + x = 0.5 * z1 + 0.5 * z2 + 0.5 * z3 + noise_x + y = 0.5 * z1 + 0.5 * z2 + 0.5 * z3 + noise_y + z = cbind(z1, z2, z3) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "pvalue") + expect_true(result$p_value >= 0.05) + expect_true(abs(result$coefficient) <= 0.1) + }) + +test_that("minimum input (n=3) with perfect correlation returns coefficient near 1", { + x = c(1.0, 2.0, 3.0) + y = c(1.0, 2.0, 3.0) + z = matrix(0, nrow = 3, ncol = 0) + + result = test$run_test(x, y, z) + expect_equal(result$kind, "pvalue") + expect_true(abs(result$coefficient - 1.0) < 1e-10) + expect_true(result$p_value < 0.05) + }) diff --git a/todo.txt b/todo.txt deleted file mode 100644 index e69de29..0000000 From f6c1984eb71026e9d09c88217299baf46198d620 Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Wed, 6 May 2026 11:29:13 +0200 Subject: [PATCH 10/20] feat: ability to list all ci_tests in R --- crates/cir/R/extendr-wrappers.R | 4 ++++ crates/cir/src/rust/src/lib.rs | 27 ++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/cir/R/extendr-wrappers.R b/crates/cir/R/extendr-wrappers.R index 51b726c..e90e395 100644 --- a/crates/cir/R/extendr-wrappers.R +++ b/crates/cir/R/extendr-wrappers.R @@ -5,6 +5,10 @@ #' @useDynLib cir, .registration = TRUE NULL +list_ci_tests <- function() .Call(wrap__list_ci_tests) + +list_ci_tests_for <- function(data_type) .Call(wrap__list_ci_tests_for, data_type) + RChiSquared <- new.env(parent = emptyenv()) RChiSquared$new <- function(boolean, significance_level) .Call(wrap__RChiSquared__new, boolean, significance_level) diff --git a/crates/cir/src/rust/src/lib.rs b/crates/cir/src/rust/src/lib.rs index 6da4982..74797f4 100644 --- a/crates/cir/src/rust/src/lib.rs +++ b/crates/cir/src/rust/src/lib.rs @@ -1,5 +1,6 @@ use extendr_api::prelude::*; -use ci_core::strategy::CITest; +use ci_core::registry::Registry; +use ci_core::strategy::{CITest, CITestDataType}; use ci_core::ci_tests::{ chi_squared::ChiSquared, cressie_read::CressieRead, @@ -46,6 +47,28 @@ macro_rules! r_ci_test { }; } +#[extendr] +fn list_ci_tests() -> anyhow::Result> { + let registry = Registry::new(); + let mut tests: Vec = registry.all_tests()?.map(String::from).collect(); + tests.sort(); + Ok(tests) +} + +#[extendr] +fn list_ci_tests_for(data_type: &str) -> anyhow::Result> { + let dt = match data_type.to_lowercase().as_str() { + "discrete" => CITestDataType::Discrete, + "continuous" => CITestDataType::Continuous, + "mixed" => CITestDataType::Mixed, + _ => anyhow::bail!("Unknown data type: '{data_type}'. Use 'discrete', 'continuous', or 'mixed'."), + }; + let registry = Registry::new(); + let mut tests: Vec = registry.tests_with_data_type(&dt)?.map(String::from).collect(); + tests.sort(); + Ok(tests) +} + r_ci_test!(RChiSquared, ChiSquared); r_ci_test!(RLogLikelihood, LogLikelihood); r_ci_test!(RCressieRead, CressieRead); @@ -57,6 +80,8 @@ r_ci_test!(RPearsonEquivalence, PearsonEquivalence); extendr_module! { mod cir; + fn list_ci_tests; + fn list_ci_tests_for; impl RChiSquared; impl RLogLikelihood; impl RCressieRead; From 4ea502b4f30a85b9928a585ee6c17e02e5d73edf Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Fri, 8 May 2026 10:10:10 +0200 Subject: [PATCH 11/20] feat: Simplified macro logic to be more R idiomatic --- crates/cir/NAMESPACE | 14 ---- crates/cir/R/extendr-wrappers.R | 84 ++----------------- crates/cir/src/rust/src/lib.rs | 69 +++++++-------- crates/cir/tests/testthat/test-chi_squared.R | 15 ++-- crates/cir/tests/testthat/test-cressie_read.R | 16 ++-- .../cir/tests/testthat/test-freeman_tukey.R | 9 +- .../cir/tests/testthat/test-log_likelihood.R | 9 +- .../tests/testthat/test-modified_likelihood.R | 9 +- .../tests/testthat/test-pearson_correlation.R | 22 +++-- 9 files changed, 67 insertions(+), 180 deletions(-) diff --git a/crates/cir/NAMESPACE b/crates/cir/NAMESPACE index 3d4893f..5d83515 100644 --- a/crates/cir/NAMESPACE +++ b/crates/cir/NAMESPACE @@ -1,17 +1,3 @@ # Generated by roxygen2: do not edit by hand -S3method("$",RChiSquared) -S3method("$",RCressieRead) -S3method("$",RFreemanTukey) -S3method("$",RLogLikelihood) -S3method("$",RModifiedLikelihood) -S3method("$",RPearsonCorrelation) -S3method("$",RPearsonEquivalence) -S3method("[[",RChiSquared) -S3method("[[",RCressieRead) -S3method("[[",RFreemanTukey) -S3method("[[",RLogLikelihood) -S3method("[[",RModifiedLikelihood) -S3method("[[",RPearsonCorrelation) -S3method("[[",RPearsonEquivalence) useDynLib(cir, .registration = TRUE) diff --git a/crates/cir/R/extendr-wrappers.R b/crates/cir/R/extendr-wrappers.R index e90e395..d7a3bef 100644 --- a/crates/cir/R/extendr-wrappers.R +++ b/crates/cir/R/extendr-wrappers.R @@ -9,88 +9,18 @@ list_ci_tests <- function() .Call(wrap__list_ci_tests) list_ci_tests_for <- function(data_type) .Call(wrap__list_ci_tests_for, data_type) -RChiSquared <- new.env(parent = emptyenv()) +chi_squared_test <- function(x_values, y_values, z, boolean, significance_level) .Call(wrap__chi_squared_test, x_values, y_values, z, boolean, significance_level) -RChiSquared$new <- function(boolean, significance_level) .Call(wrap__RChiSquared__new, boolean, significance_level) +log_likelihood_test <- function(x_values, y_values, z, boolean, significance_level) .Call(wrap__log_likelihood_test, x_values, y_values, z, boolean, significance_level) -RChiSquared$run_test <- function(x_values, y_values, z) .Call(wrap__RChiSquared__run_test, self, x_values, y_values, z) +cressie_read_test <- function(x_values, y_values, z, boolean, significance_level) .Call(wrap__cressie_read_test, x_values, y_values, z, boolean, significance_level) -#' @export -`$.RChiSquared` <- function (self, name) { func <- RChiSquared[[name]]; environment(func) <- environment(); func } +pearson_correlation_test <- function(x_values, y_values, z, boolean, significance_level) .Call(wrap__pearson_correlation_test, x_values, y_values, z, boolean, significance_level) -#' @export -`[[.RChiSquared` <- `$.RChiSquared` +freeman_tukey_test <- function(x_values, y_values, z, boolean, significance_level) .Call(wrap__freeman_tukey_test, x_values, y_values, z, boolean, significance_level) -RLogLikelihood <- new.env(parent = emptyenv()) +modified_likelihood_test <- function(x_values, y_values, z, boolean, significance_level) .Call(wrap__modified_likelihood_test, x_values, y_values, z, boolean, significance_level) -RLogLikelihood$new <- function(boolean, significance_level) .Call(wrap__RLogLikelihood__new, boolean, significance_level) - -RLogLikelihood$run_test <- function(x_values, y_values, z) .Call(wrap__RLogLikelihood__run_test, self, x_values, y_values, z) - -#' @export -`$.RLogLikelihood` <- function (self, name) { func <- RLogLikelihood[[name]]; environment(func) <- environment(); func } - -#' @export -`[[.RLogLikelihood` <- `$.RLogLikelihood` - -RCressieRead <- new.env(parent = emptyenv()) - -RCressieRead$new <- function(boolean, significance_level) .Call(wrap__RCressieRead__new, boolean, significance_level) - -RCressieRead$run_test <- function(x_values, y_values, z) .Call(wrap__RCressieRead__run_test, self, x_values, y_values, z) - -#' @export -`$.RCressieRead` <- function (self, name) { func <- RCressieRead[[name]]; environment(func) <- environment(); func } - -#' @export -`[[.RCressieRead` <- `$.RCressieRead` - -RPearsonCorrelation <- new.env(parent = emptyenv()) - -RPearsonCorrelation$new <- function(boolean, significance_level) .Call(wrap__RPearsonCorrelation__new, boolean, significance_level) - -RPearsonCorrelation$run_test <- function(x_values, y_values, z) .Call(wrap__RPearsonCorrelation__run_test, self, x_values, y_values, z) - -#' @export -`$.RPearsonCorrelation` <- function (self, name) { func <- RPearsonCorrelation[[name]]; environment(func) <- environment(); func } - -#' @export -`[[.RPearsonCorrelation` <- `$.RPearsonCorrelation` - -RFreemanTukey <- new.env(parent = emptyenv()) - -RFreemanTukey$new <- function(boolean, significance_level) .Call(wrap__RFreemanTukey__new, boolean, significance_level) - -RFreemanTukey$run_test <- function(x_values, y_values, z) .Call(wrap__RFreemanTukey__run_test, self, x_values, y_values, z) - -#' @export -`$.RFreemanTukey` <- function (self, name) { func <- RFreemanTukey[[name]]; environment(func) <- environment(); func } - -#' @export -`[[.RFreemanTukey` <- `$.RFreemanTukey` - -RModifiedLikelihood <- new.env(parent = emptyenv()) - -RModifiedLikelihood$new <- function(boolean, significance_level) .Call(wrap__RModifiedLikelihood__new, boolean, significance_level) - -RModifiedLikelihood$run_test <- function(x_values, y_values, z) .Call(wrap__RModifiedLikelihood__run_test, self, x_values, y_values, z) - -#' @export -`$.RModifiedLikelihood` <- function (self, name) { func <- RModifiedLikelihood[[name]]; environment(func) <- environment(); func } - -#' @export -`[[.RModifiedLikelihood` <- `$.RModifiedLikelihood` - -RPearsonEquivalence <- new.env(parent = emptyenv()) - -RPearsonEquivalence$new <- function(boolean, significance_level) .Call(wrap__RPearsonEquivalence__new, boolean, significance_level) - -RPearsonEquivalence$run_test <- function(x_values, y_values, z) .Call(wrap__RPearsonEquivalence__run_test, self, x_values, y_values, z) - -#' @export -`$.RPearsonEquivalence` <- function (self, name) { func <- RPearsonEquivalence[[name]]; environment(func) <- environment(); func } - -#' @export -`[[.RPearsonEquivalence` <- `$.RPearsonEquivalence` +pearson_equivalence_test <- function(x_values, y_values, z, boolean, significance_level) .Call(wrap__pearson_equivalence_test, x_values, y_values, z, boolean, significance_level) # nolint end diff --git a/crates/cir/src/rust/src/lib.rs b/crates/cir/src/rust/src/lib.rs index 74797f4..71b982f 100644 --- a/crates/cir/src/rust/src/lib.rs +++ b/crates/cir/src/rust/src/lib.rs @@ -14,35 +14,22 @@ use ndarray::{ArrayView1, ArrayView2}; mod util; macro_rules! r_ci_test { - ($r_name:ident, $inner:ty) => { + ($fn_name:ident, $inner:ty) => { #[extendr] - pub struct $r_name { - citest: $inner - } - - #[extendr] - impl $r_name { - fn new( - boolean: bool, - significance_level: f64 - ) -> Self { + fn $fn_name( + x_values: ArrayView1, + y_values: ArrayView1, + z: ArrayView2, + boolean: bool, + significance_level: f64, + ) -> anyhow::Result { let citest = <$inner>::new(boolean, significance_level); - $r_name{citest: citest} - } - - fn run_test( - &self, - x_values: ArrayView1, - y_values: ArrayView1, - z: ArrayView2, - ) -> anyhow::Result { - - let result = self.citest.run_test( - x_values.to_owned(), - y_values.to_owned(), - z.to_owned())?; - Ok(util::test_result_to_robj(result)) - } + let result = citest.run_test( + x_values.to_owned(), + y_values.to_owned(), + z.to_owned(), + )?; + Ok(util::test_result_to_robj(result)) } }; } @@ -69,24 +56,24 @@ fn list_ci_tests_for(data_type: &str) -> anyhow::Result> { Ok(tests) } -r_ci_test!(RChiSquared, ChiSquared); -r_ci_test!(RLogLikelihood, LogLikelihood); -r_ci_test!(RCressieRead, CressieRead); -r_ci_test!(RPearsonCorrelation, PearsonCorrelation); -r_ci_test!(RFreemanTukey, FreemanTukey); -r_ci_test!(RModifiedLikelihood, ModifiedLikelihood); -r_ci_test!(RPearsonEquivalence, PearsonEquivalence); +r_ci_test!(chi_squared_test, ChiSquared); +r_ci_test!(log_likelihood_test, LogLikelihood); +r_ci_test!(cressie_read_test, CressieRead); +r_ci_test!(pearson_correlation_test, PearsonCorrelation); +r_ci_test!(freeman_tukey_test, FreemanTukey); +r_ci_test!(modified_likelihood_test, ModifiedLikelihood); +r_ci_test!(pearson_equivalence_test, PearsonEquivalence); extendr_module! { mod cir; fn list_ci_tests; fn list_ci_tests_for; - impl RChiSquared; - impl RLogLikelihood; - impl RCressieRead; - impl RPearsonCorrelation; - impl RFreemanTukey; - impl RModifiedLikelihood; - impl RPearsonEquivalence; + fn chi_squared_test; + fn log_likelihood_test; + fn cressie_read_test; + fn pearson_correlation_test; + fn freeman_tukey_test; + fn modified_likelihood_test; + fn pearson_equivalence_test; } \ No newline at end of file diff --git a/crates/cir/tests/testthat/test-chi_squared.R b/crates/cir/tests/testthat/test-chi_squared.R index b2e3b67..3672e30 100644 --- a/crates/cir/tests/testthat/test-chi_squared.R +++ b/crates/cir/tests/testthat/test-chi_squared.R @@ -1,38 +1,35 @@ library(cir) -test = RChiSquared$new(FALSE, 0.05) test_that("independent data is not rejected", { x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) z = matrix(0, nrow = 8, ncol = 0) - result = test$run_test(x, y, z) + result = chi_squared_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "statistic") expect_true(result$statistic < 1e-9) expect_true(result$p_value >= 0.99) expect_equal(result$df, 1) }) - test_that("dependent data is rejected", { +test_that("dependent data is rejected", { x = c(1., 1., 1., 1., 2., 2., 2., 2.) y = c(1., 1., 1., 1., 2., 2., 2., 2.) z = matrix(0, nrow = 8, ncol = 0) - result = test$run_test(x, y, z) + result = chi_squared_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "statistic") expect_true(abs(result$statistic - 8.0) < 1e-9) expect_true(abs(result$p_value - 0.004677734981047276) < 1e-12) expect_equal(result$df, 1) }) -test_bool = RChiSquared$new(TRUE, 0.05) - test_that("boolean mode returns independent=TRUE for independent data", { x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) z = matrix(0, nrow = 8, ncol = 0) - result = test_bool$run_test(x, y, z) + result = chi_squared_test(x, y, z, TRUE, 0.05) expect_equal(result$kind, "boolean") expect_true(result$independent) }) @@ -42,7 +39,7 @@ test_that("boolean mode returns independent=FALSE for dependent data", { y = c(1., 1., 1., 1., 2., 2., 2., 2.) z = matrix(0, nrow = 8, ncol = 0) - result = test_bool$run_test(x, y, z) + result = chi_squared_test(x, y, z, TRUE, 0.05) expect_equal(result$kind, "boolean") expect_false(result$independent) - }) \ No newline at end of file + }) diff --git a/crates/cir/tests/testthat/test-cressie_read.R b/crates/cir/tests/testthat/test-cressie_read.R index 2510ae3..ad8d20c 100644 --- a/crates/cir/tests/testthat/test-cressie_read.R +++ b/crates/cir/tests/testthat/test-cressie_read.R @@ -1,13 +1,11 @@ library(cir) -test = RCressieRead$new(FALSE, 0.05) -test_bool = RCressieRead$new(TRUE, 0.05) test_that("unconditional independent data is not rejected", { x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) z = matrix(0, nrow = 8, ncol = 0) - result = test$run_test(x, y, z) + result = cressie_read_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "statistic") expect_true(result$statistic < 1e-9) expect_true(result$p_value > 0.99) @@ -19,7 +17,7 @@ test_that("unconditional boolean accepts independent data", { y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) z = matrix(0, nrow = 8, ncol = 0) - result = test_bool$run_test(x, y, z) + result = cressie_read_test(x, y, z, TRUE, 0.05) expect_equal(result$kind, "boolean") expect_true(result$independent) }) @@ -29,7 +27,7 @@ test_that("unconditional dependent data is rejected", { y = c(1., 1., 1., 1., 2., 2., 2., 2.) z = matrix(0, nrow = 8, ncol = 0) - result = test$run_test(x, y, z) + result = cressie_read_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "statistic") expect_true(result$statistic > 5.0) expect_true(result$p_value < 0.05) @@ -41,7 +39,7 @@ test_that("unconditional boolean rejects dependent data", { y = c(1., 1., 1., 1., 2., 2., 2., 2.) z = matrix(0, nrow = 8, ncol = 0) - result = test_bool$run_test(x, y, z) + result = cressie_read_test(x, y, z, TRUE, 0.05) expect_equal(result$kind, "boolean") expect_false(result$independent) }) @@ -51,7 +49,7 @@ test_that("conditional independent data is not rejected", { y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) z = matrix(c(0., 0., 0., 0., 1., 1., 1., 1.), nrow = 8, ncol = 1) - result = test$run_test(x, y, z) + result = cressie_read_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "statistic") expect_true(result$statistic < 1e-9) expect_true(result$p_value > 0.99) @@ -63,7 +61,7 @@ test_that("conditional boolean accepts conditionally independent data", { y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) z = matrix(c(0., 0., 0., 0., 1., 1., 1., 1.), nrow = 8, ncol = 1) - result = test_bool$run_test(x, y, z) + result = cressie_read_test(x, y, z, TRUE, 0.05) expect_equal(result$kind, "boolean") expect_true(result$independent) }) @@ -73,7 +71,7 @@ test_that("conditional dependent data is rejected", { y = c(1., 1., 2., 2., 1., 1., 2., 2.) z = matrix(c(0., 0., 0., 0., 1., 1., 1., 1.), nrow = 8, ncol = 1) - result = test$run_test(x, y, z) + result = cressie_read_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "statistic") expect_true(result$statistic > 5.0) expect_true(result$p_value < 0.05) diff --git a/crates/cir/tests/testthat/test-freeman_tukey.R b/crates/cir/tests/testthat/test-freeman_tukey.R index 19629c8..b02b2d8 100644 --- a/crates/cir/tests/testthat/test-freeman_tukey.R +++ b/crates/cir/tests/testthat/test-freeman_tukey.R @@ -1,12 +1,11 @@ library(cir) -test = RFreemanTukey$new(FALSE, 0.05) test_that("independent data is not rejected", { x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) z = matrix(0, nrow = 8, ncol = 0) - result = test$run_test(x, y, z) + result = freeman_tukey_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "statistic") expect_true(result$statistic < 1e-9) expect_true(result$p_value >= 0.99) @@ -18,21 +17,19 @@ test_that("dependent data is rejected", { y = c(1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.) z = matrix(0, nrow = 12, ncol = 0) - result = test$run_test(x, y, z) + result = freeman_tukey_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "statistic") expect_true(abs(result$statistic - 6.319453539579289) < 1e-9) expect_true(abs(result$p_value - 0.011942042564347121) < 1e-12) expect_equal(result$df, 1) }) -test_bool = RFreemanTukey$new(TRUE, 0.05) - test_that("boolean mode returns independent=TRUE for independent data", { x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) z = matrix(0, nrow = 8, ncol = 0) - result = test_bool$run_test(x, y, z) + result = freeman_tukey_test(x, y, z, TRUE, 0.05) expect_equal(result$kind, "boolean") expect_true(result$independent) }) diff --git a/crates/cir/tests/testthat/test-log_likelihood.R b/crates/cir/tests/testthat/test-log_likelihood.R index 53dee87..58cad3d 100644 --- a/crates/cir/tests/testthat/test-log_likelihood.R +++ b/crates/cir/tests/testthat/test-log_likelihood.R @@ -1,12 +1,11 @@ library(cir) -test = RLogLikelihood$new(FALSE, 0.05) test_that("independent data is not rejected", { x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) z = matrix(0, nrow = 8, ncol = 0) - result = test$run_test(x, y, z) + result = log_likelihood_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "statistic") expect_true(result$statistic < 1e-9) expect_true(result$p_value >= 0.99) @@ -18,21 +17,19 @@ test_that("dependent data is rejected", { y = c(1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.) z = matrix(0, nrow = 12, ncol = 0) - result = test$run_test(x, y, z) + result = log_likelihood_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "statistic") expect_true(abs(result$statistic - 5.822063320647374) < 1e-9) expect_true(abs(result$p_value - 0.015826368796540195) < 1e-12) expect_equal(result$df, 1) }) -test_bool = RLogLikelihood$new(TRUE, 0.05) - test_that("boolean mode returns independent=FALSE for dependent data", { x = c(1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2.) y = c(1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.) z = matrix(0, nrow = 12, ncol = 0) - result = test_bool$run_test(x, y, z) + result = log_likelihood_test(x, y, z, TRUE, 0.05) expect_equal(result$kind, "boolean") expect_false(result$independent) }) diff --git a/crates/cir/tests/testthat/test-modified_likelihood.R b/crates/cir/tests/testthat/test-modified_likelihood.R index a89cf9a..9204669 100644 --- a/crates/cir/tests/testthat/test-modified_likelihood.R +++ b/crates/cir/tests/testthat/test-modified_likelihood.R @@ -1,12 +1,11 @@ library(cir) -test = RModifiedLikelihood$new(FALSE, 0.05) test_that("independent data is not rejected", { x = c(1.0, 1.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0) y = c(1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0) z = matrix(0, nrow = 8, ncol = 0) - result = test$run_test(x, y, z) + result = modified_likelihood_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "statistic") expect_true(result$statistic < 1e-9) expect_true(result$p_value >= 0.99) @@ -18,21 +17,19 @@ test_that("dependent data is rejected", { y = c(1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.) z = matrix(0, nrow = 12, ncol = 0) - result = test$run_test(x, y, z) + result = modified_likelihood_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "statistic") expect_true(abs(result$statistic - 7.053439978825427) < 1e-9) expect_true(abs(result$p_value - 0.007911317670556329) < 1e-12) expect_equal(result$df, 1) }) -test_bool = RModifiedLikelihood$new(TRUE, 0.05) - test_that("boolean mode returns independent=FALSE for dependent data", { x = c(1., 1., 1., 1., 1., 1., 2., 2., 2., 2., 2., 2.) y = c(1., 1., 1., 1., 1., 2., 1., 2., 2., 2., 2., 2.) z = matrix(0, nrow = 12, ncol = 0) - result = test_bool$run_test(x, y, z) + result = modified_likelihood_test(x, y, z, TRUE, 0.05) expect_equal(result$kind, "boolean") expect_false(result$independent) }) diff --git a/crates/cir/tests/testthat/test-pearson_correlation.R b/crates/cir/tests/testthat/test-pearson_correlation.R index a22becb..707f160 100644 --- a/crates/cir/tests/testthat/test-pearson_correlation.R +++ b/crates/cir/tests/testthat/test-pearson_correlation.R @@ -1,7 +1,5 @@ library(cir) N = 1000 -test = RPearsonCorrelation$new(FALSE, 0.05) -test_bool = RPearsonCorrelation$new(TRUE, 0.05) test_that("unconditional independent data is not rejected", { set.seed(42) @@ -9,7 +7,7 @@ test_that("unconditional independent data is not rejected", { y = rnorm(N) z = matrix(0, nrow = N, ncol = 0) - result = test$run_test(x, y, z) + result = pearson_correlation_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "pvalue") expect_true(result$p_value > 0.05) expect_true(abs(result$coefficient) < 0.1) @@ -21,7 +19,7 @@ test_that("unconditional boolean accepts independent data", { y = rnorm(N) z = matrix(0, nrow = N, ncol = 0) - result = test_bool$run_test(x, y, z) + result = pearson_correlation_test(x, y, z, TRUE, 0.05) expect_equal(result$kind, "boolean") expect_true(result$independent) }) @@ -33,7 +31,7 @@ test_that("unconditional dependent data is rejected", { y = 3 * x + noise z = matrix(0, nrow = N, ncol = 0) - result = test$run_test(x, y, z) + result = pearson_correlation_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "pvalue") expect_true(result$p_value < 0.05) expect_true(abs(result$coefficient) > 0.9) @@ -46,7 +44,7 @@ test_that("unconditional boolean rejects dependent data", { y = 3 * x + noise z = matrix(0, nrow = N, ncol = 0) - result = test_bool$run_test(x, y, z) + result = pearson_correlation_test(x, y, z, TRUE, 0.05) expect_equal(result$kind, "boolean") expect_false(result$independent) }) @@ -60,7 +58,7 @@ test_that("conditional independent data is not rejected", { y = 2 * z_col + noise_y z = matrix(z_col, nrow = N, ncol = 1) - result = test$run_test(x, y, z) + result = pearson_correlation_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "pvalue") expect_true(result$p_value > 0.05) expect_true(abs(result$coefficient) < 0.1) @@ -75,7 +73,7 @@ test_that("conditional boolean accepts conditionally independent data", { y = 2 * z_col + noise_y z = matrix(z_col, nrow = N, ncol = 1) - result = test_bool$run_test(x, y, z) + result = pearson_correlation_test(x, y, z, TRUE, 0.05) expect_equal(result$kind, "boolean") expect_true(result$independent) }) @@ -88,7 +86,7 @@ test_that("conditional dependent data (v-structure collider) is rejected", { z_col = 2 * x + 2 * y + noise z = matrix(z_col, nrow = N, ncol = 1) - result = test$run_test(x, y, z) + result = pearson_correlation_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "pvalue") expect_true(result$p_value < 0.05) expect_true(abs(result$coefficient) > 0.9) @@ -102,7 +100,7 @@ test_that("conditional boolean rejects dependent data (v-structure collider)", { z_col = 2 * x + 2 * y + noise z = matrix(z_col, nrow = N, ncol = 1) - result = test_bool$run_test(x, y, z) + result = pearson_correlation_test(x, y, z, TRUE, 0.05) expect_equal(result$kind, "boolean") expect_false(result$independent) }) @@ -118,7 +116,7 @@ test_that("conditional independent data with multiple conditioning variables is y = 0.5 * z1 + 0.5 * z2 + 0.5 * z3 + noise_y z = cbind(z1, z2, z3) - result = test$run_test(x, y, z) + result = pearson_correlation_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "pvalue") expect_true(result$p_value >= 0.05) expect_true(abs(result$coefficient) <= 0.1) @@ -129,7 +127,7 @@ test_that("minimum input (n=3) with perfect correlation returns coefficient near y = c(1.0, 2.0, 3.0) z = matrix(0, nrow = 3, ncol = 0) - result = test$run_test(x, y, z) + result = pearson_correlation_test(x, y, z, FALSE, 0.05) expect_equal(result$kind, "pvalue") expect_true(abs(result$coefficient - 1.0) < 1e-10) expect_true(result$p_value < 0.05) From db001b4b0dcd7f6450bcbdb008c99f2784751cb8 Mon Sep 17 00:00:00 2001 From: Johan Voskamp <55276630+Hiddentale@users.noreply.github.com> Date: Fri, 8 May 2026 10:19:56 +0200 Subject: [PATCH 12/20] docs: added documentation --- crates/cir/src/rust/src/lib.rs | 26 ++++++++++++++++++++++++-- crates/cir/src/rust/src/util.rs | 8 ++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/crates/cir/src/rust/src/lib.rs b/crates/cir/src/rust/src/lib.rs index 71b982f..bea91c4 100644 --- a/crates/cir/src/rust/src/lib.rs +++ b/crates/cir/src/rust/src/lib.rs @@ -1,9 +1,15 @@ +//! R bindings for the conditional independence testing library. +//! +//! Exposes CI test functions and registry queries to R via the `extendr` framework. +//! Each CI test accepts paired observation vectors and a conditioning matrix, returning +//! a named R list whose shape depends on whether the test runs in boolean or numeric mode. + use extendr_api::prelude::*; use ci_core::registry::Registry; use ci_core::strategy::{CITest, CITestDataType}; use ci_core::ci_tests::{ - chi_squared::ChiSquared, - cressie_read::CressieRead, + chi_squared::ChiSquared, + cressie_read::CressieRead, freeman_tukey::FreemanTukey, log_likelihood::LogLikelihood, modified_likelihood::ModifiedLikelihood, @@ -13,6 +19,17 @@ use ci_core::ci_tests::{ use ndarray::{ArrayView1, ArrayView2}; mod util; +/// Generates an R-callable wrapper for a [`CITest`] implementation. +/// +/// The generated function signature is: +/// ```text +/// fn $fn_name(x_values, y_values, z, boolean, significance_level) -> Robj +/// ``` +/// - `x_values` / `y_values`: paired observation vectors. +/// - `z`: conditioning matrix; pass a 0-column matrix for unconditional tests. +/// - `boolean`: when `true`, returns only an independence verdict at `significance_level` +/// instead of the raw test statistic and p-value. +/// - `significance_level`: threshold used only when `boolean` is `true`. macro_rules! r_ci_test { ($fn_name:ident, $inner:ty) => { #[extendr] @@ -34,6 +51,7 @@ macro_rules! r_ci_test { }; } +/// Returns a sorted vector of all registered CI test names. #[extendr] fn list_ci_tests() -> anyhow::Result> { let registry = Registry::new(); @@ -42,6 +60,10 @@ fn list_ci_tests() -> anyhow::Result> { Ok(tests) } +/// Returns a sorted vector of CI test names compatible with the given data type. +/// +/// `data_type` must be one of `"discrete"`, `"continuous"`, or `"mixed"` (case-insensitive). +/// Returns an error for any other value. #[extendr] fn list_ci_tests_for(data_type: &str) -> anyhow::Result> { let dt = match data_type.to_lowercase().as_str() { diff --git a/crates/cir/src/rust/src/util.rs b/crates/cir/src/rust/src/util.rs index 9fe6c6f..c322ec6 100644 --- a/crates/cir/src/rust/src/util.rs +++ b/crates/cir/src/rust/src/util.rs @@ -1,6 +1,14 @@ +//! Conversion utilities from core result types to R objects. + use ci_core::strategy::{TestResult}; use extendr_api::prelude::*; +/// Converts a [`TestResult`] into a named R list. +/// +/// The returned list always contains a `kind` field identifying the variant: +/// - `"pvalue"` — also contains `p_value` and `coefficient`. +/// - `"statistic"` — also contains `statistic`, `p_value`, and `df` (degrees of freedom). +/// - `"boolean"` — also contains `independent` (logical). pub fn test_result_to_robj(r: TestResult) -> Robj { match r { TestResult::PValue(p, coef) => list!( From 1100df8da1cdee14e816cc769eb79cb068668aff Mon Sep 17 00:00:00 2001 From: Johan Voskamp <55276630+Hiddentale@users.noreply.github.com> Date: Fri, 8 May 2026 10:35:04 +0200 Subject: [PATCH 13/20] refactor: clippy and fmt --- crates/ci-core/src/ci_tests/chi_squared.rs | 1 + crates/ci-core/src/ci_tests/cressie_read.rs | 1 + crates/ci-core/src/ci_tests/freeman_tukey.rs | 1 + crates/ci-core/src/ci_tests/log_likelihood.rs | 1 + crates/ci-core/src/ci_tests/mod.rs | 3 ++ .../src/ci_tests/modified_likelihood.rs | 1 + .../src/ci_tests/pearson_correlation.rs | 1 + .../src/ci_tests/pearson_equivalence.rs | 1 + crates/cir/DESCRIPTION | 16 +++++---- crates/cir/src/rust/document.rs | 2 +- crates/cir/src/rust/src/lib.rs | 34 ++++++++----------- crates/cir/src/rust/src/util.rs | 20 +++++------ 12 files changed, 43 insertions(+), 39 deletions(-) diff --git a/crates/ci-core/src/ci_tests/chi_squared.rs b/crates/ci-core/src/ci_tests/chi_squared.rs index d32ec3c..9106d7d 100644 --- a/crates/ci-core/src/ci_tests/chi_squared.rs +++ b/crates/ci-core/src/ci_tests/chi_squared.rs @@ -10,6 +10,7 @@ pub struct ChiSquared { } impl ChiSquared { + #[must_use] pub fn new(boolean: bool, significance_level: f64) -> Self { Self { boolean, diff --git a/crates/ci-core/src/ci_tests/cressie_read.rs b/crates/ci-core/src/ci_tests/cressie_read.rs index 9ce61f8..47ffb16 100644 --- a/crates/ci-core/src/ci_tests/cressie_read.rs +++ b/crates/ci-core/src/ci_tests/cressie_read.rs @@ -10,6 +10,7 @@ pub struct CressieRead { } impl CressieRead { + #[must_use] pub fn new(boolean: bool, significance_level: f64) -> Self { Self { boolean, diff --git a/crates/ci-core/src/ci_tests/freeman_tukey.rs b/crates/ci-core/src/ci_tests/freeman_tukey.rs index b4474bc..27c018c 100644 --- a/crates/ci-core/src/ci_tests/freeman_tukey.rs +++ b/crates/ci-core/src/ci_tests/freeman_tukey.rs @@ -10,6 +10,7 @@ pub struct FreemanTukey { } impl FreemanTukey { + #[must_use] pub fn new(boolean: bool, significance_level: f64) -> Self { Self { boolean, diff --git a/crates/ci-core/src/ci_tests/log_likelihood.rs b/crates/ci-core/src/ci_tests/log_likelihood.rs index 0d443b2..296437f 100644 --- a/crates/ci-core/src/ci_tests/log_likelihood.rs +++ b/crates/ci-core/src/ci_tests/log_likelihood.rs @@ -10,6 +10,7 @@ pub struct LogLikelihood { } impl LogLikelihood { + #[must_use] pub fn new(boolean: bool, significance_level: f64) -> Self { Self { boolean, diff --git a/crates/ci-core/src/ci_tests/mod.rs b/crates/ci-core/src/ci_tests/mod.rs index 732cb28..0cea465 100644 --- a/crates/ci-core/src/ci_tests/mod.rs +++ b/crates/ci-core/src/ci_tests/mod.rs @@ -16,6 +16,9 @@ use pearson_equivalence::PearsonEquivalence; use crate::registry::Registry; +/// # Panics +/// +/// Panics if any test name is already registered (indicates a duplicate registration bug). pub fn register_all_tests(registry: &mut Registry) { registry .add_to_registry("chi_square", ChiSquared::new(true, 0.05)) diff --git a/crates/ci-core/src/ci_tests/modified_likelihood.rs b/crates/ci-core/src/ci_tests/modified_likelihood.rs index ed97967..4ca950e 100644 --- a/crates/ci-core/src/ci_tests/modified_likelihood.rs +++ b/crates/ci-core/src/ci_tests/modified_likelihood.rs @@ -10,6 +10,7 @@ pub struct ModifiedLikelihood { } impl ModifiedLikelihood { + #[must_use] pub fn new(boolean: bool, significance_level: f64) -> Self { Self { boolean, diff --git a/crates/ci-core/src/ci_tests/pearson_correlation.rs b/crates/ci-core/src/ci_tests/pearson_correlation.rs index 57b9b4f..fca1126 100644 --- a/crates/ci-core/src/ci_tests/pearson_correlation.rs +++ b/crates/ci-core/src/ci_tests/pearson_correlation.rs @@ -21,6 +21,7 @@ pub struct PearsonCorrelation { } impl PearsonCorrelation { + #[must_use] pub fn new(boolean: bool, significance_level: f64) -> Self { Self { boolean, diff --git a/crates/ci-core/src/ci_tests/pearson_equivalence.rs b/crates/ci-core/src/ci_tests/pearson_equivalence.rs index d7c37d6..bc50c04 100644 --- a/crates/ci-core/src/ci_tests/pearson_equivalence.rs +++ b/crates/ci-core/src/ci_tests/pearson_equivalence.rs @@ -8,6 +8,7 @@ pub struct PearsonEquivalence { } impl PearsonEquivalence { + #[must_use] pub fn new(boolean: bool, significance_level: f64) -> Self { Self { boolean, diff --git a/crates/cir/DESCRIPTION b/crates/cir/DESCRIPTION index bd80d1b..daf6886 100644 --- a/crates/cir/DESCRIPTION +++ b/crates/cir/DESCRIPTION @@ -1,15 +1,17 @@ Package: cir -Title: What the Package Does (One Line, Title Case) +Title: Conditional Independence Testing 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 +Authors@R: + person("GIP", "House", role = c("aut", "cre")) +Description: R bindings for a collection of conditional independence tests + implemented in Rust. Supports discrete, continuous, and mixed data via + chi-squared, log-likelihood, Cressie-Read, Freeman-Tukey, Pearson + correlation, modified log-likelihood, and Pearson equivalence tests. +License: MIT Encoding: UTF-8 Roxygen: list(markdown = TRUE) RoxygenNote: 7.3.3 Config/rextendr/version: 0.5.0 SystemRequirements: Cargo (Rust's package manager), rustc >= 1.65.0, xz -Depends: +Depends: R (>= 4.2) diff --git a/crates/cir/src/rust/document.rs b/crates/cir/src/rust/document.rs index 1dd2ff7..ef5b73f 100644 --- a/crates/cir/src/rust/document.rs +++ b/crates/cir/src/rust/document.rs @@ -17,4 +17,4 @@ fn main() -> Result<(), Box> { std::fs::write(wrapper_path, format!("{header}{wrappers}{footer}")) .map_err(|e| format!("failed to write {wrapper_path}: {e}"))?; Ok(()) -} \ No newline at end of file +} diff --git a/crates/cir/src/rust/src/lib.rs b/crates/cir/src/rust/src/lib.rs index bea91c4..f6ae097 100644 --- a/crates/cir/src/rust/src/lib.rs +++ b/crates/cir/src/rust/src/lib.rs @@ -4,18 +4,14 @@ //! Each CI test accepts paired observation vectors and a conditioning matrix, returning //! a named R list whose shape depends on whether the test runs in boolean or numeric mode. -use extendr_api::prelude::*; -use ci_core::registry::Registry; -use ci_core::strategy::{CITest, CITestDataType}; use ci_core::ci_tests::{ - chi_squared::ChiSquared, - cressie_read::CressieRead, - freeman_tukey::FreemanTukey, - log_likelihood::LogLikelihood, - modified_likelihood::ModifiedLikelihood, - pearson_correlation::PearsonCorrelation, - pearson_equivalence::PearsonEquivalence, + chi_squared::ChiSquared, cressie_read::CressieRead, freeman_tukey::FreemanTukey, + log_likelihood::LogLikelihood, modified_likelihood::ModifiedLikelihood, + pearson_correlation::PearsonCorrelation, pearson_equivalence::PearsonEquivalence, }; +use ci_core::registry::Registry; +use ci_core::strategy::{CITest, CITestDataType}; +use extendr_api::prelude::*; use ndarray::{ArrayView1, ArrayView2}; mod util; @@ -41,11 +37,7 @@ macro_rules! r_ci_test { significance_level: f64, ) -> anyhow::Result { let citest = <$inner>::new(boolean, significance_level); - let result = citest.run_test( - x_values.to_owned(), - y_values.to_owned(), - z.to_owned(), - )?; + let result = citest.run_test(x_values.to_owned(), y_values.to_owned(), z.to_owned())?; Ok(util::test_result_to_robj(result)) } }; @@ -70,10 +62,15 @@ fn list_ci_tests_for(data_type: &str) -> anyhow::Result> { "discrete" => CITestDataType::Discrete, "continuous" => CITestDataType::Continuous, "mixed" => CITestDataType::Mixed, - _ => anyhow::bail!("Unknown data type: '{data_type}'. Use 'discrete', 'continuous', or 'mixed'."), + _ => anyhow::bail!( + "Unknown data type: '{data_type}'. Use 'discrete', 'continuous', or 'mixed'." + ), }; let registry = Registry::new(); - let mut tests: Vec = registry.tests_with_data_type(&dt)?.map(String::from).collect(); + let mut tests: Vec = registry + .tests_with_data_type(&dt)? + .map(String::from) + .collect(); tests.sort(); Ok(tests) } @@ -86,7 +83,6 @@ r_ci_test!(freeman_tukey_test, FreemanTukey); r_ci_test!(modified_likelihood_test, ModifiedLikelihood); r_ci_test!(pearson_equivalence_test, PearsonEquivalence); - extendr_module! { mod cir; fn list_ci_tests; @@ -98,4 +94,4 @@ extendr_module! { fn freeman_tukey_test; fn modified_likelihood_test; fn pearson_equivalence_test; -} \ No newline at end of file +} diff --git a/crates/cir/src/rust/src/util.rs b/crates/cir/src/rust/src/util.rs index c322ec6..4804aaa 100644 --- a/crates/cir/src/rust/src/util.rs +++ b/crates/cir/src/rust/src/util.rs @@ -1,6 +1,6 @@ //! Conversion utilities from core result types to R objects. -use ci_core::strategy::{TestResult}; +use ci_core::strategy::TestResult; use extendr_api::prelude::*; /// Converts a [`TestResult`] into a named R list. @@ -11,20 +11,16 @@ use extendr_api::prelude::*; /// - `"boolean"` — also contains `independent` (logical). pub fn test_result_to_robj(r: TestResult) -> Robj { match r { - TestResult::PValue(p, coef) => list!( - kind = "pvalue", - p_value = p, - coefficient = coef, - ).into(), + TestResult::PValue(p, coef) => { + list!(kind = "pvalue", p_value = p, coefficient = coef,).into() + } TestResult::Statistic(p, stat, df) => list!( kind = "statistic", statistic = stat, p_value = p, df = df as i32, - ).into(), - TestResult::Boolean(b) => list!( - kind = "boolean", - independent = b, - ).into(), + ) + .into(), + TestResult::Boolean(b) => list!(kind = "boolean", independent = b,).into(), } -} \ No newline at end of file +} From 40f4cc2ccf0673f45f142e38fa6f09a5e1920f63 Mon Sep 17 00:00:00 2001 From: Johan Voskamp <55276630+Hiddentale@users.noreply.github.com> Date: Fri, 8 May 2026 10:40:38 +0200 Subject: [PATCH 14/20] feat: ci workflow for R tests --- .github/workflows/ci.yml | 49 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c0addc..0a7db51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,12 @@ jobs: - name: Install system dependencies run: sudo apt-get install -y libopenblas-dev - + + - name: Install R + uses: r-lib/actions/setup-r@v2 + with: + r-version: release + - name: Install Rust uses: dtolnay/rust-toolchain@stable with: @@ -47,10 +52,10 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Run Clippy - run: cargo clippy --workspace --exclude ci_python --exclude ci_r --exclude ci_js --all-targets -- -D warnings + run: cargo clippy --workspace --exclude ci_python --exclude ci_js --all-targets -- -D warnings test: - name: Test Suite + name: Rust Tests needs: [fmt, clippy] runs-on: ${{ matrix.os }} strategy: @@ -76,7 +81,41 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Build - run: cargo build --workspace --exclude ci_python --exclude ci_r --exclude ci_js --verbose + run: cargo build --workspace --exclude ci_python --exclude cir --exclude ci_js --verbose - name: Run tests - run: cargo test --workspace --exclude ci_python --exclude ci_r --exclude ci_js --verbose + run: cargo test --workspace --exclude ci_python --exclude cir --exclude ci_js --verbose + + r-test: + name: R Package Tests + needs: [fmt, clippy] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Install system dependencies + run: sudo apt-get install -y libopenblas-dev + + - name: Install R + uses: r-lib/actions/setup-r@v2 + with: + r-version: release + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + + - name: Install R package dependencies + run: install.packages(c("devtools", "testthat", "rextendr")) + shell: Rscript {0} + + - name: Generate Makevars and R wrappers + working-directory: crates/cir + run: rextendr::document() + shell: Rscript {0} + + - name: Run R tests + working-directory: crates/cir + run: devtools::test(reporter = "summary") + shell: Rscript {0} From cbd2692571dd474a624946e5bbcd9fb2f5bf45b5 Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Fri, 8 May 2026 13:45:29 +0200 Subject: [PATCH 15/20] refactor: made README better and added temp email to DESCRIPTION --- .github/workflows/ci.yml | 6 +++-- crates/cir/DESCRIPTION | 7 +++++- crates/cir/R/extendr-wrappers.R | 5 +++++ crates/cir/README.md | 39 +++++++++++++++++++++++---------- 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a7db51..c8b93a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,8 +107,10 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Install R package dependencies - run: install.packages(c("devtools", "testthat", "rextendr")) - shell: Rscript {0} + uses: r-lib/actions/setup-r-dependencies@v2 + with: + working-directory: crates/cir + extra-packages: any::devtools, any::rextendr - name: Generate Makevars and R wrappers working-directory: crates/cir diff --git a/crates/cir/DESCRIPTION b/crates/cir/DESCRIPTION index daf6886..3f20518 100644 --- a/crates/cir/DESCRIPTION +++ b/crates/cir/DESCRIPTION @@ -2,7 +2,7 @@ Package: cir Title: Conditional Independence Testing Version: 0.0.0.9000 Authors@R: - person("GIP", "House", role = c("aut", "cre")) + person("GIP", "House", email = "giphouse@example.com", role = c("aut", "cre")) Description: R bindings for a collection of conditional independence tests implemented in Rust. Supports discrete, continuous, and mixed data via chi-squared, log-likelihood, Cressie-Read, Freeman-Tukey, Pearson @@ -15,3 +15,8 @@ Config/rextendr/version: 0.5.0 SystemRequirements: Cargo (Rust's package manager), rustc >= 1.65.0, xz Depends: R (>= 4.2) +Suggests: + devtools, + rextendr, + testthat (>= 3.0.0) +Config/testthat/edition: 3 diff --git a/crates/cir/R/extendr-wrappers.R b/crates/cir/R/extendr-wrappers.R index d7a3bef..02e79f9 100644 --- a/crates/cir/R/extendr-wrappers.R +++ b/crates/cir/R/extendr-wrappers.R @@ -5,8 +5,13 @@ #' @useDynLib cir, .registration = TRUE NULL +#' Returns a sorted vector of all registered CI test names. list_ci_tests <- function() .Call(wrap__list_ci_tests) +#' Returns a sorted vector of CI test names compatible with the given data type. +#' +#' `data_type` must be one of `"discrete"`, `"continuous"`, or `"mixed"` (case-insensitive). +#' Returns an error for any other value. list_ci_tests_for <- function(data_type) .Call(wrap__list_ci_tests_for, data_type) chi_squared_test <- function(x_values, y_values, z, boolean, significance_level) .Call(wrap__chi_squared_test, x_values, y_values, z, boolean, significance_level) diff --git a/crates/cir/README.md b/crates/cir/README.md index e4c5279..f39a747 100644 --- a/crates/cir/README.md +++ b/crates/cir/README.md @@ -1,18 +1,35 @@ extendr bindings -##Usage +## Usage -Install R -In your command line open an R session by typing: R -install.packages("rextendr") (this takes 5-15 mins) +Install R, then open an R session: -usethis::create_package("crates/ci-r-pkg", open = FALSE) -setwd("crates/ci-r-pkg") -rextendr::use_extendr() +```sh +R +``` -to regenerate bindings -> install.packages("devtools") +Install `pak` (the modern R package manager that handles system dependencies automatically): -devtools::document() # generates R bindings -devtools::load_all() # compiles Rust + loads the package +```r +install.packages("pak") +``` -devtools::test() # Run all tests \ No newline at end of file +Install dev dependencies: + +```r +pak::pak(c("devtools", "rextendr")) +``` + +### Regenerate bindings + +```r +setwd("crates/cir") +rextendr::document() # regenerates R wrappers and compiles Rust +``` + +### Load and test + +```r +devtools::load_all() # compiles Rust + loads the package +devtools::test() # run all tests +``` From edb473232f8df5b2cee3a7775afc958cd8583c6d Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Fri, 8 May 2026 14:00:37 +0200 Subject: [PATCH 16/20] refactor: added specification to readme --- crates/cir/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cir/README.md b/crates/cir/README.md index f39a747..b49d77a 100644 --- a/crates/cir/README.md +++ b/crates/cir/README.md @@ -11,7 +11,7 @@ R Install `pak` (the modern R package manager that handles system dependencies automatically): ```r -install.packages("pak") +install.packages("pak", repos = "https://cloud.r-project.org") ``` Install dev dependencies: From 534e5a2bf726b66ca6967a6ad0ffc56731608db3 Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Fri, 8 May 2026 14:16:50 +0200 Subject: [PATCH 17/20] refactor: clippy and fmt --- crates/ci-core/src/ci_tests/pearson_correlation.rs | 1 + crates/ci-core/src/ci_tests/pearson_equivalence.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/crates/ci-core/src/ci_tests/pearson_correlation.rs b/crates/ci-core/src/ci_tests/pearson_correlation.rs index a876520..1214b7a 100644 --- a/crates/ci-core/src/ci_tests/pearson_correlation.rs +++ b/crates/ci-core/src/ci_tests/pearson_correlation.rs @@ -85,6 +85,7 @@ impl CITest for PearsonCorrelation { } /// Construct the appropriate [`TestResult`] variant based on the `boolean` flag. +#[must_use] pub fn wrap_result( boolean: bool, p_value: f64, diff --git a/crates/ci-core/src/ci_tests/pearson_equivalence.rs b/crates/ci-core/src/ci_tests/pearson_equivalence.rs index b3010c6..4b44b11 100644 --- a/crates/ci-core/src/ci_tests/pearson_equivalence.rs +++ b/crates/ci-core/src/ci_tests/pearson_equivalence.rs @@ -11,6 +11,7 @@ pub struct PearsonEquivalence { } impl PearsonEquivalence { + #[must_use] pub fn new(boolean: bool, significance_level: f64, delta_threshold: f64) -> Self { Self { boolean, @@ -87,6 +88,7 @@ impl CITest for PearsonEquivalence { } } +#[must_use] pub fn wrap_result( boolean: bool, p_value: f64, From 41b32fa1279b59619205fe3171c22561252c5a80 Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Fri, 8 May 2026 14:23:27 +0200 Subject: [PATCH 18/20] refactor: commented out unused citest in R --- crates/cir/src/rust/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cir/src/rust/src/lib.rs b/crates/cir/src/rust/src/lib.rs index f6ae097..ef0c690 100644 --- a/crates/cir/src/rust/src/lib.rs +++ b/crates/cir/src/rust/src/lib.rs @@ -81,7 +81,7 @@ r_ci_test!(cressie_read_test, CressieRead); r_ci_test!(pearson_correlation_test, PearsonCorrelation); r_ci_test!(freeman_tukey_test, FreemanTukey); r_ci_test!(modified_likelihood_test, ModifiedLikelihood); -r_ci_test!(pearson_equivalence_test, PearsonEquivalence); +//r_ci_test!(pearson_equivalence_test, PearsonEquivalence); extendr_module! { mod cir; @@ -93,5 +93,5 @@ extendr_module! { fn pearson_correlation_test; fn freeman_tukey_test; fn modified_likelihood_test; - fn pearson_equivalence_test; + //fn pearson_equivalence_test; } From 5e07aecd81d6f63b0a325564e24e53d1e912cf28 Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Fri, 8 May 2026 14:25:29 +0200 Subject: [PATCH 19/20] chore: clippy --- crates/cir/src/rust/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cir/src/rust/src/lib.rs b/crates/cir/src/rust/src/lib.rs index ef0c690..4dc1bbd 100644 --- a/crates/cir/src/rust/src/lib.rs +++ b/crates/cir/src/rust/src/lib.rs @@ -7,7 +7,7 @@ use ci_core::ci_tests::{ chi_squared::ChiSquared, cressie_read::CressieRead, freeman_tukey::FreemanTukey, log_likelihood::LogLikelihood, modified_likelihood::ModifiedLikelihood, - pearson_correlation::PearsonCorrelation, pearson_equivalence::PearsonEquivalence, + pearson_correlation::PearsonCorrelation, }; use ci_core::registry::Registry; use ci_core::strategy::{CITest, CITestDataType}; From 99bdc87cae275187b84a4c9d42cbffa91cf8f035 Mon Sep 17 00:00:00 2001 From: Hiddentale <55276630+Hiddentale@users.noreply.github.com> Date: Fri, 8 May 2026 14:26:57 +0200 Subject: [PATCH 20/20] chore: fmt --- crates/cir/src/rust/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cir/src/rust/src/lib.rs b/crates/cir/src/rust/src/lib.rs index 4dc1bbd..8cd84db 100644 --- a/crates/cir/src/rust/src/lib.rs +++ b/crates/cir/src/rust/src/lib.rs @@ -7,7 +7,7 @@ use ci_core::ci_tests::{ chi_squared::ChiSquared, cressie_read::CressieRead, freeman_tukey::FreemanTukey, log_likelihood::LogLikelihood, modified_likelihood::ModifiedLikelihood, - pearson_correlation::PearsonCorrelation, + pearson_correlation::PearsonCorrelation, }; use ci_core::registry::Registry; use ci_core::strategy::{CITest, CITestDataType};