diff --git a/CHANGELOG.md b/CHANGELOG.md index 670e4d85..3e69a477 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog +## 2.0.0 - (2025-12-11) + +**Breaking changes:** + +- Update `ndarray` to 0.17.1, `biosphere` to 0.4.2, `numpy` and `pyo3` to 0.27.1, `r-extendr` to 0.8.1, and `rand` from 0.8 to 0.9. As the rust crate `rand` changed the algorithm for the `Uniform` distribution, this breaks backwards reproducibility. + ## 1.2.1 - (2025-09-22) **Bug fixes:** diff --git a/Cargo.toml b/Cargo.toml index 00c33bd1..100991cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "changeforest" description = "Random Forests for Change Point Detection" authors = ["Malte Londschien "] repository = "https://github.com/mlondschien/changeforest/" -version = "1.2.1" +version = "2.0.0" edition = "2021" readme = "README.md" license = "BSD-3-Clause" @@ -13,14 +13,14 @@ exclude = ["/.github", "/changeforest-py", "/changeforest-r", "/testdata", ".pre name = "changeforest" [dependencies] -ndarray = "0.16" -rand = "0.8" -biosphere = "0.4.0" +ndarray = "0.17.1" +rand = "0.9" +biosphere = "0.4.2" [dev-dependencies] rstest = "0.26" assert_approx_eq = "1.1" -ndarray-rand = "0.15" -ndarray = { version = "0.16", features = ["approx"] } -ndarray-csv = "^0.5" +ndarray-rand = "0.16" +ndarray = { version = "0.17.1", features = ["approx"] } +ndarray-csv = "0.5.4" csv = "^1" diff --git a/changeforest-py/Cargo.toml b/changeforest-py/Cargo.toml index 08f11ffd..111ccbda 100644 --- a/changeforest-py/Cargo.toml +++ b/changeforest-py/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "changeforest_py" -version = "1.2.1" +version = "2.0.0" edition = "2021" readme = "README.md" @@ -12,8 +12,8 @@ crate-type = ["cdylib"] name = "changeforest" [dependencies] -numpy = "0.26" +numpy = "0.27.1" changeforest = { path = "../" } -ndarray = "0.16" -pyo3 = {version = "0.26", features = ["extension-module"]} -biosphere = "0.4.0" +ndarray = "0.17.1" +pyo3 = {version = "0.27", features = ["extension-module"]} +biosphere = "0.4.2" diff --git a/changeforest-py/pyproject.toml b/changeforest-py/pyproject.toml index 2d4501bf..d2843516 100644 --- a/changeforest-py/pyproject.toml +++ b/changeforest-py/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "changeforest" description = "Random Forests for Change Point Detection" -version = "1.2.1" +version = "2.0.0" readme = "README.md" requires-python = ">=3.8" author = "Malte Londschien " diff --git a/changeforest-py/src/control.rs b/changeforest-py/src/control.rs index 6c1ac18a..8f3f7f87 100644 --- a/changeforest-py/src/control.rs +++ b/changeforest-py/src/control.rs @@ -1,9 +1,9 @@ use biosphere::MaxFeatures; use changeforest::Control; use pyo3::exceptions; -use pyo3::prelude::{pyclass, Bound, FromPyObject, PyAny, PyErr, PyResult}; +use pyo3::prelude::{pyclass, FromPyObject, PyAny, PyErr, PyResult}; use pyo3::prelude::{Py, Python}; -use pyo3::types::PyAnyMethods; +use pyo3::Borrowed; pub fn control_from_pyobj(py: Python, obj: Option>) -> PyResult { let mut control = Control::default(); @@ -109,8 +109,10 @@ impl PyMaxFeatures { } } -impl FromPyObject<'_> for PyMaxFeatures { - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { +impl<'py> FromPyObject<'_, 'py> for PyMaxFeatures { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { if let Ok(value) = ob.extract::() { Ok(PyMaxFeatures::new(MaxFeatures::Value(value))) } else if let Ok(value) = ob.extract::() { diff --git a/changeforest-py/tests/test_changeforest.py b/changeforest-py/tests/test_changeforest.py index 6a1c7649..5bd13eda 100644 --- a/changeforest-py/tests/test_changeforest.py +++ b/changeforest-py/tests/test_changeforest.py @@ -22,11 +22,11 @@ def test_changeforest_repr(iris_dataset): result.__repr__() == """\ best_split max_gain p_value -(0, 150] 50 96.233 0.005 - ¦--(0, 50] 2 -14.191 1 - °--(50, 150] 100 52.799 0.005 - ¦--(50, 100] 53 5.44 0.245 - °--(100, 150] 136 -2.398 0.875\ +(0, 150] 50 95.867 0.005 + ¦--(0, 50] 2 -14.37 1 + °--(50, 150] 100 52.853 0.005 + ¦--(50, 100] 53 5.152 0.27 + °--(100, 150] 102 -6.981 0.9\ """ ) @@ -42,12 +42,12 @@ def test_changeforest_repr_segments(iris_dataset): result.__repr__() == """\ best_split max_gain p_value -(0, 150] 50 95.1 0.005 +(0, 150] 50 94.844 0.005 ¦--(0, 50] - °--(50, 150] 100 52.799 0.005 - ¦--(50, 100] 53 6.892 0.315 - °--(100, 150] 136 -3.516 0.68\ -""" # noqa: W291 + °--(50, 150] 100 52.853 0.005 + ¦--(50, 100] 53 5.152 0.28 + °--(100, 150] 147 -12.303 0.975\ +""" # noqa W291 ) @@ -62,10 +62,10 @@ def test_changeforest_repr_segments2(iris_dataset): result.__repr__() == """\ best_split max_gain p_value -(0, 150] 49 87.462 0.005 - ¦--(0, 49] 2 -8.889 0.995 - °--(49, 150] 102 41.237 0.005 +(0, 150] 49 87.437 0.005 + ¦--(0, 49] 2 -13.76 0.96 + °--(49, 150] 102 34.621 0.005 ¦--(49, 102] - °--(102, 150] 136 1.114 0.36\ -""" # noqa: W291 + °--(102, 150] 138 0.301 0.475\ +""" # noqa W291 ) diff --git a/changeforest-py/tests/test_control.py b/changeforest-py/tests/test_control.py index f12d896f..b0a081f6 100644 --- a/changeforest-py/tests/test_control.py +++ b/changeforest-py/tests/test_control.py @@ -27,26 +27,25 @@ "change_in_mean", {"minimal_gain_to_split": None}, [50, 100], - ), # log(150) * 4 / 150 + ), # model_selection_alpha ("iris", "bs", "knn", {"model_selection_alpha": 0.001}, []), ("iris", "bs", "knn", {"model_selection_alpha": 0.05}, [50, 100]), # random_forest_n_estimators - # This is impressive and unexpected. - ("iris", "bs", "random_forest", {"random_forest_n_estimators": 1}, [47, 99]), + ("iris", "bs", "random_forest", {"random_forest_n_estimators": 1}, [48]), ("iris", "bs", "random_forest", {"random_forest_n_estimators": 100}, [50, 100]), # Use X_test instead ("X_test", "bs", "random_forest", {"random_forest_n_estimators": 1}, []), ("X_test", "bs", "random_forest", {"random_forest_n_estimators": 1.0}, []), - ("X_test", "bs", "random_forest", {"random_forest_n_estimators": 100}, [5]), + ("X_test", "bs", "random_forest", {"random_forest_n_estimators": 100}, [3, 5]), ("X_correlated", "bs", "random_forest", {"random_forest_max_depth": 1}, []), - ("X_correlated", "bs", "random_forest", {"random_forest_max_depth": 2}, [49]), + ("X_correlated", "bs", "random_forest", {"random_forest_max_depth": 2}, [50]), ( "X_correlated", "bs", "random_forest", {"random_forest_max_features": "sqrt"}, - [49], + [50], ), ("iris", "bs", "random_forest", {"model_selection_n_permutations": 10}, []), ], diff --git a/changeforest-r/DESCRIPTION b/changeforest-r/DESCRIPTION index 9605b92a..116853fc 100644 --- a/changeforest-r/DESCRIPTION +++ b/changeforest-r/DESCRIPTION @@ -1,7 +1,7 @@ Package: changeforest Type: Package Title: Random Forests for Change Point Detection -Version: 1.2.1 +Version: 2.0.0 Author: Malte Londschien Maintainer: Malte Londschien Description: diff --git a/changeforest-r/src/rust/Cargo.toml b/changeforest-r/src/rust/Cargo.toml index bc9d3418..0b78fc95 100644 --- a/changeforest-r/src/rust/Cargo.toml +++ b/changeforest-r/src/rust/Cargo.toml @@ -1,7 +1,7 @@ [package] name = 'changeforestr' publish = false -version = '1.2.1' +version = '2.0.0' edition = '2021' [lib] @@ -11,5 +11,5 @@ name = 'changeforestr' [dependencies] extendr-api = { version="0.8", features = ["ndarray"] } changeforest = { path = "../../../" } -ndarray = "0.16" -biosphere = "0.4.0" +ndarray = "0.17.1" +biosphere = "0.4.2" diff --git a/src/segmentation.rs b/src/segmentation.rs index d62c680e..76d421fa 100644 --- a/src/segmentation.rs +++ b/src/segmentation.rs @@ -2,7 +2,7 @@ use crate::optimizer::OptimizerResult; use crate::ModelSelectionResult; use crate::Optimizer; use rand::{ - distributions::{Distribution, Uniform}, + distr::{Distribution, Uniform}, rngs::StdRng, SeedableRng, }; @@ -68,7 +68,7 @@ impl<'a> Segmentation<'a> { } SegmentationType::WBS => { let mut rng = StdRng::seed_from_u64(optimizer.control().seed); - let dist = Uniform::from(0..(optimizer.n() + 1)); + let dist = Uniform::new(0, optimizer.n() + 1).unwrap(); let mut start: usize; let mut stop: usize; @@ -181,11 +181,11 @@ mod tests { (50, 100, 62, 3000.0) ])] #[case(SegmentationType::WBS, vec![ - (73, 78, 74, 415.0), - (2, 59, 16, 684.0), - (26, 77, 38, 1836.0), - (22, 80, 36, 1856.0), - (75, 97, 80, 1870.0) + (56, 78, 61, 1452.0), + (12, 77, 28, 1430.0), + (7, 22, 10, 255.0), + (79, 80, 79, 89.0), + (62, 75, 65, 936.0) ])] fn test_generate_segments( #[case] segmentation_type: SegmentationType, @@ -212,7 +212,7 @@ mod tests { #[rstest] #[case(SegmentationType::BS, (25, 1000.))] #[case(SegmentationType::SBS, (62, 3000.))] - #[case(SegmentationType::WBS, (60, 2900.))] + #[case(SegmentationType::WBS, (60, 2958.))] fn test_optimizer(#[case] segmentation_type: SegmentationType, #[case] expected: (usize, f64)) { let control = Control::default(); let optimizer = testing::TrivialOptimizer { control: &control }; diff --git a/src/testing.rs b/src/testing.rs index 7179b5fc..07257e2c 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -209,7 +209,7 @@ pub fn array() -> Array2 { X.slice_mut(s![25..40, 2]).fill(3.); X.slice_mut(s![25..80, 1]).fill(-2.); - X + Array::random_using((100, 5), Uniform::new(0., 1.), &mut rng) + X + Array::random_using((100, 5), Uniform::new(0., 1.).unwrap(), &mut rng) } #[cfg(test)]