diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1a0c39c..019a3a9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,8 +58,8 @@ jobs: strategy: fail-fast: false matrix: - # macos-13 is an intel runner, macos-latest is apple silicon - os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, macos-latest] + # macos-15-intel is an intel runner, macos-latest is apple silicon + os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, windows-11-arm, macos-15-intel, macos-latest] steps: - uses: actions/checkout@v4 @@ -67,7 +67,7 @@ jobs: fetch-depth: "0" - name: Build wheels - uses: pypa/cibuildwheel@v2.22.0 + uses: pypa/cibuildwheel@v3.3.0 with: output-dir: dist-wheel-${{ matrix.os }} @@ -82,19 +82,17 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-13, macos-14] + os: [ubuntu-latest, windows-latest, macos-15-intel, macos-latest] # https://github.com/scipy/oldest-supported-numpy/blob/main/setup.cfg ver: - - { py: "3.9", np: "==1.20.0" } - { py: "3.10", np: "==1.21.6" } - { py: "3.11", np: "==1.23.2" } - { py: "3.12", np: "==1.26.2" } - { py: "3.13", np: "==2.1.0" } - - { py: "3.13", np: ">=2.1.0" } + - { py: "3.14", np: "==2.3.2" } + - { py: "3.14", np: ">=2.3.2" } exclude: - - os: macos-14 - ver: { py: "3.9", np: "==1.20.0" } - - os: macos-14 + - os: macos-latest ver: { py: "3.10", np: "==1.21.6" } steps: - uses: actions/checkout@v4 @@ -155,6 +153,7 @@ jobs: - name: Install GSTools-Cython env: GSTOOLS_CY_COV: 1 + GSTOOLS_BUILD_PARALLEL: 1 run: | pip install -v --editable .[test] @@ -162,7 +161,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - pip install "numpy${{ matrix.ver.np }}" python -m pytest --cov gstools_cython --cov-report term-missing -v tests/ python -m coveralls --service=github diff --git a/CHANGELOG.md b/CHANGELOG.md index de323f9..c3f304d 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to **GSTools-Cython** will be documented in this file. +## [1.2.0] - 2025-12 + +See [#11](https://github.com/GeoStat-Framework/GSTools-Cython/pull/11) + +### Changes + +- add support for Python 3.14 (incl. free-threaded support) +- move pypy version to 3.11 +- add win arm64 wheels (without Python 3.10, since there are no numpy wheels prior to 3.11) +- remove support for Python 3.9 (EOL) +- fix bug in error message in variogram.pyx (undetected by cython<3.1) +- update pyproject.toml and use setuptools>=77 +- increased coverage + ## [1.1.0] - 2025-04 See [#5](https://github.com/GeoStat-Framework/GSTools-Cython/pull/5) @@ -23,6 +37,7 @@ First release of GSTools-Cython - moved Cython files into this separate package -[Unreleased]: https://github.com/GeoStat-Framework/gstools-cython/compare/v1.1.0...HEAD +[Unreleased]: https://github.com/GeoStat-Framework/gstools-cython/compare/v1.2.0...HEAD +[1.2.0]: https://github.com/GeoStat-Framework/gstools-cython/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/GeoStat-Framework/gstools-cython/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/GeoStat-Framework/gstools-cython/releases/tag/v1.0.0 diff --git a/docs/source/conf.py b/docs/source/conf.py index 50b3611..232fc63 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -203,11 +203,13 @@ def setup(app): # latex_show_urls = 'footnote' # http://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-latex-output latex_elements = { - "preamble": r""" + "preamble": ( + r""" \setcounter{secnumdepth}{1} \setcounter{tocdepth}{2} \pagestyle{fancy} -""", +""" + ), "pointsize": "10pt", "papersize": "a4paper", "fncychap": "\\usepackage[Glenn]{fncychap}", diff --git a/pyproject.toml b/pyproject.toml index 1650c9e..d4a2240 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,23 @@ [build-system] requires = [ - "setuptools>=64", - "setuptools<72.2; implementation_name == 'pypy'", # https://github.com/pypa/distutils/issues/283 + "setuptools>=77", "setuptools_scm>=7", - "numpy>=2.0.0rc1", - "Cython>=3.0.10,<3.1.0", + "numpy>=2", + "Cython>=3", "extension-helpers>=1", ] build-backend = "setuptools.build_meta" [project] -requires-python = ">=3.9" +requires-python = ">=3.10" name = "gstools_cython" description = "Cython backend for GSTools." authors = [ {name = "Sebastian Müller, Lennart Schüler", email = "info@geostat-framework.org"}, ] readme = "README.md" -license = {text = "LGPL-3.0"} +license = "LGPL-3.0-or-later" +license-files = ["LICENSE"] dynamic = ["version"] classifiers = [ "Development Status :: 5 - Production/Stable", @@ -25,7 +25,6 @@ classifiers = [ "Intended Audience :: End Users/Desktop", "Intended Audience :: Science/Research", "Intended Audience :: Education", - "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", "Natural Language :: English", "Operating System :: Unix", "Operating System :: Microsoft", @@ -33,11 +32,11 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: GIS", "Topic :: Scientific/Engineering :: Hydrology", @@ -58,7 +57,7 @@ doc = [ ] test = [ "pytest-cov>=3", - "Cython>=3.0.10,<3.1.0", + "Cython>=3", ] lint = [ "black>=24", @@ -75,9 +74,6 @@ Homepage = "https://geostat-framework.org/#gstools" Source = "https://github.com/GeoStat-Framework/GSTools-Cython" Tracker = "https://github.com/GeoStat-Framework/GSTools-Cython/issues" -[tool.setuptools] -license-files = ["LICENSE"] - [tool.setuptools_scm] write_to = "src/gstools_cython/_version.py" write_to_template = "__version__ = '{version}'" @@ -90,11 +86,11 @@ multi_line_output = 3 [tool.black] target-version = [ - "py39", "py310", "py311", "py312", "py313", + "py314", ] [tool.coverage] @@ -145,8 +141,8 @@ target-version = [ build-frontend = "build" # explicitly enable pypy enable = ["pypy"] -# Disable building py3.6/7/8, pp3.8, 32bit linux -skip = ["cp36-*", "cp37-*", "cp38-*", "pp38-*", "*_i686"] +# Disable building py3.8/9, 32bit linux, and arm64 windows wheels for python 3.10 (no numpy support) +skip = ["cp38-*", "cp39-*", "*_i686", "cp310-win_arm64"] # Run the package tests using `pytest` test-extras = "test" test-command = "pytest -v {package}/tests" diff --git a/src/gstools_cython/variogram.pyx b/src/gstools_cython/variogram.pyx index 195fdc2..1907c6a 100644 --- a/src/gstools_cython/variogram.pyx +++ b/src/gstools_cython/variogram.pyx @@ -253,7 +253,7 @@ def directional( counts of samples per bin and direciton """ if pos.shape[1] != f.shape[1]: - raise ValueError(f'len(pos) = {pos.shape[1]} != len(f) = {f.shape[1])}') + raise ValueError(f'len(pos) = {pos.shape[1]} != len(f) = {f.shape[1]}') if bin_edges.shape[0] < 2: raise ValueError('len(bin_edges) too small') @@ -359,7 +359,7 @@ def unstructured( raise ValueError(f'Haversine: dim = {dim} != 2') if pos.shape[1] != f.shape[1]: - raise ValueError(f'len(pos) = {pos.shape[1]} != len(f) = {f.shape[1])}') + raise ValueError(f'len(pos) = {pos.shape[1]} != len(f) = {f.shape[1]}') if bin_edges.shape[0] < 2: raise ValueError('len(bin_edges) too small') diff --git a/tests/test_field.py b/tests/test_field.py index bf18db1..7de0591 100644 --- a/tests/test_field.py +++ b/tests/test_field.py @@ -69,6 +69,8 @@ def test_summate(self): ) summed = gs_cy.field.summate(cov_samples, z_1, z_2, pos) np.testing.assert_allclose(summed_modes, summed) + summed_threads = gs_cy.field.summate(cov_samples, z_1, z_2, pos, num_threads=2) + np.testing.assert_allclose(summed_modes, summed_threads) def test_summate_incompr(self): # x = y = np.linspace(0,1,3) diff --git a/tests/test_krige.py b/tests/test_krige.py index e07fde3..b44c7a6 100644 --- a/tests/test_krige.py +++ b/tests/test_krige.py @@ -44,6 +44,11 @@ def test_calc_field_krige_and_variance(self): ) np.testing.assert_allclose(field, self.field_ref) np.testing.assert_allclose(error, self.error_ref) + field_threads, error_threads = gs_cy.krige.calc_field_krige_and_variance( + self.krig_mat, self.krig_vecs, self.cond, num_threads=2 + ) + np.testing.assert_allclose(field_threads, self.field_ref) + np.testing.assert_allclose(error_threads, self.error_ref) def test_calc_field_krige(self): field = gs_cy.krige.calc_field_krige(self.krig_mat, self.krig_vecs, self.cond) diff --git a/tests/test_variogram.py b/tests/test_variogram.py index c8b6631..32357b7 100644 --- a/tests/test_variogram.py +++ b/tests/test_variogram.py @@ -34,6 +34,58 @@ def test_directional(self): self.assertAlmostEqual(gamma[1, len(gamma[0]) // 2], var, places=2) self.assertAlmostEqual(gamma[1, -1], var, places=2) + def test_directional_separate_dirs(self): + pos = np.array(((0.0, 1.0), (0.0, 0.0)), dtype=np.double) + dirs = np.array(((1.0, 0.0), (1.0, 0.0)), dtype=np.double) + field = np.array(((1.0, 2.0),), dtype=np.double) + bins = np.array((0.0, 2.0), dtype=np.double) + + _, counts = gs_cy.variogram.directional( + field, bins, pos, dirs, angles_tol=np.pi, separate_dirs=True + ) + + self.assertEqual(counts[0, 0], 1) + self.assertEqual(counts[1, 0], 0) + + def test_directional_bandwidth_cressie(self): + pos = np.array(((0.0, 0.0, 1.0), (0.0, 1.0, 0.0)), dtype=np.double) + dirs = np.array(((1.0, 0.0),), dtype=np.double) + field = np.array(((1.0, 2.0, 5.0),), dtype=np.double) + bins = np.array((0.0, 2.0), dtype=np.double) + + gamma, counts = gs_cy.variogram.directional( + field, + bins, + pos, + dirs, + angles_tol=np.pi, + bandwidth=0.5, + estimator_type="c", + ) + + self.assertEqual(counts[0, 0], 1) + f_diff = field[0, 2] - field[0, 0] + raw = np.sqrt(abs(f_diff)) + expected = 0.5 * raw**4 / (0.457 + 0.494 + 0.045) + self.assertAlmostEqual(gamma[0, 0], expected, places=6) + + def test_directional_error_checks(self): + pos = np.array(((0.0, 1.0), (0.0, 1.0)), dtype=np.double) + dirs = np.array(((1.0, 0.0),), dtype=np.double) + bins = np.array((0.0, 1.0), dtype=np.double) + field = np.array(((1.0, 2.0),), dtype=np.double) + + with self.assertRaises(ValueError): + gs_cy.variogram.directional(field[:, :1], bins, pos, dirs) + + with self.assertRaises(ValueError): + gs_cy.variogram.directional( + field, np.array((0.0,), dtype=np.double), pos, dirs + ) + + with self.assertRaises(ValueError): + gs_cy.variogram.directional(field, bins, pos, dirs, angles_tol=0.0) + def test_unstructured(self): x = np.arange(1, 11, 1, dtype=np.double) z = np.array( @@ -73,6 +125,46 @@ def test_unstructured(self): self.assertAlmostEqual(gamma[len(gamma) // 2], var, places=2) self.assertAlmostEqual(gamma[-1], var, places=2) + def test_unstructured_haversine(self): + pos = np.array(((0.0, 0.0), (0.0, 90.0)), dtype=np.double) + field = np.array(((1.0, 3.0),), dtype=np.double) + bins = np.array((0.0, 2.0), dtype=np.double) + + gamma, counts = gs_cy.variogram.unstructured( + field, bins, pos, distance_type="h" + ) + + self.assertEqual(counts[0], 1) + self.assertAlmostEqual(gamma[0], 2.0, places=6) + + def test_unstructured_num_threads(self): + pos = np.array(((0.0, 1.0, 2.0),), dtype=np.double) + field = np.array(((1.0, 3.0, 2.0),), dtype=np.double) + bins = np.array((0.0, 2.0), dtype=np.double) + + gamma_default, counts_default = gs_cy.variogram.unstructured(field, bins, pos) + gamma_threads, counts_threads = gs_cy.variogram.unstructured( + field, bins, pos, num_threads=2 + ) + + np.testing.assert_allclose(gamma_threads, gamma_default) + np.testing.assert_array_equal(counts_threads, counts_default) + + def test_unstructured_error_checks(self): + pos = np.array(((0.0, 1.0), (0.0, 1.0)), dtype=np.double) + field = np.array(((1.0, 2.0),), dtype=np.double) + bins = np.array((0.0, 1.0), dtype=np.double) + + with self.assertRaises(ValueError): + gs_cy.variogram.unstructured(field[:, :1], bins, pos) + + with self.assertRaises(ValueError): + gs_cy.variogram.unstructured(field, np.array((0.0,), dtype=np.double), pos) + + pos_bad = np.array(((0.0, 1.0), (0.0, 1.0), (0.0, 1.0)), dtype=np.double) + with self.assertRaises(ValueError): + gs_cy.variogram.unstructured(field, bins, pos_bad, distance_type="h") + def test_structured(self): z = np.array( (41.2, 40.2, 39.7, 39.2, 40.1, 38.3, 39.1, 40.0, 41.1, 40.3),