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/changepoint/CHANGELOG.md b/changepoint/CHANGELOG.md
new file mode 100644
index 0000000..2f78f6f
--- /dev/null
+++ b/changepoint/CHANGELOG.md
@@ -0,0 +1,12 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [0.3.1] - 
+
+### Added
+
+- (#7) Added `PartialEq` derive macro to `Bocpd` and `Argpcpd` structs.
diff --git a/changepoint/Cargo.toml b/changepoint/Cargo.toml
index 78cdc10..f2b98d9 100644
--- a/changepoint/Cargo.toml
+++ b/changepoint/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "changepoint"
-version = "0.14.0"
+version = "0.14.1"
 authors = [
     "Mike Schmidt <schmidmt@gmail.com>",
     "Baxter Eaves <baxyjr@gmail.com>",
diff --git a/changepoint/src/bocpd.rs b/changepoint/src/bocpd.rs
index 38eed6a..e5924e4 100644
--- a/changepoint/src/bocpd.rs
+++ b/changepoint/src/bocpd.rs
@@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize};
 
 /// Online Bayesian Change Point Detection state container.
 #[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, PartialEq)]
 pub struct Bocpd<X, Fx, Pr>
 where
     Fx: Rv<X> + HasSuffStat<X>,
diff --git a/pychangepoint/CHANGELOG.md b/pychangepoint/CHANGELOG.md
new file mode 100644
index 0000000..ee9aaf7
--- /dev/null
+++ b/pychangepoint/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [0.3.1] - 
+- Added Pickle support for `ArgpCpd` and `Bocpd`
+- 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..b1db56c 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"
@@ -16,12 +16,14 @@ name = "changepoint"
 crate-type = ["cdylib"]
 
 [dependencies]
-changepoint = { path = "../changepoint" }
+bincode = "1.3.3"
+changepoint = { path = "../changepoint", features = ["serde1"] }
 nalgebra = { version = "0.32" }
 numpy = "0.19"
 pyo3 = { version ="0.19", features = ["extension-module"] }
 rand = { version = "0.8", features = ["small_rng"] }
 rv = { version = "0.16", features = ["process", "arraydist"] }
+serde = "1.0.175"
 
 [features]
 extension-module = ["pyo3/extension-module"]
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/argpcpd.rs b/pychangepoint/src/argpcpd.rs
index 32fc482..0efbc23 100644
--- a/pychangepoint/src/argpcpd.rs
+++ b/pychangepoint/src/argpcpd.rs
@@ -1,7 +1,10 @@
+use bincode::{deserialize, serialize};
 use changepoint::gp::Argpcp;
 use changepoint::BocpdLike;
 use pyo3::exceptions::PyValueError;
 use pyo3::prelude::*;
+use pyo3::pyclass::CompareOp;
+use pyo3::types::PyBytes;
 use rv::process::gaussian::kernel::{
     AddKernel, ConstantKernel, ProductKernel, RBFKernel, WhiteKernel,
 };
@@ -30,7 +33,7 @@ use rv::process::gaussian::kernel::{
 ///     Roughly the slope of the logistic hazard function
 /// logistic_hazard_b: float
 ///     The offset of the logistic hazard function.
-#[pyclass]
+#[pyclass(module = "changepoint")]
 pub struct ArgpCpd {
     argpcpd: Argpcp<
         AddKernel<ProductKernel<ConstantKernel, RBFKernel>, WhiteKernel>,
@@ -105,4 +108,33 @@ impl ArgpCpd {
     pub fn step(&mut self, datum: f64) -> Vec<f64> {
         self.argpcpd.step(&datum).to_vec()
     }
+
+    pub fn __setstate__(
+        &mut self,
+        py: Python,
+        state: PyObject,
+    ) -> PyResult<()> {
+        match state.extract::<&PyBytes>(py) {
+            Ok(s) => {
+                self.argpcpd = deserialize(s.as_bytes()).unwrap();
+                Ok(())
+            }
+            Err(e) => Err(e),
+        }
+    }
+
+    pub fn __getstate__(&self, py: Python) -> PyResult<PyObject> {
+        Ok(PyBytes::new(py, &serialize(&self.argpcpd).unwrap()).to_object(py))
+    }
+
+    fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult<bool> {
+        match op {
+            CompareOp::Lt => Ok(false),
+            CompareOp::Le => Ok(false),
+            CompareOp::Eq => Ok(self.argpcpd == other.argpcpd),
+            CompareOp::Ne => Ok(self.argpcpd != other.argpcpd),
+            CompareOp::Gt => Ok(false),
+            CompareOp::Ge => Ok(false),
+        }
+    }
 }
diff --git a/pychangepoint/src/bocpd.rs b/pychangepoint/src/bocpd.rs
index 80df11a..0ad4b1a 100644
--- a/pychangepoint/src/bocpd.rs
+++ b/pychangepoint/src/bocpd.rs
@@ -1,54 +1,20 @@
+use crate::convert;
 use changepoint::BocpdLike;
-use nalgebra::{DMatrix, DVector};
-use pyo3::exceptions::PyValueError;
+use nalgebra::DVector;
+use pyo3::exceptions::{PyTypeError, PyValueError};
 use pyo3::prelude::*;
+use pyo3::pyclass::CompareOp;
+use pyo3::types::PyTuple;
 use rv::dist::{
     Bernoulli, Beta, Gamma, Gaussian, MvGaussian, NormalGamma,
     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)
-    })
-}
+use bincode::{deserialize, serialize};
+use serde::{Deserialize, Serialize};
 
 /// The variant of the prior distribution
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
 pub enum PriorVariant {
     NormalGamma(NormalGamma),
     NormalInvGamma(NormalInvGamma),
@@ -60,14 +26,61 @@ pub enum PriorVariant {
 
 /// Prior distribution, which also describes the liklihood distribution of the
 /// change point detector.
-#[pyclass]
-#[derive(Clone, Debug)]
+#[pyclass(module = "changepoint")]
+#[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct Prior {
     pub dist: PriorVariant,
 }
 
+macro_rules! count {
+    () => (0usize);
+    ( $x:tt $($xs:tt)* ) => (1usize + count!($($xs)*));
+}
+
+macro_rules! handle_kind {
+    ($name: ident, $args: ident, $($idx:tt $n:tt),+) => {{
+        if $args.len() == count!($($idx)*) {
+            Self::$name(
+                $(
+                    $args.get_item($idx)?.extract()?,
+                )+
+            )
+        } else {
+            Err(PyTypeError::new_err(format!("Prior kind '{}' requires the following arguments: {}", stringify!($name), stringify!($($n),+))))
+        }
+    }}
+}
+
 #[pymethods]
 impl Prior {
+    #[new]
+    #[pyo3(signature = (kind, *args))]
+    pub fn new(kind: &str, args: &PyTuple) -> PyResult<Self> {
+        match kind {
+            "normal_gamma" => {
+                handle_kind!(normal_gamma, args, 0 m, 1 r, 2 s, 3 v)
+            }
+            "normal_inv_gamma" => {
+                handle_kind!(normal_inv_gamma, args, 0 m, 1 v, 2 a, 3 b)
+            }
+            "normal_inv_chi_squared" => {
+                handle_kind!(normal_inv_chi_squared, args, 0 m, 1 k, 2 v, 3 s2)
+            }
+            "normal_inv_wishart" => {
+                handle_kind!(normal_inv_wishart, args, 0 m, 1 k, 2 df, 3 scale)
+            }
+            "beta_bernoulli" => {
+                handle_kind!(beta_bernoulli, args, 0 alpha, 1 beta)
+            }
+            "poisson_gamma" => {
+                handle_kind!(poisson_gamma, args, 0 shape, 1 rate)
+            }
+            unknown_kind => Err(PyTypeError::new_err(format!(
+                "Unknown prior kind '{unknown_kind}'"
+            ))),
+        }
+    }
+
     #[staticmethod]
     #[pyo3(signature = (m = 0.0, r = 1.0, s = 1.0, v = 1.0))]
     pub fn normal_gamma(m: f64, r: f64, s: f64, v: f64) -> PyResult<Self> {
@@ -111,8 +124,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 {
@@ -182,6 +195,19 @@ impl Prior {
             ),
         }
     }
+
+    pub fn __setstate__(&mut self, state: Vec<u8>) -> PyResult<()> {
+        self.dist = deserialize(&state).unwrap();
+        Ok(())
+    }
+
+    pub fn __getstate__(&self) -> PyResult<Vec<u8>> {
+        Ok(serialize(&self.dist).unwrap())
+    }
+
+    pub fn __getnewargs__(&self) -> PyResult<(String, f64, f64, f64, f64)> {
+        Ok(("normal_gamma".to_string(), 0.0, 1.0, 1.0, 1.0))
+    }
 }
 
 /// Normal Gamma prior on univariate Normal random variable.
@@ -263,8 +289,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,
@@ -357,7 +386,7 @@ fn dist_to_bocpd(dist: Prior, lambda: f64) -> BocpdVariant {
 
 /// The variant of the `Bocpd`. Describes the prior, likelihood, and the input
 /// data type.
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
 pub enum BocpdVariant {
     NormalGamma(changepoint::Bocpd<f64, Gaussian, NormalGamma>),
     NormalInvGamma(changepoint::Bocpd<f64, Gaussian, NormalInvGamma>),
@@ -386,28 +415,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())
             }
         }
@@ -415,8 +444,8 @@ impl BocpdVariant {
 }
 
 /// Online Bayesian Change Point Detection state container
-#[derive(Clone, Debug)]
-#[pyclass]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+#[pyclass(module = "changepoint")]
 /// Create a new BOCPD
 ///
 /// Parameters
@@ -459,4 +488,28 @@ impl Bocpd {
     pub fn step(&mut self, datum: &PyAny) -> PyResult<Vec<f64>> {
         self.bocpd.step(datum).map(|rs| rs.to_vec())
     }
+
+    pub fn __setstate__(&mut self, state: Vec<u8>) -> PyResult<()> {
+        self.bocpd = deserialize(&state).unwrap();
+        Ok(())
+    }
+
+    pub fn __getstate__(&self) -> PyResult<Vec<u8>> {
+        Ok(serialize(&self.bocpd).unwrap())
+    }
+
+    pub fn __getnewargs__(&self) -> PyResult<(Prior, f64)> {
+        Ok((Prior::beta_bernoulli(0.5, 0.5).unwrap(), 1.0))
+    }
+
+    fn __richcmp__(&self, other: &Self, op: CompareOp) -> PyResult<bool> {
+        match op {
+            CompareOp::Lt => Ok(false),
+            CompareOp::Le => Ok(false),
+            CompareOp::Eq => Ok(self.bocpd == other.bocpd),
+            CompareOp::Ne => Ok(self.bocpd != other.bocpd),
+            CompareOp::Gt => Ok(false),
+            CompareOp::Ge => Ok(false),
+        }
+    }
 }
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)
diff --git a/pychangepoint/tests/test_pickle.py b/pychangepoint/tests/test_pickle.py
new file mode 100644
index 0000000..7810f9b
--- /dev/null
+++ b/pychangepoint/tests/test_pickle.py
@@ -0,0 +1,16 @@
+from changepoint import Bocpd, ArgpCpd, NormalGamma
+import pickle
+
+
+def test_pickle_bocpd():
+    cpd = Bocpd(NormalGamma(), 12)
+    s = pickle.dumps(cpd)
+    cpd_b = pickle.loads(s)
+    assert cpd == cpd_b
+
+
+def test_pickle_argpcpd():
+    cpd = ArgpCpd(logistic_hazard_h=-2, scale=3, noise_level=0.01)
+    s = pickle.dumps(cpd)
+    cpd_b = pickle.loads(s)
+    assert cpd == cpd_b