diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 69447a90..a1d6de32 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -2,38 +2,63 @@ name: Run tests on: push: + pull_request: release: workflow_dispatch: +# Cancel superseded runs on the same ref so stale jobs don't pile up. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test_ubuntu: + name: py${{ matrix.python-version }} / ${{ matrix.numpy-spec }} runs-on: ubuntu-latest strategy: + fail-fast: false + # This workflow's unique job is to prove a NumPy-2-built package runs on + # NumPy 1.x (the gh-114 ABI path) from a source install. Per-OS / per-Python + # coverage with NumPy 2.x is already provided by the wheel build+test in + # pypi.yml, so we only need a small representative matrix here: + # - NumPy 1.x runtime on the oldest and a recent Python (the ABI path) + # - one NumPy 2.x source-build smoke test on the newest Python matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] - + include: + - python-version: "3.9" + numpy-spec: "numpy>=1.19,<2" + - python-version: "3.12" + numpy-spec: "numpy>=1.19,<2" + - python-version: "3.13" + numpy-spec: "numpy>=2" + steps: - - uses: actions/checkout@v2 - + - uses: actions/checkout@v4 + - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - pip install numpy - pip install scipy - pip install Cython - - - name: Install - run: | - pip install . - + - name: Set up build environment (Fortran + ninja) + uses: ./.github/workflows/setup-build-env + + - name: Build and install stripy + # Built with PEP 517 isolation -> compiled against NumPy 2.x + run: pip install . + + - name: Pin runtime NumPy and install test deps + # Resolve together so the NumPy pin is honoured (a separate scipy/pytest + # resolve could otherwise pull NumPy back up). + run: pip install "${{ matrix.numpy-spec }}" pytest scipy + + - name: Show resolved versions + run: pip list | grep -iE "numpy|stripy" || true + - name: Run tests + # Run from a neutral directory so the *installed* package (with its + # compiled extensions) is imported, not the ./stripy source-tree shadow. run: | - pip install pytest - pytest -v + cd "${{ runner.temp }}" + python -m pytest -v "${{ github.workspace }}/stripy/tests" diff --git a/.github/workflows/build_deploy_jbdoc.yml b/.github/workflows/build_deploy_jbdoc.yml index 7b1615c5..3b5724e2 100644 --- a/.github/workflows/build_deploy_jbdoc.yml +++ b/.github/workflows/build_deploy_jbdoc.yml @@ -1,51 +1,48 @@ name: API docs / jupyterbook -# We should trigger this from an upload event +# We should trigger this from an upload event on: push: # We publish the beta docs as well - branches: - - master + branches: + - master - dev jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" + + - name: Set up build environment (Fortran + ninja) + uses: ./.github/workflows/setup-build-env - - name: Install dependencies + - name: Install stripy and doc dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine - pip install numpy - pip install scipy - pip install Cython - pip install jupyter-book - - - name: Install latest stripy - run: | + pip install scipy jupyter-book pip install . - name: Build docs with jupyterbook shell: bash -l {0} run: | - VERSION=`python setup.py --version` + VERSION=`python -c "from importlib.metadata import version; print(version('stripy'))"` echo "VERSION=$(echo $VERSION)" >> $GITHUB_ENV cd jupyterbook - ./build-book.sh + ./build-book.sh # Note deploy key needs the ----BEGIN ... KEY---- lines too ## Always deploy to version directory - name: Deploy uses: peaceiris/actions-gh-pages@v3 - + with: github_token: ${{ secrets.GITHUB_TOKEN }} destination_dir: ${{ env.VERSION }} publish_dir: jupyterbook/_build/html - - diff --git a/.github/workflows/build_deploy_pdoc.yml b/.github/workflows/build_deploy_pdoc.yml index b958b061..809c8554 100644 --- a/.github/workflows/build_deploy_pdoc.yml +++ b/.github/workflows/build_deploy_pdoc.yml @@ -1,9 +1,9 @@ -name: API docs / pdoc3 +name: API docs / pdoc3 -# We should trigger this from an upload event +# We should trigger this from an upload event on: - push: # This is not really a great idea + push: # This is not really a great idea branches: - master @@ -11,22 +11,20 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 with: python-version: "3.11" - - name: Install dependencies + - name: Set up build environment (Fortran + ninja) + uses: ./.github/workflows/setup-build-env + + - name: Install stripy and doc dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine - pip install numpy - pip install scipy - pip install Cython - pip install pdoc3 - pip install pytest - - - name: Install latest stripy - run: | + pip install scipy pdoc3 pip install . - name: Build docs with pdoc @@ -38,15 +36,15 @@ jobs: cd docs pdoc --config latex_math=True --html -o api --force stripy - # Here we can add an action to build the jupyter book to a different directory + # Here we can add an action to build the jupyter book to a different directory # and then they can be uploaded together. The jupyter book can contain a link to the # API docs. # Note deploy key needs the ----BEGIN ... KEY---- lines too - name: Deploy uses: peaceiris/actions-gh-pages@v3 - + with: github_token: ${{ secrets.GITHUB_TOKEN }} destination_dir: docs/ - publish_dir: docs/api/stripy \ No newline at end of file + publish_dir: docs/api/stripy diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index aabe4af4..f539e11f 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -1,34 +1,77 @@ name: Build wheels -on: [push,pull_request,release,workflow_dispatch] +on: + push: + branches: [master] # feature branches are covered by the pull_request event + pull_request: + release: + workflow_dispatch: + +# Cancel superseded runs on the same ref so stale wheel builds don't pile up in +# the (slow) macOS runner queue. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: build_wheels: name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: + # Don't let one OS failing cancel (and mask) the others. + fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-13, macos-14] + include: + - os: ubuntu-latest + - os: windows-latest + # Apple-silicon (arm64) only; Intel-mac (x86_64) users are served by conda. + - os: macos-14 + macos_target: "14.0" steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: 3.x - name: Build wheels - uses: pypa/cibuildwheel@v2.20.0 + uses: pypa/cibuildwheel@v2.21.3 env: # Disable building PyPy wheels on all platforms CIBW_SKIP: 'pp*' - CIBW_ENVIRONMENT_WINDOWS: "CFLAGS='-lquadmath'" + # PIP_ONLY_BINARY=scipy: install the test-time scipy from a wheel only. + # Otherwise pip may pick a scipy version with no wheel for the target + # interpreter and try to build it from source, which fails in the + # manylinux container (no OpenBLAS/build tooling). Forcing wheels makes + # pip fall back to the newest scipy that ships one. + CIBW_ENVIRONMENT_LINUX: "PIP_ONLY_BINARY=scipy" + CIBW_ENVIRONMENT_WINDOWS: "CFLAGS='-lquadmath' PIP_ONLY_BINARY=scipy" + # Homebrew's gfortran on the macos-14 runner targets the runner's + # macOS version, so the libgfortran/libquadmath that delocate bundles + # require that minimum. Match MACOSX_DEPLOYMENT_TARGET to the runner or + # delocate rejects the wheel ("Library dependencies do not satisfy + # target MacOS version"). (This does raise the wheel's minimum macOS; + # a portable gfortran targeting an older macOS could lower it later.) + CIBW_ENVIRONMENT_MACOS: "MACOSX_DEPLOYMENT_TARGET=${{ matrix.macos_target }} PIP_ONLY_BINARY=scipy" CIBW_PROJECT_REQUIRES_PYTHON: ">=3.9" CIBW_ARCHS: 'auto64' + # Use the modern manylinux/musllinux images (GCC 12). The default + # manylinux2014 image ships GCC 10.2, but recent NumPy only publishes + # manylinux_2_28 wheels and its source build requires GCC >= 10.3, so + # the build-isolation NumPy install would fail there. manylinux_2_28 + # has wheels available and a new enough compiler. + CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 + CIBW_MUSLLINUX_X86_64_IMAGE: musllinux_1_2 CIBW_BEFORE_BUILD_WINDOWS: "choco install ninja" CIBW_BEFORE_BUILD_MACOS: "brew install ninja gcc && brew reinstall gcc" # CIBW_BEFORE_BUILD_LINUX: "apt-get install -y ninja-build" - CIBW_BEFORE_BUILD: "pip install numpy meson-python ninja setuptools build" - CIBW_TEST_REQUIRES: pytest + # NumPy is NOT installed here on purpose: cibuildwheel builds with PEP 517 + # isolation, so the compile-time NumPy comes from `build-system.requires` + # in pyproject.toml (numpy>=2.0), giving wheels that run on NumPy 1.x and 2.x. + # A system ninja just lets meson find one without a per-build pip install. + CIBW_BEFORE_BUILD: "pip install ninja" + # scipy is needed by the mesh tests (stripy uses scipy.optimize/spatial lazily) + CIBW_TEST_REQUIRES: pytest scipy CIBW_TEST_COMMAND: "pytest {project}/stripy/tests" # ... # with: @@ -38,6 +81,9 @@ jobs: - uses: actions/upload-artifact@v4 with: + # Unique name per matrix job: upload-artifact@v4 rejects duplicate + # artifact names within a run, so a shared default name would fail the build. + name: cibw-wheels-${{ matrix.os }} path: ./wheelhouse/*.whl build_sdist: @@ -51,6 +97,7 @@ jobs: - uses: actions/upload-artifact@v4 with: + name: cibw-sdist path: dist/*.tar.gz upload_pypi: @@ -63,11 +110,11 @@ jobs: # or, alternatively, upload to PyPI on every tag starting with 'v' (remove on: release above to use this) # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') steps: - - uses: actions/download-artifact@v4.1.7 + - uses: actions/download-artifact@v4 with: - # unpacks default artifact into dist/ - # if `name: artifact` is omitted, the action will create extra parent dir - name: artifact + # Gather every wheel/sdist artifact from the build jobs into dist/ + pattern: cibw-* + merge-multiple: true path: dist - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/resources/conda_api_docs_environment.yml b/.github/workflows/resources/conda_api_docs_environment.yml deleted file mode 100644 index cf763ad7..00000000 --- a/.github/workflows/resources/conda_api_docs_environment.yml +++ /dev/null @@ -1,11 +0,0 @@ -# This is the environment needed to build stripy. As this will be cached, we can include any stripy dependencies -# so they are not downloaded again - this should save a bit of time - -name: conda-build-docs -channels: - - conda-forge - - geo-down-under -dependencies: - - numpy>=1.16.0 - - scipy>=1.0.0 - - pdoc3 diff --git a/.github/workflows/resources/conda_build_environment.yml b/.github/workflows/resources/conda_build_environment.yml deleted file mode 100644 index df33c121..00000000 --- a/.github/workflows/resources/conda_build_environment.yml +++ /dev/null @@ -1,13 +0,0 @@ -# This is the environment needed to build stripy. Note, the build is in a new -# environment so all we need here is the tools to run the build in the first place -# not all the stripy dependencies. - -name: conda-build -channels: - - conda-forge -dependencies: - # numpy is needed for some of the setup tools - - numpy - - conda-build - - conda-verify - - anaconda-client diff --git a/.github/workflows/resources/conda_jb_docs_environment.yml b/.github/workflows/resources/conda_jb_docs_environment.yml deleted file mode 100644 index 94429c7c..00000000 --- a/.github/workflows/resources/conda_jb_docs_environment.yml +++ /dev/null @@ -1,22 +0,0 @@ -# This is the environment needed to run the stripy examples / build the jbook - -name: conda-build-docs -channels: - - conda-forge - - geo-down-under -dependencies: - - geo-down-under::stripy==2.0.5b2 # this only works after the upload, of course - - geo-down-under::litho1pt0 - - matplotlib - - pyproj - - pyepsg - - h5netcdf - - netcdf4 - - cartopy - - xarray - - k3d - - pip - - - pip: - - jupyter-book - diff --git a/.github/workflows/resources/conda_test_environment.yml b/.github/workflows/resources/conda_test_environment.yml deleted file mode 100644 index 99e742c3..00000000 --- a/.github/workflows/resources/conda_test_environment.yml +++ /dev/null @@ -1,14 +0,0 @@ -# This is the environment needed to build stripy for pip - -name: conda-test -channels: - - conda-forge -dependencies: - # numpy is needed for some of the setup tools - - compilers - - numpy - - scipy - - cython - - pip - - diff --git a/.github/workflows/setup-build-env/action.yml b/.github/workflows/setup-build-env/action.yml index 6f1f4b4a..a8b4565a 100644 --- a/.github/workflows/setup-build-env/action.yml +++ b/.github/workflows/setup-build-env/action.yml @@ -6,7 +6,7 @@ description: "Install ninja and a fortran compiler" runs: using: "composite" steps: - - uses: awvwgk/setup-fortran@v1 + - uses: fortran-lang/setup-fortran@v1 - run: pip install --upgrade pip shell: bash diff --git a/.gitignore b/.gitignore index 5429b725..784a462c 100755 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ __pycache__ .pytest_cache *.png +# Local notebook run outputs / GPlates data caches (not part of the package) +stripy/Notebooks/spherical_grid.* +*.gplates.cache + .coverage swpcom.mod @@ -24,8 +28,11 @@ litho1pt0-src/dist/* *.egg .eggs -src/_*module.c -src/_*wrappers.f +# f2py-generated extension sources (the historical in-root location and src/) +/_*module.c +/_*-f2pywrappers.f +src/*module.c +src/*wrappers.f .local .ipython diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e47bcc87..3900c7c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ It is a good idea to commit frequently and with commit messages that refer to th We use `pytest` for the unit testing framework in stripy. In the source directory this means running: ```bash -python setup.py test +pytest ``` The existing tests should be passing before you start coding (help us out with an issue if that is not the case !) and when you have finished. Any new functionality should also have tests that we can use to verify the code. It is important that you make it clear if the original tests have had to change to accomodate new code / functionality. diff --git a/Changelog.md b/Changelog.md index 1a5540d9..07338af5 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,12 @@ # Stripy changelog +## 2.3.4 + + - Build against NumPy 2.x so wheels run on both NumPy 1.x and 2.x at runtime ([#114](https://github.com/underworldcode/stripy/issues/114)). + - Replace NumPy aliases removed in NumPy 1.24 (`np.int`, `np.bool`) for NumPy 2.x compatibility. + - Fix the release wheel workflow for `actions/upload-artifact@v4` (unique per-job artifact names). + - Meson build robustness fixes and CI modernisation; remove the obsolete `numpy.distutils` `setup.py` build path. + ## 2.1.0 - Jupyterbook diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100755 index 039e22f1..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,15 +0,0 @@ -include README.md -include MANIFEST.in -include LICENCE -include CONTRIBUTING.md -include src/* -include stripy/__init__.py -include stripy/cartesian.py -include stripy/spherical.py -include stripy/spherical_meshes.py -include stripy/cartesian_meshes.py -include stripy/documentation.py -include stripy/Notebooks/SphericalTriangulations/*.ipynb -include stripy/Notebooks/CartesianTriangulations/*.ipynb -include stripy/Notebooks/Data/* -include stripy/Notebooks/0-StartHere.ipynb diff --git a/README.md b/README.md index a1367bfe..e3bbaa4b 100755 --- a/README.md +++ b/README.md @@ -3,9 +3,8 @@ [![Docker Cloud Automated build](https://img.shields.io/docker/cloud/automated/underworldcode/stripy.svg)](https://hub.docker.com/r/underworldcode/stripy) [![PyPI](https://img.shields.io/pypi/v/stripy.svg)](https://pypi.org/project/stripy/) -[![pip builds](https://github.com/underworldcode/stripy/workflows/pip%20builds/badge.svg)](https://github.com/underworldcode/stripy/actions?query=workflow%3A%22pip+builds%22) - -![Conda Deployment](https://github.com/underworldcode/stripy/workflows/Conda%20Deployment/badge.svg) +[![Build wheels](https://github.com/underworldcode/stripy/actions/workflows/pypi.yml/badge.svg)](https://github.com/underworldcode/stripy/actions/workflows/pypi.yml) +[![Run tests](https://github.com/underworldcode/stripy/actions/workflows/CI.yml/badge.svg)](https://github.com/underworldcode/stripy/actions/workflows/CI.yml) A Python interface to TRIPACK and STRIPACK Fortran code for (constrained) triangulation in Cartesian coordinates and on a sphere. Stripy is an object-oriented package and includes routines from SRFPACK and SSRFPACK for interpolation (nearest neighbor, linear and hermite cubic) and to evaluate derivatives (Renka 1996a,b and 1997a,b). @@ -145,16 +144,16 @@ All the dependencies will be automatically installed by `pip`, except for `gfort (or any Fortran compiler). It must be installed in your system before installing `stripy` with `pip`. -If you change the Fortran compiler, you may have to add the -flags `config_fc --fcompiler=` when `setup.py` is run -(see docs for [numpy.distutils](http://docs.scipy.org/doc/numpy-dev/f2py/distutils.html)). +The Fortran extensions are built with [meson-python](https://meson-python.readthedocs.io). +If you need to select a specific Fortran compiler, set the standard `FC` environment +variable (e.g. `FC=gfortran-13`) before invoking `pip install`. ### Installing with conda If you use the anaconda packaging system, then you should be able to ```bash -conda install -c geo-down-under stripy +conda install -c conda-forge stripy ``` ## Usage diff --git a/conda/conda_build_config.yaml b/conda/conda_build_config.yaml deleted file mode 100644 index b0fe25b7..00000000 --- a/conda/conda_build_config.yaml +++ /dev/null @@ -1,4 +0,0 @@ -fortran_compiler: # [unix or win64] - - gfortran # [(linux64 or osx)] - - gfortran # [aarch64 or ppc64le or armv7l] - - flang # [win64] diff --git a/conda/meta.yaml b/conda/meta.yaml deleted file mode 100644 index bb35cf0e..00000000 --- a/conda/meta.yaml +++ /dev/null @@ -1,55 +0,0 @@ -{% set name = "stripy" %} -{% set data = load_setup_py_data() %} - -package: - name: "{{ name|lower }}" - version: {{ data.get('version') }} - -source: - path: .. - -build: - number: 0 - script: "{{ PYTHON }} -m pip install . -vv" - -requirements: - - host: - - python - - pip - - numpy >=1.16.0 - - scipy >=1.0.0 - - {{ compiler('fortran')}} - - {{ compiler('c')}} - - run: - - python - - numpy >=1.16.0 - - scipy >=1.0.0 - -test: - requires: - - pytest - - source_files: - - stripy/tests - - commands: - - pytest - - - -about: - home: "https://github.com/underworldcode/stripy" - license: LGPL - license_family: GPL - license_file: ../COPYING.LESSER - summary: "Python interface to TRIPACK and STRIPACK fortran code for triangulation/interpolation in Cartesian coordinates and on a sphere" - doc_url: - dev_url: - -extra: - recipe-maintainers: - - lmoresi - - brmather - - rbeucher diff --git a/meson.build b/meson.build index bad6db72..4a5a32e6 100644 --- a/meson.build +++ b/meson.build @@ -5,12 +5,27 @@ name = 'stripy' add_languages('fortran') platform = host_machine.system() -if platform == 'windows' - add_project_link_arguments('-static', language: ['fortran', 'c']) -elif platform == 'darwin' - add_project_link_arguments('-Wl,-rpath, "@loader_path"', language: ['fortran', 'c']) -else - add_project_link_arguments('-Wl,-rpath,"$ORIGIN"', language: ['fortran', 'c']) + +# These link arguments make each extension module self-contained, but they are +# GNU/gfortran conventions. Other Fortran toolchains (LLVM flang, Intel, the +# MSVC-based linkers) warn about, reject, or silently ignore them and provide +# their runtime libraries differently, so only emit them for a GNU (gfortran/GCC) +# compiler (see gh-116). Non-GNU builds rely on the packaging step +# (delocate/auditwheel, or conda compiler activation) to resolve the runtime. +# +# This stays correct under cross-compilation: both host_machine.system() and +# meson.get_compiler('fortran') refer to the *host* (target) machine, so a cross +# build with a gfortran cross-compiler (e.g. conda-forge linux-aarch64 or +# osx-arm64) still selects the right per-target flags. +if meson.get_compiler('fortran').get_id() == 'gcc' + if platform == 'windows' + # Statically link libgfortran/libquadmath (MinGW) so the .pyd is standalone. + add_project_link_arguments('-static', language: ['fortran', 'c']) + elif platform == 'darwin' + add_project_link_arguments('-Wl,-rpath,@loader_path', language: ['fortran', 'c']) + else + add_project_link_arguments('-Wl,-rpath,$ORIGIN', language: ['fortran', 'c']) + endif endif py_mod = import('python') @@ -18,45 +33,60 @@ py = py_mod.find_installation(pure: false) py_dep = py.dependency() incdir_numpy = run_command(py, - ['-c', 'import os; os.chdir(".."); import numpy; print(numpy.get_include())'], + ['-c', 'import numpy; print(numpy.get_include())'], check : true ).stdout().strip() incdir_f2py = run_command(py, - ['-c', 'import os; os.chdir(".."); import numpy.f2py; print(numpy.f2py.get_include())'], - check : true + ['-c', 'import numpy.f2py; print(numpy.f2py.get_include())'], + check : true ).stdout().strip() inc_np = include_directories(incdir_numpy, incdir_f2py) -install_subdir(name, install_dir: py.get_install_dir() / name, strip_directory: true) +# Install the pure-Python package, including the shipped example Notebooks that +# stripy.documentation relies on. The excludes drop common editor/interpreter +# cruft; note meson matches exact relative paths (no globbing), so these only +# catch top-level occurrences. Clean release wheels are built by CI from a fresh +# checkout, so this is just a best-effort safety net for local (dirty) installs. +install_subdir(name, + install_dir: py.get_install_dir() / name, + strip_directory: true, + exclude_files: ['.DS_Store'], + exclude_directories: ['__pycache__', '.ipynb_checkpoints'], +) +# f2py turns each .pyf interface into a C extension source (module.c) and, +# when the interface requires it, a Fortran wrapper (-f2pywrappers.f). The +# `output` lists below must match exactly what the pinned f2py (numpy>=2.0) +# emits; if a future f2py changes its output set, update these lists to match or +# the build will fail with a missing/unexpected output error. tripack_source = custom_target('_tripackmodule.c', - input : ['src/tripack.pyf'], # .f so no F90 wrappers + input : ['src/tripack.pyf'], output : ['_tripackmodule.c', '_tripack-f2pywrappers.f'], command : [py, '-m', 'numpy.f2py', '@INPUT@'] ) stripack_source = custom_target('_stripackmodule.c', - input : ['src/stripack.pyf'], # .f so no F90 wrappers + input : ['src/stripack.pyf'], output : ['_stripackmodule.c', '_stripack-f2pywrappers.f'], command : [py, '-m', 'numpy.f2py', '@INPUT@'] ) srfpack_source = custom_target('_srfpackmodule.c', - input : ['src/srfpack.pyf'], # .f so no F90 wrappers + input : ['src/srfpack.pyf'], output : ['_srfpackmodule.c'], command : [py, '-m', 'numpy.f2py', '@INPUT@'] ) ssrfpack_source = custom_target('_ssrfpackmodule.c', - input : ['src/ssrfpack.pyf'], # .f so no F90 wrappers + input : ['src/ssrfpack.pyf'], output : ['_ssrfpackmodule.c', '_ssrfpack-f2pywrappers.f'], command : [py, '-m', 'numpy.f2py', '@INPUT@'] ) fortran_source = custom_target('_fortranmodule.c', - input : ['src/stripyf.pyf'], # .f so no F90 wrappers + input : ['src/stripyf.pyf'], output : ['_fortranmodule.c', '_fortran-f2pywrappers.f'], command : [py, '-m', 'numpy.f2py', '@INPUT@'] ) diff --git a/pyproject.toml b/pyproject.toml index 38ecafff..7a423719 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [build-system] build-backend = "mesonpy" requires = [ - "numpy>=2.0", - "pip", "meson-python>=0.15.0", - "setuptools>=61.0", - "build", + # Build against NumPy 2.x: the resulting binaries are compatible with + # both NumPy 1.x and 2.x at runtime. Building against 1.x is NOT + # forward-compatible with 2.x, which is the cause of gh-114. + "numpy>=2.0.0", ] [project] @@ -29,24 +29,16 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] keywords = ["triangulation", "sphere", "interpolation", "mesh"] dependencies = [ - "numpy>=2.0.0", + # Runtime requirement is intentionally looser than the build requirement: + # wheels built against NumPy 2.x remain ABI-compatible with older NumPy + # at runtime, back to the 2.0 backward-compat floor of 1.19.3 (see gh-114). + "numpy>=1.19.3", ] -[tool.setuptools] -packages = [ - "stripy", - "stripy._tripack", - "stripy._stripack", - "stripy._srfpack", - "stripy._ssrfpack", -] - -# [tool.setuptools.dynamic] -# version = {attr = "stripy.__version__"} - [project.optional-dependencies] dev = ["pip-tools", "pytest", "scipy>=1.0.0"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3712537d..00000000 --- a/setup.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[aliases] -test=pytest - -[bdist_wheel] - -# This flag says that the code is written to work on both Python 2 and Python -# 3. If at all possible, it is good practice to do this. If you cannot, you -# will need to generate wheels for each Python version that you support. - -[metadata] -description-file = README.md diff --git a/setup.py b/setup.py deleted file mode 100755 index 8330d62f..00000000 --- a/setup.py +++ /dev/null @@ -1,129 +0,0 @@ -# To install locally: python setup.py build && python setup.py install -# (If there are problems with installation of the documentation, the -# egg file may be out of sync and will need to be manually deleted -# - see error message for details of the corrupted zip file. ) -# -# To push a version through to pip. -# - Make sure it installs correctly locally as above -# - Update the version information in this file -# - python setup.py sdist upload -r pypitest # for the test version -# - python setup.py sdist upload -r pypi # for the real version -# With twine: -# - python setup.py sdist -# - twine upload dist/* -# -# (see http://peterdowns.com/posts/first-time-with-pypi.html) - -from setuptools import setup -from numpy.distutils.core import setup, Extension - -# try: -# from distutils.command import bdist_conda -# except ImportError: -# pass - -from os import path -import io -import os -import subprocess -import platform - -link_args = ['-Wl,-rpath,"$ORIGIN"'] - -if "Windows" in platform.system(): - link_args = ["-static"] - -if "Darwin" in platform.system(): - link_args = ['-Wl,-rpath, "@loader_path"'] - - -# in development set version to none and ... -PYPI_VERSION = "2.3.4" # Note: don't add any dashes if you want to use conda, use b1 not .b1 - -# Return the git revision as a string (from numpy) - -def git_version(): - - def _minimal_ext_cmd(cmd): - # construct minimal environment - env = {} - for k in ['SYSTEMROOT', 'PATH']: - v = os.environ.get(k) - if v is not None: - env[k] = v - # LANGUAGE is used on win32 - env['LANGUAGE'] = 'C' - env['LANG'] = 'C' - env['LC_ALL'] = 'C' - out = subprocess.Popen(cmd, stdout = subprocess.PIPE, env=env).communicate()[0] - return out - - try: - out = _minimal_ext_cmd(['git', 'rev-parse', '--short', 'HEAD']) - GIT_REVISION = out.strip().decode('ascii') - except OSError: - GIT_REVISION = "Unknown" - - return GIT_REVISION - - -if PYPI_VERSION is None: - PYPI_VERSION = git_version() - - -this_directory = path.abspath(path.dirname(__file__)) -with io.open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: - long_description = f.read() - -# What works on github does not work elsewhere -long_description = long_description.replace('(stripy/Notebooks','(https://github.com/underworldcode/stripy/blob/master/stripy/Notebooks') - -# print(long_description) - -# interface for Renka's algorithm 772 fortran code -ext1 = Extension(name = 'stripy._stripack', - sources = ['src/stripack.pyf','src/stripack.f90'], - extra_link_args=link_args) -ext2 = Extension(name = 'stripy._tripack', - sources = ['src/tripack.pyf', 'src/tripack.f90'], - extra_link_args=link_args) -ext3 = Extension(name = 'stripy._srfpack', - sources = ['src/srfpack.pyf', 'src/srfpack.f'], - extra_link_args=link_args) -ext4 = Extension(name = 'stripy._ssrfpack', - sources = ['src/ssrfpack.pyf', 'src/ssrfpack.f'], - extra_link_args=link_args) -ext5 = Extension(name = 'stripy._fortran', - sources = ['src/stripyf.pyf', 'src/stripyf.f90'], - extra_link_args=link_args) - - -if __name__ == "__main__": - setup(name = 'stripy', - author = "Louis Moresi", - author_email = "louis.moresi@anu.edu.au", - url = "https://github.com/underworldcode/stripy", - version = PYPI_VERSION, - description = "Python interface to TRIPACK and STRIPACK fortran code for triangulation/interpolation in Cartesian coordinates and on a sphere", - long_description = long_description, - long_description_content_type='text/markdown', - ext_modules = [ext1, ext2, ext3, ext4, ext5], - install_requires = ['numpy>=1.16.0'], - packages = ['stripy'], - package_data = {'stripy': ['Notebooks/*ipynb', # Worked Examples is not currently used - 'Notebooks/CartesianTriangulations/*ipynb', - 'Notebooks/SphericalTriangulations/*ipynb', - 'Notebooks/Data/*'] }, - include_package_data = True, - classifiers = ['Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - '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' - ] - ) diff --git a/stripy/Notebooks/0-StartHere.ipynb b/stripy/Notebooks/0-StartHere.ipynb index 8be50f23..8135cbeb 100644 --- a/stripy/Notebooks/0-StartHere.ipynb +++ b/stripy/Notebooks/0-StartHere.ipynb @@ -74,25 +74,25 @@ "\n", "## Installation\n", "\n", - "To install ([numpy](http://numpy.org) and fortran compiler, preferably\n", - "[gfortran](https://gcc.gnu.org/wiki/GFortran), required):\n", + "`stripy` requires [numpy](http://numpy.org) and a Fortran compiler, preferably\n", + "[gfortran](https://gcc.gnu.org/wiki/GFortran). The Fortran extensions are built\n", + "with [meson-python](https://meson-python.readthedocs.io).\n", + "\n", + "Install the released version with pip:\n", "\n", "```bash\n", - "python setup.py build\n", - "```\n", - " - If you change the fortran compiler, you may have to add the\n", - "flags `config_fc --fcompiler=` when setup.py is run\n", - "(see docs for [numpy.distutils](http://docs.scipy.org/doc/numpy-dev/f2py/distutils.html)).\n", - "```bash\n", - "python setup.py install\n", + "pip install [--user] stripy\n", "```\n", "\n", - "Alternatively install using pip:\n", + "Or build and install from a source checkout:\n", "\n", "```bash\n", - "pip install [--user] stripy\n", + "pip install .\n", "```\n", "\n", + "To select a specific Fortran compiler, set the standard `FC` environment\n", + "variable (e.g. `FC=gfortran-13`) before invoking `pip install`.\n", + "\n", "## Usage\n", "\n", "Two classes are included as part of the Stripy package:\n", diff --git a/stripy/__init__.py b/stripy/__init__.py index 721a6777..77ad37b0 100644 --- a/stripy/__init__.py +++ b/stripy/__init__.py @@ -74,7 +74,7 @@ def weighted_average_to_nodes(x1, x2, data, interpolator ): gridded_data = np.zeros(interpolator.npoints) norm = np.zeros(interpolator.npoints) - count = np.zeros(interpolator.npoints, dtype=np.int) + count = np.zeros(interpolator.npoints, dtype=int) bcc, nodes = interpolator.containing_simplex_and_bcc(x1, x2) diff --git a/stripy/cartesian.py b/stripy/cartesian.py index b071cb2e..7513e1a7 100644 --- a/stripy/cartesian.py +++ b/stripy/cartesian.py @@ -29,7 +29,11 @@ _ier_codes = {0: "no errors were encountered.", -1: "N < 3 on input.", - -2: "the first three nodes are collinear.\nSet permute to True or reorder nodes manually.", + -2: "collinear nodes were encountered: either the first three " + "nodes lie on a common line, or TRMESH's incremental insertion " + "(ADDNOD) found all nodes up to some node to be collinear.\n" + "Set permute=True or reorder the nodes manually; if the error " + "persists the points may be (near-)collinear or degenerate.", -3: "duplicate nodes were encountered.", -4: "an error flag was returned by a call to SWAP in ADDNOD.\n \ This is an internal error and should be reported to the programmer.", diff --git a/stripy/cartesian_meshes.py b/stripy/cartesian_meshes.py index 9e83063e..0cec7bef 100644 --- a/stripy/cartesian_meshes.py +++ b/stripy/cartesian_meshes.py @@ -252,7 +252,7 @@ def equal_angles_in_ellipse(arc_length, a, b): pointsx, pointsy = equal_angles_in_ellipse( spacing, a, b) - bmask = np.full_like(pointsx, 0, dtype=np.bool) + bmask = np.full_like(pointsx, 0, dtype=bool) a -= spacing b -= spacing @@ -260,7 +260,7 @@ def equal_angles_in_ellipse(arc_length, a, b): points = equal_angles_in_ellipse( spacing, a, b) pointsx = np.append(pointsx,points[0]) pointsy = np.append(pointsy,points[1]) - bmask = np.append(bmask, np.full_like(points[0], 1, dtype=np.bool)) + bmask = np.append(bmask, np.full_like(points[0], 1, dtype=bool)) a -= spacing b -= spacing diff --git a/stripy/spherical.py b/stripy/spherical.py index b3ffcc00..a8ec33b8 100755 --- a/stripy/spherical.py +++ b/stripy/spherical.py @@ -29,7 +29,12 @@ _ier_codes = {0: "no errors were encountered.", -1: "N < 3 on input.", - -2: "the first three nodes lie on a great circle.\nSet permute to True or reorder nodes manually.", + -2: "collinear nodes were encountered: either the first three " + "nodes lie on a common great circle, or STRMESH's incremental " + "insertion (ADDNOD) found all nodes up to some node to be " + "collinear.\nSet permute=True or reorder the nodes manually; if " + "the error persists the points may be (near-)collinear or " + "degenerate on the sphere.", -3: "duplicate nodes were encountered.", -4: "an error flag was returned by a call to SWAP in ADDNOD.\n \ This is an internal error and should be reported to the programmer.", diff --git a/stripy/tests/test_Triangulation.py b/stripy/tests/test_Triangulation.py index aa6b15ff..07aae9c0 100644 --- a/stripy/tests/test_Triangulation.py +++ b/stripy/tests/test_Triangulation.py @@ -50,13 +50,16 @@ def test_linear_interpolation(permute): # but machine precision may differ so we don't test it # print((Zi == ix).all()) - bounded = Zi[0] == ix[0] and Zi[-1] == ix[-1] + # The endpoints coincide with input nodes, so linear interpolation should + # recover their x. Compare with a tolerance rather than exact equality: for + # a permuted mesh the result differs at the ~1e-16 level and exact equality + # is platform-dependent (see gh-115). The interior is not checked because + # machine precision may differ. + bounded = np.isclose(Zi[0], ix[0]) and np.isclose(Zi[-1], ix[-1]) ascending = ( np.diff(Zi) > 0 ).all() - if bounded and ascending: - print("PASS! (Interpolation - linear") - else: - assert False, "FAIL! (Interpolation - linear)" + assert bounded and ascending, \ + "FAIL! (Interpolation - linear): Zi={}, ix={}, ierr={}".format(Zi, ix, ierr) @pytest.mark.parametrize("permute", [False, True]) diff --git a/stripy/tests/test_sTriangulation.py b/stripy/tests/test_sTriangulation.py index da53cfac..f116e848 100644 --- a/stripy/tests/test_sTriangulation.py +++ b/stripy/tests/test_sTriangulation.py @@ -59,14 +59,17 @@ def test_linear_interpolation(permute): # but machine precision may differ so we don't test it # print((Zi == ilons).all()) - bounded = Zi[0] == ilons[0] and Zi[-1] == ilons[-1] + # The endpoints coincide with input nodes, so linear interpolation should + # recover their longitude. Compare with a tolerance rather than exact + # equality: for a permuted mesh the result differs at the ~1e-16 level and + # exact equality is platform-dependent (see gh-115). The interior is not + # checked because machine precision may differ. + bounded = np.isclose(Zi[0], ilons[0]) and np.isclose(Zi[-1], ilons[-1]) ascending = ( np.diff(Zi) > 0 ).all() # check if linear gradient across the equator - if bounded and ascending: - print("PASS! (Interpolation - linear") - else: - assert False, "FAIL! (Interpolation - linear)" + assert bounded and ascending, \ + "FAIL! (Interpolation - linear): Zi={}, ilons={}, ierr={}".format(Zi, ilons, ierr) @pytest.mark.parametrize("permute", [False, True])