From 6d5f7a1045ea412eac9618f529613400555506d2 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Sun, 10 Nov 2024 18:23:50 +0000 Subject: [PATCH 1/6] chore: upgrade pyo3 to 0.23 Some of the APIs have changed due to changes in PyO3, but overall I think this is simpler. --- crates/pyaugurs/Cargo.toml | 4 ++-- crates/pyaugurs/augurs.pyi | 5 +---- crates/pyaugurs/src/clustering.rs | 6 +----- crates/pyaugurs/src/distance.rs | 2 +- crates/pyaugurs/src/dtw.rs | 1 + crates/pyaugurs/src/ets.rs | 2 ++ crates/pyaugurs/src/lib.rs | 8 ++++---- crates/pyaugurs/src/mstl.rs | 24 ++++++++++++++++++++---- crates/pyaugurs/src/seasons.rs | 7 ++----- crates/pyaugurs/src/trend.rs | 26 ++++++++++++-------------- 10 files changed, 46 insertions(+), 39 deletions(-) diff --git a/crates/pyaugurs/Cargo.toml b/crates/pyaugurs/Cargo.toml index e66f4fa..c7889c5 100644 --- a/crates/pyaugurs/Cargo.toml +++ b/crates/pyaugurs/Cargo.toml @@ -26,8 +26,8 @@ augurs-ets = { workspace = true, features = ["mstl"] } augurs-forecaster.workspace = true augurs-mstl.workspace = true augurs-seasons.workspace = true -numpy = "0.21.0" -pyo3 = { version = "0.21.0", features = ["extension-module"] } +numpy = "0.23.0" +pyo3 = { version = "0.23.3", features = ["extension-module"] } tracing = { version = "0.1.37", features = ["log"] } [lints] diff --git a/crates/pyaugurs/augurs.pyi b/crates/pyaugurs/augurs.pyi index 41c88e8..e635522 100644 --- a/crates/pyaugurs/augurs.pyi +++ b/crates/pyaugurs/augurs.pyi @@ -48,15 +48,12 @@ class Forecast: def lower(self) -> npt.NDArray[np.float64] | None: ... def upper(self) -> npt.NDArray[np.float64] | None: ... -class PyTrendModel: - def __init__(self, trend_model: TrendModel) -> None: ... - class MSTL: @classmethod def ets(cls, periods: Sequence[int]) -> "MSTL": ... @classmethod def custom_trend( - cls, periods: Sequence[int], trend_model: PyTrendModel + cls, periods: Sequence[int], trend_model: TrendModel ) -> "MSTL": ... def fit(self, y: npt.NDArray[np.float64]) -> None: ... def predict(self, horizon: int, level: float | None) -> Forecast: ... diff --git a/crates/pyaugurs/src/clustering.rs b/crates/pyaugurs/src/clustering.rs index 16d4fb2..adad947 100644 --- a/crates/pyaugurs/src/clustering.rs +++ b/crates/pyaugurs/src/clustering.rs @@ -84,10 +84,6 @@ impl Dbscan { distance_matrix: InputDistanceMatrix<'_>, ) -> PyResult>> { let distance_matrix = distance_matrix.try_into()?; - Ok(self - .inner - .fit(&distance_matrix) - .into_pyarray_bound(py) - .into()) + Ok(self.inner.fit(&distance_matrix).into_pyarray(py).into()) } } diff --git a/crates/pyaugurs/src/distance.rs b/crates/pyaugurs/src/distance.rs index e8d5bf4..343e050 100644 --- a/crates/pyaugurs/src/distance.rs +++ b/crates/pyaugurs/src/distance.rs @@ -50,7 +50,7 @@ impl DistanceMatrix { *elem = *val; } } - arr.into_pyarray_bound(py).into() + arr.into_pyarray(py).into() } } diff --git a/crates/pyaugurs/src/dtw.rs b/crates/pyaugurs/src/dtw.rs index b65d0f6..3278f26 100644 --- a/crates/pyaugurs/src/dtw.rs +++ b/crates/pyaugurs/src/dtw.rs @@ -148,6 +148,7 @@ impl Dtw { } #[new] + #[pyo3(signature = (window=None, distance_fn=None, max_distance=None, lower_bound=None, upper_bound=None))] fn new( window: Option, distance_fn: Option<&str>, diff --git a/crates/pyaugurs/src/ets.rs b/crates/pyaugurs/src/ets.rs index b18907e..cc86a20 100644 --- a/crates/pyaugurs/src/ets.rs +++ b/crates/pyaugurs/src/ets.rs @@ -63,6 +63,7 @@ impl AutoETS { /// # Errors /// /// This function will return an error if no model has been fit yet (using [`AutoETS::fit`]). + #[pyo3(signature = (horizon, level=None))] pub fn predict(&self, horizon: usize, level: Option) -> PyResult { self.fitted .as_ref() @@ -80,6 +81,7 @@ impl AutoETS { /// # Errors /// /// This function will return an error if no model has been fit yet (using [`AutoETS::fit`]). + #[pyo3(signature = (level=None))] pub fn predict_in_sample(&self, level: Option) -> PyResult { self.fitted .as_ref() diff --git a/crates/pyaugurs/src/lib.rs b/crates/pyaugurs/src/lib.rs index 926019b..fc8703c 100644 --- a/crates/pyaugurs/src/lib.rs +++ b/crates/pyaugurs/src/lib.rs @@ -38,6 +38,7 @@ impl From for augurs_core::Forecast { #[pymethods] impl Forecast { #[new] + #[pyo3(signature = (point, level=None, lower=None, upper=None))] fn new( py: Python<'_>, point: Py>, @@ -80,7 +81,7 @@ impl Forecast { // We could also use `into_pyarray` to construct the // numpy arrays in the Rust heap; let's see which ends up being // faster and more convenient. - self.inner.point.to_pyarray_bound(py).into() + self.inner.point.to_pyarray(py).into() } /// Get the lower prediction interval. @@ -88,7 +89,7 @@ impl Forecast { self.inner .intervals .as_ref() - .map(|x| x.lower.to_pyarray_bound(py).into()) + .map(|x| x.lower.to_pyarray(py).into()) } /// Get the upper prediction interval. @@ -96,7 +97,7 @@ impl Forecast { self.inner .intervals .as_ref() - .map(|x| x.upper.to_pyarray_bound(py).into()) + .map(|x| x.upper.to_pyarray(py).into()) } } @@ -106,7 +107,6 @@ fn augurs(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { // pyo3_log::init(); m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/crates/pyaugurs/src/mstl.rs b/crates/pyaugurs/src/mstl.rs index 3d8fadc..37a132d 100644 --- a/crates/pyaugurs/src/mstl.rs +++ b/crates/pyaugurs/src/mstl.rs @@ -44,16 +44,30 @@ impl MSTL { } } - /// Create a new MSTL model with the given periods using provided trend model. + /// Create a new MSTL model with the given periods using the custom Python trend model. + /// + /// The custom trend model must implement the following methods: + /// + /// - `fit(self, y: np.ndarray) -> None` + /// - `predict(self, horizon: int, level: float | None = None) -> augurs.Forecast` + /// - `predict_in_sample(self, level: float | None = None) -> augurs.Forecast` #[classmethod] pub fn custom_trend( _cls: &Bound<'_, PyType>, periods: Vec, - trend_model: PyTrendModel, + trend_model: Py, ) -> Self { - let trend_model_name = trend_model.name().to_string(); + let trend_model_name = Python::with_gil(|py| { + let trend_model = trend_model.bind(py).get_type(); + trend_model + .name() + .map_or_else(|_| "unknown Python class".into(), |s| s.to_string()) + }); Self { - forecaster: Forecaster::new(MSTLModel::new(periods, Box::new(trend_model))), + forecaster: Forecaster::new(MSTLModel::new( + periods, + Box::new(PyTrendModel::new(trend_model)), + )), trend_model_name, fit: false, } @@ -72,6 +86,7 @@ impl MSTL { /// intervals at the given level. /// /// If provided, `level` must be a float between 0 and 1. + #[pyo3(signature = (horizon, level=None))] pub fn predict(&self, horizon: usize, level: Option) -> PyResult { self.forecaster .predict(horizon, level) @@ -83,6 +98,7 @@ impl MSTL { /// intervals at the given level. /// /// If provided, `level` must be a float between 0 and 1. + #[pyo3(signature = (level=None))] pub fn predict_in_sample(&self, level: Option) -> PyResult { self.forecaster .predict_in_sample(level) diff --git a/crates/pyaugurs/src/seasons.rs b/crates/pyaugurs/src/seasons.rs index 5d7472c..fa43b28 100644 --- a/crates/pyaugurs/src/seasons.rs +++ b/crates/pyaugurs/src/seasons.rs @@ -16,6 +16,7 @@ use augurs_seasons::{Detector, PeriodogramDetector}; /// The default is 0.9. /// :return: an array of season lengths. #[pyfunction] +#[pyo3(signature = (y, min_period=None, max_period=None, threshold=None))] pub fn seasonalities( py: Python<'_>, y: PyReadonlyArray1<'_, f64>, @@ -35,9 +36,5 @@ pub fn seasonalities( builder = builder.threshold(threshold); } - Ok(builder - .build() - .detect(y.as_slice()?) - .to_pyarray_bound(py) - .into()) + Ok(builder.build().detect(y.as_slice()?).to_pyarray(py).into()) } diff --git a/crates/pyaugurs/src/trend.rs b/crates/pyaugurs/src/trend.rs index 289d525..e73aa05 100644 --- a/crates/pyaugurs/src/trend.rs +++ b/crates/pyaugurs/src/trend.rs @@ -10,8 +10,9 @@ //! - `fit(self, y: np.ndarray) -> None` //! - `predict(self, horizon: int, level: float | None = None) -> augurs.Forecast` //! - `predict_in_sample(self, level: float | None = None) -> augurs.Forecast` + use numpy::ToPyArray; -use pyo3::{exceptions::PyException, prelude::*}; +use pyo3::{exceptions::PyException, prelude::*, types::PyAnyMethods}; use augurs_mstl::{FittedTrendModel, TrendModel}; @@ -28,8 +29,8 @@ use crate::Forecast; /// - `predict(self, horizon: int, level: float | None = None) -> augurs.Forecast` /// - `predict_in_sample(self, level: float | None = None) -> augurs.Forecast` #[pyclass(name = "TrendModel")] -#[derive(Clone, Debug)] -pub struct PyTrendModel { +#[derive(Debug)] +pub(crate) struct PyTrendModel { model: Py, } @@ -44,7 +45,7 @@ impl PyTrendModel { /// The returned PyTrendModel can be used in MSTL models using the /// `custom_trend` method of the MSTL class. #[new] - pub fn new(model: Py) -> Self { + pub(crate) fn new(model: Py) -> Self { Self { model } } } @@ -56,7 +57,7 @@ impl TrendModel for PyTrendModel { .bind(py) .get_type() .name() - .map(|s| s.into_owned().into()) + .map(|s| s.to_string().into()) }) .unwrap_or_else(|_| "unknown Python class".into()) } @@ -68,21 +69,18 @@ impl TrendModel for PyTrendModel { Box, Box, > { - // TODO - `fitted` should be a `PyFittedTrendModel` - // which should implement `Fit` and `FittedTrendModel` - Python::with_gil(|py| { - let np = y.to_pyarray_bound(py); - self.model.call_method1(py, "fit", (np,)) + let model = Python::with_gil(|py| { + let np = y.to_pyarray(py); + self.model.call_method1(py, "fit", (np,))?; + Ok::<_, PyErr>(self.model.clone_ref(py)) })?; - Ok(Box::new(PyFittedTrendModel { - model: self.model.clone(), - }) as _) + Ok(Box::new(PyFittedTrendModel { model }) as _) } } /// A wrapper for a Python trend model that has been fitted to data. #[derive(Debug)] -pub struct PyFittedTrendModel { +pub(crate) struct PyFittedTrendModel { model: Py, } From 8ef896a4043c6537192f01278a70445adffe9747 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Mon, 9 Dec 2024 14:50:48 +0000 Subject: [PATCH 2/6] Regenerate Python CI using latest maturin --- .github/workflows/python.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 09fa0d1..bf22015 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -1,4 +1,4 @@ -# This file is autogenerated by maturin v1.6.0 +# This file is autogenerated by maturin v1.7.8 # To update, run # # maturin generate-ci github -m crates/pyaugurs/Cargo.toml @@ -24,17 +24,17 @@ jobs: strategy: matrix: platform: - - runner: ubuntu-latest + - runner: ubuntu-22.04 target: x86_64 - - runner: ubuntu-latest + - runner: ubuntu-22.04 target: x86 - - runner: ubuntu-latest + - runner: ubuntu-22.04 target: aarch64 - - runner: ubuntu-latest + - runner: ubuntu-22.04 target: armv7 - - runner: ubuntu-latest + - runner: ubuntu-22.04 target: s390x - - runner: ubuntu-latest + - runner: ubuntu-22.04 target: ppc64le steps: - uses: actions/checkout@v4 @@ -59,13 +59,13 @@ jobs: strategy: matrix: platform: - - runner: ubuntu-latest + - runner: ubuntu-22.04 target: x86_64 - - runner: ubuntu-latest + - runner: ubuntu-22.04 target: x86 - - runner: ubuntu-latest + - runner: ubuntu-22.04 target: aarch64 - - runner: ubuntu-latest + - runner: ubuntu-22.04 target: armv7 steps: - uses: actions/checkout@v4 @@ -117,7 +117,7 @@ jobs: strategy: matrix: platform: - - runner: macos-12 + - runner: macos-13 target: x86_64 - runner: macos-14 target: aarch64 @@ -139,7 +139,7 @@ jobs: path: dist sdist: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build sdist @@ -155,7 +155,7 @@ jobs: release: name: Release - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest if: "startsWith(github.ref, 'refs/tags/pyaugurs-')" needs: [linux, musllinux, windows, macos, sdist] permissions: From 6b762a78fc127373098cde9863911a45e58abe95 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Mon, 9 Dec 2024 14:50:53 +0000 Subject: [PATCH 3/6] Update uv.lock --- crates/pyaugurs/uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/pyaugurs/uv.lock b/crates/pyaugurs/uv.lock index 40e6f25..a9b9f0f 100644 --- a/crates/pyaugurs/uv.lock +++ b/crates/pyaugurs/uv.lock @@ -3,7 +3,7 @@ requires-python = ">=3.9" [[package]] name = "augurs" -version = "0.6.2" +version = "0.7.0" source = { editable = "." } dependencies = [ { name = "numpy" }, From 5777328185a4b3a5bb337f8fc479f387e7ea8e01 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Mon, 9 Dec 2024 15:11:36 +0000 Subject: [PATCH 4/6] Manually specify Python versions instead of using --find-interpreter That flag is returning Python 3.13t (free-threadead) which isn't yet supported by rust-numpy. --- .github/workflows/python.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index bf22015..2ab1054 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -45,7 +45,7 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.platform.target }} - args: --release --out dist --find-interpreter --manifest-path crates/pyaugurs/Cargo.toml + args: --release --out dist -i 3.9 -i 3.10 -i 3.11 -i 3.12 -i 3.13 -i pypy-3.9 -i pypy-3.10 --manifest-path crates/pyaugurs/Cargo.toml sccache: 'true' manylinux: auto - name: Upload wheels @@ -76,7 +76,7 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.platform.target }} - args: --release --out dist --find-interpreter --manifest-path crates/pyaugurs/Cargo.toml + args: --release --out dist -i 3.9 -i 3.10 -i 3.11 -i 3.12 -i 3.13 -i pypy-3.9 -i pypy-3.10 --manifest-path crates/pyaugurs/Cargo.toml sccache: 'true' manylinux: musllinux_1_2 - name: Upload wheels @@ -104,7 +104,7 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.platform.target }} - args: --release --out dist --find-interpreter --manifest-path crates/pyaugurs/Cargo.toml + args: --release --out dist -i 3.9 -i 3.10 -i 3.11 -i 3.12 -i 3.13 -i pypy-3.9 -i pypy-3.10 --manifest-path crates/pyaugurs/Cargo.toml sccache: 'true' - name: Upload wheels uses: actions/upload-artifact@v4 @@ -130,7 +130,7 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.platform.target }} - args: --release --out dist --find-interpreter --manifest-path crates/pyaugurs/Cargo.toml + args: --release --out dist -i 3.9 -i 3.10 -i 3.11 -i 3.12 -i 3.13 -i pypy-3.9 -i pypy-3.10 --manifest-path crates/pyaugurs/Cargo.toml sccache: 'true' - name: Upload wheels uses: actions/upload-artifact@v4 From f05bf04290b179fe5e4d80ddb63878fba4ea77a6 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Mon, 9 Dec 2024 15:22:40 +0000 Subject: [PATCH 5/6] Remove pypi interpreters from Windows builds --- .github/workflows/python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 2ab1054..c65e906 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -104,7 +104,7 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.platform.target }} - args: --release --out dist -i 3.9 -i 3.10 -i 3.11 -i 3.12 -i 3.13 -i pypy-3.9 -i pypy-3.10 --manifest-path crates/pyaugurs/Cargo.toml + args: --release --out dist -i 3.9 -i 3.10 -i 3.11 -i 3.12 -i 3.13 --manifest-path crates/pyaugurs/Cargo.toml sccache: 'true' - name: Upload wheels uses: actions/upload-artifact@v4 From b2ef042d87a52e5215715767afc5f2e301250406 Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Mon, 9 Dec 2024 15:30:01 +0000 Subject: [PATCH 6/6] Remove Python 3.13 interpreters from Windows builds It looks like Python 3.13 isn't available wherever maturin-action is running. --- .github/workflows/python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index c65e906..4926079 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -104,7 +104,7 @@ jobs: uses: PyO3/maturin-action@v1 with: target: ${{ matrix.platform.target }} - args: --release --out dist -i 3.9 -i 3.10 -i 3.11 -i 3.12 -i 3.13 --manifest-path crates/pyaugurs/Cargo.toml + args: --release --out dist -i 3.9 -i 3.10 -i 3.11 -i 3.12 --manifest-path crates/pyaugurs/Cargo.toml sccache: 'true' - name: Upload wheels uses: actions/upload-artifact@v4