From 21280d4ce3fbf744a6c95c5b97c72d55cc9f99aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Morales?= Date: Tue, 10 Oct 2023 16:17:35 -0600 Subject: [PATCH] initial packaging (#1) --- .github/workflows/ci.yaml | 41 ++++++++++++++++++++++++++++++++++ .gitignore | 3 +++ CMakeLists.txt | 24 +++++++++++++++++--- coreforecast/grouped_array.py | 35 ++++++++++++++++++++--------- dev_environment.yml | 13 +++++++++++ environment.yml | 6 ++--- include/coreforecast.h | 33 ++++++++++++++++++--------- pyproject.toml | 42 +++++++++++++++++++++++++++++++---- src/coreforecast.cpp | 8 +++---- 9 files changed, 169 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/ci.yaml create mode 100644 dev_environment.yml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..b8e185e --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +defaults: + run: + shell: bash -l {0} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + run-tests: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + python-version: ['3.10'] + steps: + - name: Clone repo + uses: actions/checkout@v3 + + - name: Set up environment + uses: mamba-org/setup-micromamba@v1 + with: + environment-file: environment.yml + create-args: python=${{ matrix.python-version }} + cache-environment: true + + - name: Install the library + run: pip install --no-build-isolation -v . + + - name: Run tests + run: pytest diff --git a/.gitignore b/.gitignore index 7ca2d29..4186e1e 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ # Python __pycache__ + +# CMake +build diff --git a/CMakeLists.txt b/CMakeLists.txt index c055137..d9850d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,10 +5,28 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() -set(CMAKE_CXX_FLAGS "-fPIC -Wall -Wextra -Wpedantic") +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_FLAGS_DEBUG "-g") -set(CMAKE_CXX_FLAGS_RELEASE "-O3") -set(CMAKE_CXX_STANDARD 17) +if(APPLE) + set(CMAKE_SHARED_LIBRARY_SUFFIX ".so") +endif() + +if(UNIX) + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -fPIC -O3 -Wall -Wextra -Wpedantic") +else() + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /O2 /Ot /Oy /W4") +endif() + + +if(SKBUILD) + set(LIBRARY_OUTPUT_PATH ${SKBUILD_PLATLIB_DIR}/coreforecast/lib) +else() + set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/coreforecast/lib) +endif() + include_directories(include) add_library(coreforecast SHARED src/coreforecast.cpp) +if(MSVC) + set_target_properties(coreforecast PROPERTIES OUTPUT_NAME "libcoreforecast") +endif() diff --git a/coreforecast/grouped_array.py b/coreforecast/grouped_array.py index 39b5cda..abf6f72 100644 --- a/coreforecast/grouped_array.py +++ b/coreforecast/grouped_array.py @@ -1,9 +1,24 @@ import ctypes +import platform import numpy as np +from importlib_resources import files -_LIB = ctypes.CDLL("build/libcoreforecast.so") +if platform.system() in ("Windows", "Microsoft"): + prefix = "Release" + extension = "dll" +else: + prefix = "" + extension = "so" + +_LIB = ctypes.CDLL( + str(files("coreforecast").joinpath("lib", prefix, f"libcoreforecast.{extension}")) +) + + +def _data_as_ptr(arr: np.ndarray, dtype): + return arr.ctypes.data_as(ctypes.POINTER(dtype)) class GroupedArray: @@ -12,9 +27,9 @@ def __init__(self, data: np.ndarray, indptr: np.ndarray): self.indptr = indptr self._handle = ctypes.c_void_p() _LIB.GroupedArray_CreateFromArrays( - data.ctypes.data_as(ctypes.POINTER(ctypes.c_float)), + _data_as_ptr(data, ctypes.c_float), ctypes.c_int32(data.size), - indptr.ctypes.data_as(ctypes.POINTER(ctypes.c_int32)), + _data_as_ptr(indptr, ctypes.c_int32), ctypes.c_int32(indptr.size), ctypes.byref(self._handle), ) @@ -25,12 +40,12 @@ def __del__(self): def __len__(self): return self.indptr.size - 1 - def scaler_fit(self, stats_fn_name: str) -> None: + def scaler_fit(self, stats_fn_name: str) -> np.ndarray: stats = np.empty((len(self), 2), dtype=np.float64) - stats_fn = getattr(_LIB, stats_fn_name) + stats_fn = _LIB[stats_fn_name] stats_fn( self._handle, - stats.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), + _data_as_ptr(stats, ctypes.c_double), ) return stats @@ -38,8 +53,8 @@ def scaler_transform(self, stats: np.ndarray) -> np.ndarray: out = np.full_like(self.data, np.nan) _LIB.GroupedArray_ScalerTransform( self._handle, - stats.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), - out.ctypes.data_as(ctypes.POINTER(ctypes.c_float)), + _data_as_ptr(stats, ctypes.c_double), + _data_as_ptr(out, ctypes.c_float), ) return out @@ -47,7 +62,7 @@ def scaler_inverse_transform(self, stats: np.ndarray) -> np.ndarray: out = np.empty_like(self.data) _LIB.GroupedArray_ScalerInverseTransform( self._handle, - stats.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), - out.ctypes.data_as(ctypes.POINTER(ctypes.c_float)), + _data_as_ptr(stats, ctypes.c_double), + _data_as_ptr(out, ctypes.c_float), ) return out diff --git a/dev_environment.yml b/dev_environment.yml new file mode 100644 index 0000000..a0e3858 --- /dev/null +++ b/dev_environment.yml @@ -0,0 +1,13 @@ +name: coreforecast +channels: + - conda-forge +dependencies: + - black + - build + - clang-format + - cmake + - mypy + - ninja + - numpy + - pytest + - scikit-build-core diff --git a/environment.yml b/environment.yml index 86ab015..d4c4f4b 100644 --- a/environment.yml +++ b/environment.yml @@ -2,10 +2,8 @@ name: coreforecast channels: - conda-forge dependencies: - - black - - clang-format - cmake - - make + - ninja - numpy - - pip - pytest + - scikit-build-core diff --git a/include/coreforecast.h b/include/coreforecast.h index a40dedb..dae2a3c 100644 --- a/include/coreforecast.h +++ b/include/coreforecast.h @@ -2,25 +2,36 @@ #include +#ifdef _MSC_VER +#define DLL_EXPORT __declspec(dllexport) +#else +#define DLL_EXPORT +#endif + typedef void *GroupedArrayHandle; extern "C" { -int GroupedArray_CreateFromArrays(float *data, int32_t n_data, int32_t *indptr, - int32_t n_groups, GroupedArrayHandle *out); +DLL_EXPORT int GroupedArray_CreateFromArrays(float *data, int32_t n_data, + int32_t *indptr, int32_t n_groups, + GroupedArrayHandle *out); -int GroupedArray_Delete(GroupedArrayHandle handle); +DLL_EXPORT int GroupedArray_Delete(GroupedArrayHandle handle); -int GroupedArray_MinMaxScalerStats(GroupedArrayHandle handle, double *out); +DLL_EXPORT int GroupedArray_MinMaxScalerStats(GroupedArrayHandle handle, + double *out); -int GroupedArray_StandardScalerStats(GroupedArrayHandle handle, double *out); +DLL_EXPORT int GroupedArray_StandardScalerStats(GroupedArrayHandle handle, + double *out); -int GroupedArray_RobustScalerIqrStats(GroupedArrayHandle handle, double *out); +DLL_EXPORT int GroupedArray_RobustScalerIqrStats(GroupedArrayHandle handle, + double *out); -int GroupedArray_RobustScalerMadStats(GroupedArrayHandle handle, double *out); +DLL_EXPORT int GroupedArray_RobustScalerMadStats(GroupedArrayHandle handle, + double *out); -int GroupedArray_ScalerTransform(GroupedArrayHandle handle, double *stats, - float *out); +DLL_EXPORT int GroupedArray_ScalerTransform(GroupedArrayHandle handle, + double *stats, float *out); -int GroupedArray_ScalerInverseTransform(GroupedArrayHandle handle, - double *stats, float *out); +DLL_EXPORT int GroupedArray_ScalerInverseTransform(GroupedArrayHandle handle, + double *stats, float *out); } diff --git a/pyproject.toml b/pyproject.toml index bbb1e20..579f02b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,44 @@ -[build-system] -requires = ["scikit-build-core"] -build-backend = "scikit_build_core.build" - [project] name = "coreforecast" version = "0.0.1" +requires-python = ">=3.7" dependencies = [ + "importlib_resources ; python_version < '3.10'", "numpy", ] +license = {file = "LICENSE"} +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Natural Language :: English", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +description = "Fast implementations of common forecasting routines" +authors = [ + {name = "José Morales", email = "jmoralz92@gmail.com"}, +] +readme = "README.md" +keywords = ["forecasting", "time-series"] + +[project.urls] +homepage = "https://nixtla.github.io/coreforecast" +documentation = "https://nixtla.github.io/coreforecast" +repository = "https://github.com/Nixtla/coreforecast" + +[build-system] +requires = ["scikit-build-core"] +build-backend = "scikit_build_core.build" + +[tool.scikit-build] +cmake.verbose = true +logging.level = "INFO" +sdist.exclude = ["tests", "*.yml"] +sdist.reproducible = true +wheel.install-dir = ["coreforecast"] +wheel.packages = ["coreforecast"] +wheel.py-api = "cp37" diff --git a/src/coreforecast.cpp b/src/coreforecast.cpp index c531d55..e6b8114 100644 --- a/src/coreforecast.cpp +++ b/src/coreforecast.cpp @@ -1,16 +1,16 @@ #include -#include +#include #include #include "coreforecast.h" inline float CommonScalerTransform(float data, double scale, double offset) { - return (data - offset) / scale; + return static_cast((data - offset) / scale); } inline float CommonScalerInverseTransform(float data, double scale, double offset) { - return data * scale + offset; + return static_cast(data * scale + offset); } inline int FirstNotNaN(const float *data, int n) { @@ -68,7 +68,7 @@ inline void RobustScalerMadStats(const float *data, int n, double *stats) { float *buffer = new float[n]; std::copy(data, data + n, buffer); std::sort(buffer, buffer + n); - const float median = Quantile(buffer, 0.5F, n); + const float median = static_cast(Quantile(buffer, 0.5F, n)); for (int i = 0; i < n; ++i) { buffer[i] = std::abs(buffer[i] - median); }