Repository: promised-ai/changepoint (language: python)

Issue:
[Bug] on python: BOCPD configured for BetaBernoulli fails in .step() function, 'x' object cannot be converted to 'PyBool'
In the python library, initializing BOCPD using a beta bernoulli prior:
```python
cpd = chp.Bocpd(
    prior=chp.BetaBernoulli(),
    lam=30)
```
with some bernoulli sequence data
```python 
bers1 = np.random.binomial(1, 0.1, size=10000)
bers2 = np.random.binomial(1, 0.2, size=10000)
bers3 = np.random.binomial(1, 0.3, size=10000)
bers4 = np.random.binomial(1, 0.1, size=10000)
data = np.concatenate([bers1,bers2,bers3,bers4])
```

yields error when trying to update with the step function:
![image](https://github.com/promised-ai/changepoint/assets/53814901/4fe430cd-38f4-463c-a078-743c3a30f932)

The same happens when trying to cast to bool directly in python:
![image](https://github.com/promised-ai/changepoint/assets/53814901/a3316c2e-1804-4482-bcac-b240b44518b2)

[Bug] on python: BOCPD configured for BetaBernoulli fails in .step() function, 'x' object cannot be converted to 'PyBool'
In the python library, initializing BOCPD using a beta bernoulli prior:
```python
cpd = chp.Bocpd(
    prior=chp.BetaBernoulli(),
    lam=30)
```
with some bernoulli sequence data
```python 
bers1 = np.random.binomial(1, 0.1, size=10000)
bers2 = np.random.binomial(1, 0.2, size=10000)
bers3 = np.random.binomial(1, 0.3, size=10000)
bers4 = np.random.binomial(1, 0.1, size=10000)
data = np.concatenate([bers1,bers2,bers3,bers4])
```

yields error when trying to update with the step function:
![image](https://github.com/promised-ai/changepoint/assets/53814901/4fe430cd-38f4-463c-a078-743c3a30f932)

The same happens when trying to cast to bool directly in python:
![image](https://github.com/promised-ai/changepoint/assets/53814901/a3316c2e-1804-4482-bcac-b240b44518b2)



Discussion / hints:
Hey @HilaManor, thanks for reporting this.

I have a fix. I'll create a PR quickly here, but if you need a faster fix, you can change `cpd.step(x)` to `cpd.step(bool(x))`. The patch I'll release soon will fix the issue as well.
Hey @HilaManor, thanks for reporting this.

I have a fix. I'll create a PR quickly here, but if you need a faster fix, you can change `cpd.step(x)` to `cpd.step(bool(x))`. The patch I'll release soon will fix the issue as well.

Fix patch:
diff --git a/pychangepoint/CHANGELOG.md b/pychangepoint/CHANGELOG.md
new file mode 100644
index 0000000..1c0f3ca
--- /dev/null
+++ b/pychangepoint/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+## v0.3.1
+- Fix type error that occurred converting numpy `bool_` and `int` types
+- Added default arguments `NormalInvChiSquared` constructor (`m=0, k=1, v=1, s2=1`)
diff --git a/pychangepoint/Cargo.toml b/pychangepoint/Cargo.toml
index 9bdcb0c..d334df7 100644
--- a/pychangepoint/Cargo.toml
+++ b/pychangepoint/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "pychangepoint"
 description = "A Python module for changepoint"
-version = "0.3.0"
+version = "0.3.1"
 repository = "https://github.com/promised-ai/changepoint"
 authors = ["Redpoll <info@redpoll.ai>"]
 edition = "2021"
diff --git a/pychangepoint/ChangePointExample.ipynb b/pychangepoint/ChangePointExample.ipynb
index ccdc088..caa9a12 100644
--- a/pychangepoint/ChangePointExample.ipynb
+++ b/pychangepoint/ChangePointExample.ipynb
@@ -103,12 +103,19 @@
     "\n",
     "    data_lower, data_upper = 0.975 * np.min(data), 1.05 * np.max(data)\n",
     "\n",
-    "    regime_bot, regime_top = data_lower - 0.1 * (data_upper - data_lower), data_lower\n",
+    "    regime_bot, regime_top = (\n",
+    "        data_lower - 0.1 * (data_upper - data_lower),\n",
+    "        data_lower,\n",
+    "    )\n",
     "    data_ax.set_ylim(regime_bot, data_upper)\n",
     "\n",
-    "    map_change_points = np.hstack([chp.map_changepoints(change_point_history), n])\n",
+    "    map_change_points = np.hstack(\n",
+    "        [chp.map_changepoints(change_point_history), n]\n",
+    "    )\n",
     "\n",
-    "    for i, (a, b) in enumerate(zip(map_change_points[:-1], map_change_points[1:])):\n",
+    "    for i, (a, b) in enumerate(\n",
+    "        zip(map_change_points[:-1], map_change_points[1:])\n",
+    "    ):\n",
     "        data_ax.fill_between(\n",
     "            [a, b],\n",
     "            [regime_bot, regime_bot],\n",
@@ -496,7 +503,9 @@
     }
    ],
    "source": [
-    "_, (data_ax, _data_ax), change_points = change_point_plot(cpi, change_point_history)\n",
+    "_, (data_ax, _data_ax), change_points = change_point_plot(\n",
+    "    cpi, change_point_history\n",
+    ")\n",
     "\n",
     "data_ax.set_xticks([5 * 12 * i for i in range(cpi.shape[0] // (5 * 12))])\n",
     "data_ax.set_xticklabels([cpi.index[i][:4] for i in data_ax.get_xticks()])\n",
@@ -826,7 +835,9 @@
     }
    ],
    "source": [
-    "_, (data_ax, _data_ax), change_points = change_point_plot(cpi, change_point_history)\n",
+    "_, (data_ax, _data_ax), change_points = change_point_plot(\n",
+    "    cpi, change_point_history\n",
+    ")\n",
     "\n",
     "data_ax.set_xticks([5 * 12 * i for i in range(cpi.shape[0] // (5 * 12))])\n",
     "data_ax.set_xticklabels([cpi.index[i][:4] for i in data_ax.get_xticks()])\n",
diff --git a/pychangepoint/README.md b/pychangepoint/README.md
index cc66f7f..1a19b39 100644
--- a/pychangepoint/README.md
+++ b/pychangepoint/README.md
@@ -20,7 +20,7 @@ See [rustup.rs](https://rustup.rs/) for instructions on installing Rust.
 ## Quick Docs
 By convention in these docs and examples, 
 ```python
-import changepoint as chp
+import changepoint as cpt
 ```
 
 ### Models
@@ -28,18 +28,18 @@ import changepoint as chp
 
 The Bayesian change point detector, `Bocpd`, takes a prior distribution, aka one of
 ```python,ignore
-chp.BetaBernoulli
-chp.NormalGamma
-chp.NormalInvChiSquared
-chp.NormalInvGamma
-chp.NormalInvWishart
-chp.PoissonGamma
+cpt.BetaBernoulli
+cpt.NormalGamma
+cpt.NormalInvChiSquared
+cpt.NormalInvGamma
+cpt.NormalInvWishart
+cpt.PoissonGamma
 ```
 
 Then, a `Bocpd` may be created:
 ```python
-cpd = chp.Bocpd(
-    prior=chp.NormalGamma(),
+cpd = cpt.Bocpd(
+    prior=cpt.NormalGamma(),
     lam=12,
 )
 ```
@@ -58,7 +58,7 @@ change_point_history = np.zeros((n, n))
 for i, x in enumerate(data):
     change_point_history[i, : i + 1] = cpd.step(x)
 
-print(chp.map_changepoints(change_point_history))
+print(cpt.map_changepoints(change_point_history))
 ```
 
 
@@ -69,7 +69,7 @@ where `c` is the scale, `X_{i-l-1, ..., i-1}` is the previous vales in the seque
 It behaves similarity to the `Bocpd` class; for example,
 
 ```python
-argp = chp.ArgpCpd(logistic_hazard_h=-2, scale=3, noise_level=0.01)
+argp = cpt.ArgpCpd(logistic_hazard_h=-2, scale=3, noise_level=0.01)
 n = len(data)
 change_point_history = np.zeros((n + 1, n + 1))
 xs = []
@@ -78,7 +78,7 @@ for i, x in enumerate(data):
     cps = argp.step(x)
     change_point_history[i, : len(cps)] = cps
 
-print(chp.map_changepoints(change_point_history))
+print(cpt.map_changepoints(change_point_history))
 ```
 
 ## Example
diff --git a/pychangepoint/pyproject.toml b/pychangepoint/pyproject.toml
index d1e90e4..3e1180f 100644
--- a/pychangepoint/pyproject.toml
+++ b/pychangepoint/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "maturin"
 
 [project]
 name = "changepoint"
-version = "0.3.0"
+version = "0.3.1"
 authors = [
     {name = "Redpoll", email = "info@redpoll.ai" },
 ]
@@ -28,3 +28,6 @@ Repository = "https://github.com/promised-ai/changepoint"
 
 [tool.maturin]
 bindings = "pyo3"
+
+[tool.black]
+line-length = 80
diff --git a/pychangepoint/src/bocpd.rs b/pychangepoint/src/bocpd.rs
index 80df11a..196a0e3 100644
--- a/pychangepoint/src/bocpd.rs
+++ b/pychangepoint/src/bocpd.rs
@@ -1,5 +1,6 @@
+use crate::convert;
 use changepoint::BocpdLike;
-use nalgebra::{DMatrix, DVector};
+use nalgebra::DVector;
 use pyo3::exceptions::PyValueError;
 use pyo3::prelude::*;
 use rv::dist::{
@@ -7,46 +8,6 @@ use rv::dist::{
     NormalInvChiSquared, NormalInvGamma, NormalInvWishart, Poisson,
 };
 
-fn pyany_to_f64(x: &PyAny) -> PyResult<f64> {
-    x.extract()
-}
-
-fn pyany_to_bool(x: &PyAny) -> PyResult<bool> {
-    x.extract()
-}
-
-fn pyany_to_u32(x: &PyAny) -> PyResult<u32> {
-    x.extract()
-}
-
-fn pyany_to_dvector(x: &PyAny) -> PyResult<DVector<f64>> {
-    Python::with_gil(|py| {
-        let np = PyModule::import(py, "numpy")?;
-        let xs: Vec<f64> = np.getattr("array")?.call1((x,))?.extract()?;
-        Ok(xs)
-    })
-    .map(DVector::from)
-}
-
-fn pyany_to_dmatrix(x: &PyAny) -> PyResult<DMatrix<f64>> {
-    use numpy::PyArray2;
-    Python::with_gil(|py| {
-        let np = PyModule::import(py, "numpy")?;
-        let xs: &PyArray2<f64> = np.getattr("array")?.call1((x,))?.extract()?;
-        let shape = xs.shape();
-
-        let data = unsafe {
-            xs.as_slice().map_err(|_| {
-                PyValueError::new_err("Non-contiguous memory error")
-            })
-        }?;
-
-        let mat: DMatrix<f64> =
-            DMatrix::from_row_slice(shape[0], shape[1], data);
-        Ok(mat)
-    })
-}
-
 /// The variant of the prior distribution
 #[derive(Clone, Debug)]
 pub enum PriorVariant {
@@ -111,8 +72,8 @@ impl Prior {
         df: usize,
         scale: &PyAny,
     ) -> PyResult<Self> {
-        let mu_vec = pyany_to_dvector(mu)?;
-        let scale_mat = pyany_to_dmatrix(scale)?;
+        let mu_vec = convert::pyany_to_dvector(mu)?;
+        let scale_mat = convert::pyany_to_dmatrix(scale)?;
         NormalInvWishart::new(mu_vec, k, df, scale_mat)
             .map_err(|err| PyValueError::new_err(err.to_string()))
             .map(|dist| Prior {
@@ -263,8 +224,11 @@ pub fn normal_inv_gamma(m: f64, v: f64, a: f64, b: f64) -> PyResult<Prior> {
 /// ValueError:
 ///     - m, k, v, s2 is infinite or NaN,
 ///     - k, v, s2 <= 0.0
-#[pyfunction]
-#[pyo3(name = "NormalInvChiSquared")]
+#[pyfunction(signature = (m = 0.0, k = 1.0, v = 1.0, s2 = 1.0))]
+#[pyo3(
+    name = "NormalInvChiSquared",
+    text_signature = "(m = 0.0, k = 1.0, v = 1.0, s2 = 1.0)"
+)]
 pub fn normal_inv_chi_squared(
     m: f64,
     k: f64,
@@ -386,28 +350,28 @@ impl BocpdVariant {
     fn step(&mut self, datum: &PyAny) -> PyResult<Vec<f64>> {
         match self {
             Self::NormalGamma(bocpd) => {
-                let x = pyany_to_f64(datum)?;
+                let x = convert::pyany_to_f64(datum)?;
                 Ok(bocpd.step(&x).to_vec())
             }
             Self::NormalInvGamma(bocpd) => {
-                let x = pyany_to_f64(datum)?;
+                let x = convert::pyany_to_f64(datum)?;
                 Ok(bocpd.step(&x).to_vec())
             }
             Self::NormalInvChiSquared(bocpd) => {
-                let x = pyany_to_f64(datum)?;
+                let x = convert::pyany_to_f64(datum)?;
                 Ok(bocpd.step(&x).to_vec())
             }
             Self::BetaBernoulli(bocpd) => {
-                let x = pyany_to_bool(datum)?;
+                let x = convert::pyany_to_bool(datum)?;
                 Ok(bocpd.step(&x).to_vec())
             }
             Self::PoissonGamma(bocpd) => {
-                let x = pyany_to_u32(datum)?;
+                let x = convert::pyany_to_u32(datum)?;
                 Ok(bocpd.step(&x).to_vec())
             }
             Self::NormalInvWishart(bocpd) => {
                 // FIXME: check cardinality
-                let x = pyany_to_dvector(datum)?;
+                let x = convert::pyany_to_dvector(datum)?;
                 Ok(bocpd.step(&x).to_vec())
             }
         }
diff --git a/pychangepoint/src/convert.rs b/pychangepoint/src/convert.rs
new file mode 100644
index 0000000..2f68eb1
--- /dev/null
+++ b/pychangepoint/src/convert.rs
@@ -0,0 +1,43 @@
+use nalgebra::{DMatrix, DVector};
+use pyo3::exceptions::PyValueError;
+use pyo3::prelude::*;
+
+pub(crate) fn pyany_to_f64(x: &PyAny) -> PyResult<f64> {
+    x.extract()
+}
+
+pub(crate) fn pyany_to_bool(x: &PyAny) -> PyResult<bool> {
+    x.is_true()
+}
+
+pub(crate) fn pyany_to_u32(x: &PyAny) -> PyResult<u32> {
+    x.extract()
+}
+
+pub(crate) fn pyany_to_dvector(x: &PyAny) -> PyResult<DVector<f64>> {
+    Python::with_gil(|py| {
+        let np = PyModule::import(py, "numpy")?;
+        let xs: Vec<f64> = np.getattr("array")?.call1((x,))?.extract()?;
+        Ok(xs)
+    })
+    .map(DVector::from)
+}
+
+pub(crate) fn pyany_to_dmatrix(x: &PyAny) -> PyResult<DMatrix<f64>> {
+    use numpy::PyArray2;
+    Python::with_gil(|py| {
+        let np = PyModule::import(py, "numpy")?;
+        let xs: &PyArray2<f64> = np.getattr("array")?.call1((x,))?.extract()?;
+        let shape = xs.shape();
+
+        let data = unsafe {
+            xs.as_slice().map_err(|_| {
+                PyValueError::new_err("Non-contiguous memory error")
+            })
+        }?;
+
+        let mat: DMatrix<f64> =
+            DMatrix::from_row_slice(shape[0], shape[1], data);
+        Ok(mat)
+    })
+}
diff --git a/pychangepoint/src/lib.rs b/pychangepoint/src/lib.rs
index d591c2d..ecdae15 100644
--- a/pychangepoint/src/lib.rs
+++ b/pychangepoint/src/lib.rs
@@ -1,5 +1,6 @@
 pub mod argpcpd;
 pub mod bocpd;
+pub(crate) mod convert;
 
 use changepoint::utils;
 use pyo3::prelude::*;


Test patch:
diff --git a/pychangepoint/tests/test_bocpd.py b/pychangepoint/tests/test_bocpd.py
new file mode 100644
index 0000000..d769123
--- /dev/null
+++ b/pychangepoint/tests/test_bocpd.py
@@ -0,0 +1,55 @@
+import numpy as np
+import pytest
+import changepoint as cpt
+
+# cpt.BetaBernoulli
+# cpt.NormalGamma
+# cpt.NormalInvChiSquared
+# cpt.NormalInvGamma
+# cpt.NormalInvWishart
+# cpt.PoissonGamma
+
+BINARY_SEQS = [
+    [0, 1, 1, 0, 1, 1],
+    [False, True, True, False, True, True],
+    np.array([0, 1, 1, 0, 1, 1], dtype=np.int_),
+    np.array([0, 1, 1, 0, 1, 1], dtype=np.intc),
+    np.array([0, 1, 1, 0, 1, 1], dtype=np.uint),
+    np.array([0, 1, 1, 0, 1, 1], dtype=np.uintc),
+    np.array([False, True, True, False, True, True], dtype=np.bool_),
+]
+
+
+@pytest.mark.parametrize("seq", BINARY_SEQS)
+def test_beta_bernoulli(seq):
+    cpd = cpt.Bocpd(
+        prior=cpt.BetaBernoulli(),
+        lam=12,
+    )
+    for x in seq:
+        cpd.step(x)
+
+
+@pytest.mark.parametrize("dtype", [int, np.int_, np.uint, np.intc, np.uintc])
+def test_beta_poission_gamma(dtype):
+    seq = np.random.randint(10, size=(20,), dtype=dtype)
+    cpd = cpt.Bocpd(
+        prior=cpt.PoissonGamma(),
+        lam=12,
+    )
+    for x in seq:
+        cpd.step(x)
+
+
+@pytest.mark.parametrize(
+    "prior", [cpt.NormalGamma, cpt.NormalInvGamma, cpt.NormalInvChiSquared]
+)
+@pytest.mark.parametrize("dtype", [float, np.float_, np.single, np.double])
+def test_normal_x(prior, dtype):
+    seq = np.array(np.random.randn(20), dtype=dtype)
+    cpd = cpt.Bocpd(
+        prior=prior(),
+        lam=12,
+    )
+    for x in seq:
+        cpd.step(x)
diff --git a/pychangepoint/tests/test_example.py b/pychangepoint/tests/test_example.py
index 2c533a2..1b6ecee 100644
--- a/pychangepoint/tests/test_example.py
+++ b/pychangepoint/tests/test_example.py
@@ -18,7 +18,22 @@ def print_box(xs):
 
 
 def test_example():
-    data = [1.0, 2.0, 1.0, 2.0, 1.5, 1.5, 1.0, 2.0, 10.0, 11.0, 9.0, 10.0, 9.0, 11.0]
+    data = [
+        1.0,
+        2.0,
+        1.0,
+        2.0,
+        1.5,
+        1.5,
+        1.0,
+        2.0,
+        10.0,
+        11.0,
+        9.0,
+        10.0,
+        9.0,
+        11.0,
+    ]
 
     # BOCPD
     # -----
diff --git a/pychangepoint/tests/test_infer.py b/pychangepoint/tests/test_infer.py
index 230de63..936a76e 100644
--- a/pychangepoint/tests/test_infer.py
+++ b/pychangepoint/tests/test_infer.py
@@ -4,7 +4,22 @@
 def test_map_changepoints():
     bocpd = cpt.Bocpd(cpt.NormalGamma(), 50)
 
-    data = [1.0, 2.0, 1.0, 2.0, 1.5, 1.5, 1.0, 2.0, 10.0, 11.0, 9.0, 10.0, 9.0, 11.0]
+    data = [
+        1.0,
+        2.0,
+        1.0,
+        2.0,
+        1.5,
+        1.5,
+        1.0,
+        2.0,
+        10.0,
+        11.0,
+        9.0,
+        10.0,
+        9.0,
+        11.0,
+    ]
 
     rs = [bocpd.step(datum) for datum in data]
 
diff --git a/pychangepoint/tests/test_int_to_bool_conversion.py b/pychangepoint/tests/test_int_to_bool_conversion.py
new file mode 100644
index 0000000..3e0fd5e
--- /dev/null
+++ b/pychangepoint/tests/test_int_to_bool_conversion.py
@@ -0,0 +1,25 @@
+def test_int_to_bool():
+    """Test for issue #5 by HilaManor
+
+    The failure is the NumPy types `int64` and `bool_` aren't supported by
+    default in pyo3.
+    """
+    import changepoint as cpt
+    import numpy as np
+
+    cpd = cpt.Bocpd(
+        prior=cpt.BetaBernoulli(),
+        lam=30,
+    )
+
+    bers1 = np.random.binomial(1, 0.1, size=10)
+    bers2 = np.random.binomial(1, 0.2, size=10)
+    bers3 = np.random.binomial(1, 0.3, size=10)
+    bers4 = np.random.binomial(1, 0.1, size=10)
+
+    data = np.concatenate([bers1, bers2, bers3, bers4])
+
+    n = len(data)
+    change_point_history = np.zeros((n, n))
+    for i, x in enumerate(data):
+        change_point_history[i, : i + 1] = cpd.step(x)