diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..16bde10 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +.DS_Store +build +docs/_build +quickstart diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 56df676..8128bcd 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -11,21 +11,21 @@ on: push jobs: test: name: Run Unit Tests - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 defaults: run: # pinning the shell name helps to properly set conda shell: bash -l {0} strategy: matrix: - python-version: [ 3.7, 3.8, 3.9] + python-version: [ 3.7, 3.8, 3.9, '3.12', '3.13', '3.14' ] env: OMP_NUM_THREADS: 2 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v6 - name: Setup conda for Python ${{ matrix.python-version }} - uses: conda-incubator/setup-miniconda@v2 + uses: conda-incubator/setup-miniconda@v4 with: activate-environment: test python-version: ${{ matrix.python-version }} @@ -33,7 +33,6 @@ jobs: allow-softlinks: true channel-priority: flexible show-channel-urls: true - use-only-tar-bz2: true - name: Print conda config run: | conda info @@ -45,17 +44,21 @@ jobs: - name: Build and install galario, build docs run: | conda activate test - conda install astropy cython nomkl numpy pytest scipy sphinx + conda install cython nomkl numpy pytest scipy sphinx pip install coverage codecov pytest-cov mkdir build && cd build cmake -DCMAKE_INSTALL_PREFIX=/tmp -DCMAKE_PREFIX_PATH=${CONDA_PREFIX} .. make make install - name: Run unit tests - run: python/py.test.sh -sv --cov=./ python/test_galario.py + run: python/py.test.sh -sv --cov=./ --cov-report=xml:coverage.xml python/test_galario.py working-directory: build - name: Upload code coverage report - run: bash <(curl -s https://codecov.io/bash) || echo 'Codecov failed to upload' + uses: codecov/codecov-action@v5 + with: + files: ./build/coverage.xml + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} - name: build docs run: | conda activate test diff --git a/CMakeLists.txt b/CMakeLists.txt index d1a5c32..a81d1e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,9 @@ # For documentation see https://mtazzari.github.io/galario/ # ############################################################################### -cmake_minimum_required(VERSION 3.0) +# CMake 4 no longer provides compatibility for projects declaring a version +# older than 3.5. 3.10 still supports the legacy FindCUDA-based GPU build. +cmake_minimum_required(VERSION 3.10) project(galario) set(PACKAGE_VERSION "'1.2.2'") diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a668b52 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +# Reproduce the Linux CPU installation with pip-managed Python dependencies. +# CUDA is intentionally disabled here: a CUDA build requires an NVIDIA toolkit +# and is covered by the same CMake path when GALARIO_CHECK_CUDA is enabled. +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive \ + OMP_NUM_THREADS=2 \ + PYTHONPATH=/opt/galario/lib/python3.10/site-packages \ + PYTHONDONTWRITEBYTECODE=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + git \ + libfftw3-dev \ + python3 \ + python3-dev \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* + +RUN python3 -m pip install --no-cache-dir \ + 'Cython>=3' \ + numpy \ + pytest \ + pytest-cov \ + scipy + +WORKDIR /src +COPY . . + +RUN cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/opt/galario \ + -DGALARIO_CHECK_CUDA=OFF \ + && cmake --build build --parallel \ + && cmake --install build \ + && python3 -c 'import galario; print(galario.__file__)' \ + && ctest --test-dir build --output-on-failure diff --git a/Dockerfile.conda b/Dockerfile.conda new file mode 100644 index 0000000..b07aab4 --- /dev/null +++ b/Dockerfile.conda @@ -0,0 +1,37 @@ +# Reproduce the documented and GitHub Actions Linux CPU build in a Conda +# environment. CUDA is intentionally disabled because this image has neither +# an NVIDIA toolkit nor GPU access. +FROM continuumio/miniconda3:latest + +ENV DEBIAN_FRONTEND=noninteractive \ + OMP_NUM_THREADS=2 \ + PYTHONDONTWRITEBYTECODE=1 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + git \ + libfftw3-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN conda create -y --name galario --channel conda-forge \ + python=3.12 \ + cython \ + numpy \ + pytest \ + pytest-cov \ + scipy \ + && conda clean --all --yes + +WORKDIR /src +COPY . . + +RUN conda run --no-capture-output --name galario \ + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGALARIO_CHECK_CUDA=OFF \ + && conda run --no-capture-output --name galario cmake --build build --parallel \ + && conda run --no-capture-output --name galario cmake --install build \ + && conda run --no-capture-output --name galario \ + python -c 'import galario; print(galario.__file__)' \ + && conda run --no-capture-output --name galario \ + ctest --test-dir build --output-on-failure diff --git a/docs/conf.py.in b/docs/conf.py.in index 26de1cd..22c72d0 100644 --- a/docs/conf.py.in +++ b/docs/conf.py.in @@ -72,7 +72,7 @@ release = str(galario.__version__) # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: @@ -307,4 +307,4 @@ texinfo_documents = [ # override CSS file def setup(app): - app.add_stylesheet('css/custom.css') + app.add_css_file('css/custom.css') diff --git a/docs/install.rst b/docs/install.rst index 7681a92..4a065f0 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -27,6 +27,30 @@ To create a conda environment for |galario|, see Section 1.4, step 2. Due to technical limitations, the conda package does not support GPUs at the moment. If you want to use a GPU, read on as you have to build |galario| by hand. +Linux installation test containers +---------------------------------- + +The repository includes two Linux CPU test containers. The Conda container +mirrors the manual and GitHub Actions installation path, using system FFTW and +a Conda environment for Python and its build dependencies: + +.. code-block:: bash + + ./scripts/build-linux-conda-container.sh + +The pip container provides complementary coverage for a future PyPI release: + +.. code-block:: bash + + ./scripts/build-linux-container.sh + +It builds an ``linux/amd64`` image by default, matching the usual Linux +research-server target. Set ``DOCKER_PLATFORM`` to build another platform. +The image deliberately configures ``-DGALARIO_CHECK_CUDA=OFF`` because Docker +Desktop on a non-NVIDIA host cannot compile or run CUDA. The production CUDA +configuration remains available by enabling ``GALARIO_CHECK_CUDA`` on a host +with a compatible NVIDIA toolkit. + Build requirements ------------------ diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 6ba1672..ce80c98 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -48,8 +48,19 @@ endif() ### # installation ### -# to set ${PYTHON_PKG_DIR}. It is prepended by ${CMAKE_INSTALL_PREFIX} if set -include(PythonInstall) +# PythonInstall from GreatCMakeCookOff imports distutils, which was removed in +# Python 3.12. Use the conventional installation layout relative to +# CMAKE_INSTALL_PREFIX instead. +execute_process( + COMMAND "${PYTHON_EXECUTABLE}" -c + "import sys; print('lib/python{}.{}{}'.format(sys.version_info[0], sys.version_info[1], '/site-packages'))" + OUTPUT_VARIABLE PYTHON_PKG_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE +) +if(PYTHON_PKG_DIR MATCHES "^\\.\\.") + message(FATAL_ERROR "Could not determine a Python package directory relative to ${PYTHON_EXECUTABLE}") +endif() +message(STATUS "Python install path (PYTHON_PKG_DIR): ${PYTHON_PKG_DIR}") # I guess variable has to be in cache to survive reinvocations of cmake that happen during building set(GALARIO_PYTHON_PKG_DIR "${PYTHON_PKG_DIR}" CACHE PATH "Current python install path") @@ -77,13 +88,14 @@ configure_file("${galario_test}" "${CMAKE_CURRENT_BINARY_DIR}" COPYONLY) add_test(NAME CanImport COMMAND ${PYTHON_EXECUTABLE} -c "import ${PROJECT_NAME}") # https://github.com/UCL/GreatCMakeCookOff/wiki/Adding-py.test-to-ctest -include(AddPyTest) - -# I don't understand the first arg but it seems to work if py.test already installed -setup_pytest("${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/py.test.sh") +# Keep a small compatibility wrapper for documented/manual test commands. +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/py.test.sh.in" + "${CMAKE_CURRENT_BINARY_DIR}/py.test.sh" @ONLY) +if(UNIX) + execute_process(COMMAND chmod +x "${CMAKE_CURRENT_BINARY_DIR}/py.test.sh") +endif() -# the source name has to match test_*.py -# https://github.com/UCL/GreatCMakeCookOff/issues/56 -# -# need to run in binary_dir so import of pygalario works -add_pytest("${CMAKE_CURRENT_BINARY_DIR}/${galario_test}" NOINSTALL CMDLINE --cov=./) +# Run in the binary directory so the generated galario package is importable. +add_test(NAME galario + COMMAND "${PYTHON_EXECUTABLE}" -m pytest "${CMAKE_CURRENT_BINARY_DIR}/${galario_test}" --cov=./) +set_tests_properties(galario PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") diff --git a/python/libcommon.pyx b/python/libcommon.pyx index 5420888..95cebe3 100644 --- a/python/libcommon.pyx +++ b/python/libcommon.pyx @@ -18,7 +18,7 @@ ############################################################################### cimport numpy as np -from cpython cimport PyObject, Py_INCREF +from cpython cimport Py_INCREF # Numpy must be initialized. When using numpy from C or Cython you must # _always_ do that, or you will have segfaults @@ -82,10 +82,12 @@ cdef class ArrayWrapper: # Create a 2D array, of length `nx*ny/2+1` ndarray = np.PyArray_SimpleNewFromData(2, shape, complex_typenum, self.data_ptr) - ndarray.base = self - # without this, data would be cleaned up right away + # NumPy owns a reference to its base object. Assigning ``.base`` was + # accepted by older Cython releases, but is a read-only property in + # Cython 3. PyArray_SetBaseObject steals the reference passed to it. Py_INCREF(self) + np.PyArray_SetBaseObject(ndarray, self) return ndarray def __dealloc__(self): diff --git a/python/py.test.sh.in b/python/py.test.sh.in new file mode 100644 index 0000000..63b1bf3 --- /dev/null +++ b/python/py.test.sh.in @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +exec "@PYTHON_EXECUTABLE@" -m pytest "$@" diff --git a/python/utils.py b/python/utils.py index 8bf3742..be55d69 100644 --- a/python/utils.py +++ b/python/utils.py @@ -25,7 +25,6 @@ import numpy as np from scipy.interpolate import interp1d, RectBivariateSpline -from scipy.integrate import trapz, quadrature __all__ = ["py_sampleImage", "py_sampleProfile", "py_chi2Profile", "py_chi2Image", "radial_profile", "g_sweep_prototype", "sweep_ref", @@ -217,7 +216,7 @@ def g_sweep_prototype(I, Rmin, dR, nrow, ncol, dxy, inc, dtype_image='float64'): inc_cos = np.cos(inc) # radial extent in number of image pixels covered by the profile - rmax = min(np.int(np.ceil((Rmin+nrad*dR)/dxy)), irow_center) + rmax = min(int(np.ceil((Rmin+nrad*dR)/dxy)), irow_center) row_offset = irow_center-rmax col_offset = icol_center-rmax for irow in range(rmax*2): @@ -227,7 +226,7 @@ def g_sweep_prototype(I, Rmin, dR, nrow, ncol, dxy, inc, dtype_image='float64'): rr = np.sqrt((x/inc_cos)**2. + (y)**2.) # interpolate 1D - iR = np.int(np.floor((rr-Rmin) / dR)) + iR = int(np.floor((rr-Rmin) / dR)) if iR >= nrad-1: image[irow+row_offset, jcol+col_offset] = 0. else: @@ -394,10 +393,10 @@ def int_bilin_MT(f, x, y): for i in range(len(x)): t = y[i] - np.floor(y[i]) u = x[i] - np.floor(x[i]) - y0 = f[np.int(np.floor(y[i])), np.int(np.floor(x[i]))] - y1 = f[np.int(np.floor(y[i])) + 1, np.int(np.floor(x[i]))] - y2 = f[np.int(np.floor(y[i])) + 1, np.int(np.floor(x[i])) + 1] - y3 = f[np.int(np.floor(y[i])), np.int(np.floor(x[i])) + 1] + y0 = f[int(np.floor(y[i])), int(np.floor(x[i]))] + y1 = f[int(np.floor(y[i])) + 1, int(np.floor(x[i]))] + y2 = f[int(np.floor(y[i])) + 1, int(np.floor(x[i])) + 1] + y3 = f[int(np.floor(y[i])), int(np.floor(x[i])) + 1] vis_int[i] = t * u * (y0 - y1 + y2 - y3) vis_int[i] += t * (y1 - y0) diff --git a/scripts/build-linux-conda-container.sh b/scripts/build-linux-conda-container.sh new file mode 100755 index 0000000..2b99fe1 --- /dev/null +++ b/scripts/build-linux-conda-container.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +# Build the Linux/amd64 Conda installation test image from the repository root. +set -eu + +image_tag=${1:-galario-linux-conda-test:local} +platform=${DOCKER_PLATFORM:-linux/amd64} +exec docker build --quiet --platform "$platform" --file Dockerfile.conda --tag "$image_tag" . diff --git a/scripts/build-linux-container.sh b/scripts/build-linux-container.sh new file mode 100755 index 0000000..c3a7f2e --- /dev/null +++ b/scripts/build-linux-container.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +# Build the Linux CPU installation test image from the repository root. +set -eu + +image_tag=${1:-galario-linux-test:local} +platform=${DOCKER_PLATFORM:-linux/amd64} +exec docker build --quiet --platform "$platform" --tag "$image_tag" .