diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 8745acd..8f2ee01 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -46,11 +46,14 @@ jobs: # This action sets the current path to the root of your github repo - uses: actions/checkout@v7 - - name: Update pip - run: python3 -m pip install --upgrade pip setuptools + - name: Update pip and extra deps + run: | + python3 -m pip install --upgrade pip setuptools + python3 -m pip install --no-binary=h5py h5py + python3 -m pip install . --group docs - name: Install dependencies - run: python3 -m pip install --no-binary=h5py -e ".[docs]" + run: python3 -m pip install -e . --no-build-isolation - name: Build docs run: jupyter book build . diff --git a/.github/workflows/check_formatting.yml b/.github/workflows/check_formatting.yml index afe2700..96183fc 100644 --- a/.github/workflows/check_formatting.yml +++ b/.github/workflows/check_formatting.yml @@ -43,11 +43,13 @@ jobs: steps: - uses: actions/checkout@v7 - - name: Update pip - run: python3 -m pip install --upgrade pip setuptools + - name: Update pip and install build deps + run: | + python3 -m pip install --upgrade pip setuptools + python3 -m pip install . --group dev --no-build-isolation - name: Install code - run: python3 -m pip install .[dev] + run: python3 -m pip install . --no-build-isolation - name: Check code formatting with ruff run: | diff --git a/.github/workflows/test_dolfinx_versions.yml b/.github/workflows/test_dolfinx_versions.yml index 56441dd..c9233ab 100644 --- a/.github/workflows/test_dolfinx_versions.yml +++ b/.github/workflows/test_dolfinx_versions.yml @@ -31,4 +31,4 @@ jobs: # Call the reusable workflow uses: ./.github/workflows/test_workflow.yml with: - dolfinx_tag: ${{ matrix.tag }} + tag: ${{ matrix.tag }} diff --git a/.github/workflows/test_package_openmpi.yml b/.github/workflows/test_package_openmpi.yml deleted file mode 100644 index 7c98a9a..0000000 --- a/.github/workflows/test_package_openmpi.yml +++ /dev/null @@ -1,88 +0,0 @@ -name: Test package with different mpi compilers - -on: - push: - branches: - - main - pull_request: - branches: - - main - - workflow_call: - workflow_dispatch: - schedule: - - cron: "0 8 * * *" - -jobs: - create-datasets: - uses: ./.github/workflows/create_legacy_data.yml - with: - artifact_name: "legacy_ompi" - - create-legacy-datasets: - uses: ./.github/workflows/create_legacy_checkpoint.yml - with: - artifact_name: "legacy_checkpoint_ompi" - - test-code: - runs-on: ubuntu-latest - needs: [create-datasets, create-legacy-datasets] - container: ${{ matrix.container }} - env: - DEB_PYTHON_INSTALL_LAYOUT: deb_system - PETSC_ARCH: "linux-gnu-real64-32" - OMPI_ALLOW_RUN_AS_ROOT: 1 - OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: 1 - PRTE_MCA_rmaps_default_mapping_policy: :oversubscribe - working-directory: ./src - PIP_NO_BINARY: h5py - - strategy: - matrix: - adios2: ["v2.11.0", "v2.12.1", "default"] - container: - - ghcr.io/fenics/test-env:current-openmpi - - ghcr.io/fenics/test-env:current-mpich - steps: - - uses: actions/checkout@v7 - - - name: Update pip - run: python3 -m pip install --upgrade pip - - - name: Install build requirements - run: python3 -m pip install -r build-requirements.txt - - - name: Install DOLFINx - uses: jorgensd/actions/install-dolfinx@v0.4 - with: - adios2: ${{ matrix.adios2 }} - petsc_arch: ${{ env.PETSC_ARCH }} - dolfinx: main - basix: main - ufl: main - ffcx: main - working-directory: ${{ env.working-directory}} - - - name: Download legacy data - uses: actions/download-artifact@v8 - with: - name: legacy_ompi - path: ./legacy - - - name: Download legacy data - uses: actions/download-artifact@v8 - with: - name: legacy_checkpoint_ompi - path: ./legacy_checkpoint - - - name: Install package - run: | - HDF5_MPI=ON HDF5_PKGCONFIG_NAME="hdf5" python3 -m pip install h5py --no-build-isolation --no-binary=h5py - python3 -m pip install .[test] - - name: Run tests - run: | - coverage run --rcfile=.coveragerc -m mpi4py -m pytest -xvs ./tests/ - - - name: Run tests in parallel - run: | - mpirun -n 2 coverage run --rcfile=.coveragerc -m mpi4py -m pytest -xvs ./tests/ diff --git a/.github/workflows/test_redhat.yml b/.github/workflows/test_redhat.yml deleted file mode 100644 index e0a8b65..0000000 --- a/.github/workflows/test_redhat.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: Test package with redhat - -on: - # push: - # branches: - # - main - # pull_request: - # branches: - # - main - - workflow_call: - workflow_dispatch: - # schedule: - # - cron: "0 8 * * *" - -jobs: - create-datasets: - uses: ./.github/workflows/create_legacy_data.yml - with: - artifact_name: "legacy_ompi" - - create-legacy-datasets: - uses: ./.github/workflows/create_legacy_checkpoint.yml - with: - artifact_name: "legacy_checkpoint_ompi" - - test-code: - runs-on: "ubuntu-latest" - needs: [create-datasets, create-legacy-datasets] - container: docker.io/fenicsproject/test-env:current-redhat - env: - DEB_PYTHON_INSTALL_LAYOUT: deb_system - PETSC_ARCH: "" - PETSC_DIR: "/usr/local/" - PYTHONPATH: "/usr/local/lib/:${PYTHONPATH}" - working-directory: ./src - - strategy: - matrix: - adios2: ["master"] - steps: - - uses: actions/checkout@v7 - - - name: Get pip flags based on version - id: python-version - shell: bash -el {0} - run: | - MODERN_PIP=$(python3 -c "import sys; t = sys.version_info >= (3, 11, 0); sys.stdout.write(str(t))") - if [ ${MODERN_PIP} == "True" ]; then - FLAGS="--break-system-packages" - else - FLAGS="" - python3 -m pip install --upgrade pip - fi - echo "PYTHON_FLAGS=${FLAGS}" >> "$GITHUB_OUTPUT" - - - name: Update pip - run: python3 -m pip install ${{ steps.python-version.outputs.PYTHON_FLAGS}} --upgrade pip setuptools - - - name: Install DOLFINx - uses: jorgensd/actions/install-dolfinx@v0.4 - with: - adios2: ${{ matrix.adios2 }} - petsc_arch: ${{ env.PETSC_ARCH }} - petsc_dir: ${{ env.PETSC_DIR }} - dolfinx: main - basix: main - ufl: main - ffcx: main - working-directory: ${{ env.working-directory}} - - - name: Download legacy data - uses: actions/download-artifact@v8 - with: - name: legacy_ompi - path: ./legacy - - - name: Download legacy data - uses: actions/download-artifact@v8 - with: - name: legacy_checkpoint_ompi - path: ./legacy_checkpoint - - - name: Install package - run: python3 -m pip install ${{ steps.python-version.outputs.PYTHON_FLAGS}} --check-build-dependencies .[test] - - - name: Run tests - run: | - coverage run --rcfile=.coveragerc -m mpi4py -m pytest -xvs ./tests - - - name: Run tests in parallel - run: | - mpirun -n 4 coverage run --rcfile=.coveragerc -m mpi4py -m pytest -xvs ./tests diff --git a/.github/workflows/test_workflow.yml b/.github/workflows/test_workflow.yml index 7849337..11e0728 100644 --- a/.github/workflows/test_workflow.yml +++ b/.github/workflows/test_workflow.yml @@ -1,17 +1,49 @@ -name: Test +name: Test package on: workflow_call: inputs: - dolfinx_tag: + tag: + description: "Tag of DOLFINx docker image" + default: "nightly" required: true type: string + workflow_dispatch: + inputs: + tag: + description: "Tag of DOLFINx docker image" + default: "nightly" + required: true + type: string +env: + DEB_PYTHON_INSTALL_LAYOUT: deb_system + DEFAULT_TAG: nightly + HDF5_MPI: "ON" + HDF5_INCLUDEDIR: "/usr/include/hdf5/openmpi" + H5PY_SETUP_REQUIRES: 0 + OMPI_ALLOW_RUN_AS_ROOT: 1 + OMPI_ALLOW_RUN_AS_ROOT_CONFIRM: 1 + PRTE_MCA_rmaps_default_mapping_policy: :oversubscribe + CC: mpicc + CXX: mpicxx jobs: + get_image_tag: + runs-on: ubuntu-latest + outputs: + image: ${{ steps.docker_tag.outputs.image }} + steps: + - id: docker_tag + run: echo "image=${{ inputs.tag || env.DEFAULT_TAG }}" >> $GITHUB_OUTPUT + test-code: runs-on: ubuntu-24.04 # The container tag is now dynamic based on the input passed by the caller - container: ghcr.io/fenics/dolfinx/dolfinx:${{ inputs.dolfinx_tag }} + container: ghcr.io/fenics/test-env:current + needs: + [get_image_tag] + env: + working-directory: ./src steps: - uses: actions/checkout@v7 @@ -20,7 +52,8 @@ jobs: run: python3 -m pip install --upgrade pip - name: Install build requirements - run: python3 -m pip install -r build-requirements.txt + run: python3 -m pip install --group build --no-build-isolation + # Note: We assume the caller workflow has already uploaded these artifacts - name: Download legacy mpich data @@ -35,10 +68,38 @@ jobs: name: legacy_checkpoint_mpich path: ./legacy_checkpoint + - name: Install DOLFINx (stable) + if: needs.get_image_tag.outputs.image == 'stable' + uses: jorgensd/actions/install-dolfinx@v0.4 + with: + adios2: default + petsc_arch: linux-gnu-real64-32 + dolfinx: release + basix: release + ufl: release + ffcx: release + working-directory: ${{ env.working-directory}} + + - name: Install DOLFINx (nightly) + if: needs.get_image_tag.outputs.image == 'nightly' + uses: jorgensd/actions/install-dolfinx@v0.4 + with: + adios2: default + petsc_arch: linux-gnu-real64-32 + working-directory: ${{ env.working-directory}} + + - name: Configure HDF5 MPI Environment + run: | + # Evaluate architecture dynamically and write to GITHUB_ENV + ARCH_TRIPLET=$(gcc -print-multiarch) + echo "HDF5_LIBDIR=/usr/lib/${ARCH_TRIPLET}/hdf5/openmpi" >> $GITHUB_ENV + - name: Install package run: | - HDF5_MPI=ON HDF5_PKGCONFIG_NAME="hdf5" python3 -m pip install h5py --no-build-isolation --no-binary=h5py - python3 -m pip install .[test] + python3 -m pip install "Cython>=3.0.11" "pkgconfig>=1.5.5" + python3 -m pip install h5py --no-binary=h5py --no-build-isolation + python3 -m pip install --group dev --group test + python3 -m pip install . --no-build-isolation - name: Show adios2 version run: python3 -c "import adios2; print(adios2.__version__)" @@ -65,6 +126,6 @@ jobs: - name: Upload coverage report uses: actions/upload-artifact@v7 with: - name: code-coverage-report-${{ inputs.dolfinx_tag }} + name: code-coverage-report-${{ inputs.tag }} path: htmlcov if-no-files-found: error diff --git a/build-requirements.txt b/build-requirements.txt deleted file mode 100644 index a58cd3b..0000000 --- a/build-requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -scikit-build-core[pyproject]>=0.11.0 -nanobind>=1.3.2 -setuptools>=77.0.3 -packaging>=24.2 -pkgconfig diff --git a/docs/testing.md b/docs/testing.md index 7751c05..e7cbe20 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -21,7 +21,8 @@ Install the library with the optional `test` dependencies to ensure you have packages like `pytest`, `coverage`, and `ipyparallel` ```bash -python3 -m pip install ".[test]" +python3 -m pip install . --group test --no-build-isolation +python3 -m pip install -e . --no-build-isolation ``` ### Running Tests diff --git a/pyproject.toml b/pyproject.toml index ccb8852..fb1bed6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,13 @@ -[build-system] # Require setuptool version due to https://github.com/pypa/setuptools/issues/2938 -requires = ["setuptools>=61.0.0", "wheel"] +[build-system] +requires = [ + "setuptools>=77.0.3", + "wheel", + "scikit-build-core[pyproject]>=0.11.0", + "nanobind>=1.3.2,<2.15", # Temporary cap on nanobind for stable dolfinx + "packaging>=24.2", + "pkgconfig" +] +build-backend = "setuptools.build_meta" [project] name = "io4dolfinx" @@ -11,9 +19,24 @@ authors = [ ] license = { file = "LICENSE" } readme = "README.md" +requires-python = ">=3.12" dependencies = ["fenics-dolfinx>=0.10.0", "packaging"] [project.optional-dependencies] +h5py = ["h5py"] +pyvista = ["pyvista"] +xdmf = ["h5py"] +exodus = ["netcdf4"] + +[dependency-groups] +build = [ + "scikit-build-core[pyproject]>=0.11.0", + "nanobind>=1.3.2,<3.0", + "setuptools>=77.0.3", + "packaging>=24.2", + "pkgconfig" +] + test = [ "pytest", "coverage", @@ -22,23 +45,28 @@ test = [ "io4dolfinx[pyvista]", "io4dolfinx[xdmf]" ] -dev = ["pdbpp", "ipython", "mypy", "ruff"] -h5py = ["h5py"] -pyvista = ["pyvista"] -xdmf = ["h5py"] -exodus = ["netcdf4"] +dev = [ + "pdbpp", + "ipython", + "mypy", + "ruff" +] docs = [ "jupyter-book<2.0.0", "ipyparallel", "ipywidgets", "jupytext", - "ipykernel<7.0.0", # Note: Remove once https://github.com/ipython/ipykernel/issues/1450 is in a release + "ipykernel", "sphinx-codeautolink", "io4dolfinx[h5py]", "io4dolfinx[pyvista]", "sphinx_external_toc<1.1.0", ] -all = ["io4dolfinx[test,dev,docs]"] +all = [ + { include-group = "test" }, + { include-group = "dev" }, + { include-group = "docs" } +] [tool.pytest.ini_options] addopts = ["--import-mode=importlib"] @@ -67,7 +95,6 @@ select = [ "I001", ] - [tool.ruff.lint.isort] known-first-party = ["io4dolfinx"] known-third-party = [ @@ -91,7 +118,6 @@ section-order = [ [tool.ruff.lint.isort.sections] "mpi" = ["mpi4py", "petsc4py"] - [tool.bumpversion] allow_dirty = false commit = true @@ -102,7 +128,6 @@ tag_name = "v{new_version}" tag_message = "Bump version: {current_version} → {new_version}" current_version = "1.3.0" - [[tool.bumpversion.files]] filename = "pyproject.toml" search = 'version = "{current_version}"' diff --git a/src/io4dolfinx/checkpointing.py b/src/io4dolfinx/checkpointing.py index a128ef6..de4f8c0 100644 --- a/src/io4dolfinx/checkpointing.py +++ b/src/io4dolfinx/checkpointing.py @@ -6,6 +6,7 @@ from __future__ import annotations +import inspect import logging import typing from pathlib import Path @@ -454,20 +455,21 @@ def read_mesh( dtype=dist_in_data.x.dtype, ) domain = ufl.Mesh(element) - PartitionerType = Callable[ - [MPI.Comm, int, list[dolfinx.mesh.CellType], list[npt.NDArray[np.int64]]], - dolfinx.cpp.graph.AdjacencyList_int32, - ] - partitioner: PartitionerType + # `dolfinx.mesh.PartitioningFunc` is not available in all supported + # versions of DOLFINx, and the accepted callback signature varies (see + # below), so type the callback permissively. + partitioner: Callable[..., dolfinx.cpp.graph.AdjacencyList_int32] if (partition_graph := dist_in_data.partition_graph) is not None: - - def _custom_partitioner( - comm: MPI.Comm, - nparts: int, - cell_types: list[dolfinx.mesh.CellType], - local_graph: list[npt.NDArray[np.int64]], - ) -> dolfinx.cpp.graph.AdjacencyList_int32: - assert len(local_graph[0]) % (len(partition_graph.offsets) - 1) == 0 + # The arguments DOLFINx passes to a partitioner callback have changed + # over time: 0.11 calls it with + # ``(comm, nparts, cell_types, local_graph)``, while newer versions call + # it with ``(comm, nparts, dual_graph, cell_weights, edge_weights, + # ghosting)`` (see https://github.com/FEniCS/dolfinx/pull/4403). + # We read the partitioning from file, so the returned graph does not + # depend on any of these arguments. Accept them all and stay agnostic to + # the calling convention rather than branching on `dolfinx.__version__`, + # which does not distinguish pre-releases and post-releases reliably. + def _custom_partitioner(*args: Any, **kwargs: Any) -> dolfinx.cpp.graph.AdjacencyList_int32: if hasattr(partition_graph, "_cpp_object"): cpp_obj = partition_graph._cpp_object assert isinstance(cpp_obj, dolfinx.cpp.graph.AdjacencyList_int32) @@ -478,21 +480,28 @@ def _custom_partitioner( partitioner = _custom_partitioner else: - try: - partitioner = dolfinx.cpp.mesh.create_cell_partitioner( - ghost_mode, max_facet_to_cell_links=max_facet_to_cell_links - ) - except TypeError: - partitioner = dolfinx.cpp.mesh.create_cell_partitioner(ghost_mode) # type: ignore[call-overload] - - # Should change to the commented code below when we require python - # minimum version to be >=3.12 see https://github.com/python/cpython/pull/116198 - # import inspect - # sig = inspect.signature(dolfinx.mesh.create_cell_partitioner) - # part_kwargs = {} - # if "max_facet_to_cell_links" in list(sig.parameters.keys()): - # part_kwargs["max_facet_to_cell_links"] = max_facet_to_cell_links - # partitioner = dolfinx.cpp.mesh.create_cell_partitioner(ghost_mode, **part_kwargs) + if not hasattr(dolfinx.mesh, "create_cell_partitioner"): + partitioner = dolfinx.graph.partitioner() + else: + sig = inspect.signature(dolfinx.mesh.create_cell_partitioner) + part_kwargs = {} + + if "max_facet_to_cell_links" in sig.parameters: + part_kwargs["max_facet_to_cell_links"] = max_facet_to_cell_links + + partitioner = dolfinx.mesh.create_cell_partitioner(ghost_mode, **part_kwargs) + + mesh_args: dict[str, Any] = {} + mesh_sig = inspect.signature(dolfinx.mesh.create_mesh) + if "max_facet_to_cell_links" in mesh_sig.parameters: + mesh_args["max_facet_to_cell_links"] = max_facet_to_cell_links + if "ghost_mode" in mesh_sig.parameters: + mesh_args["ghost_mode"] = ghost_mode + # TODO: Add more options here later + if "cell_weights" in mesh_sig.parameters: + mesh_args["cell_weights"] = None # No cell weights provided, default to None + if "num_threads" in mesh_sig.parameters: + mesh_args["num_threads"] = 1 # Default to 1 thread, can be adjusted if needed return dolfinx.mesh.create_mesh( comm, @@ -500,6 +509,7 @@ def _custom_partitioner( x=dist_in_data.x, e=domain, partitioner=partitioner, + **mesh_args, ) diff --git a/tests/create_legacy_data.py b/tests/create_legacy_data.py index f4a578f..ba39985 100644 --- a/tests/create_legacy_data.py +++ b/tests/create_legacy_data.py @@ -14,7 +14,6 @@ import dolfin import numpy as np -import ufl_legacy as ufl def create_reference_data( @@ -31,7 +30,12 @@ def create_reference_data( W = dolfin.VectorFunctionSpace(mesh, family, degree) x = dolfin.SpatialCoordinate(mesh) - f0 = ufl.conditional(ufl.gt(x[0], 0.5), x[1], 2 * x[0]) + # NOTE: f0 must be exactly representable in the (family, degree) function space above. + # Legacy dolfin (FIAT-based quadrature) and current dolfinx (basix-based quadrature) + # use different quadrature rules, so a non-polynomial (e.g. conditional) integrand + # would project to slightly different values in each toolchain, making bit-exact + # comparisons in test_legacy_readers.py inherently flaky across dolfinx versions. + f0 = x[0] ** 2 + 2 * x[1] * x[2] - x[1] v0 = dolfin.project(f0, V) w0 = dolfin.interpolate(dolfin.Expression(("x[0]", "3*x[2]", "7*x[1]"), degree=1), W) diff --git a/tests/test_legacy_readers.py b/tests/test_legacy_readers.py index f8adf92..b567cb7 100644 --- a/tests/test_legacy_readers.py +++ b/tests/test_legacy_readers.py @@ -106,7 +106,9 @@ def test_legacy_function(backend): v = ufl.TestFunction(V) a = ufl.inner(u, v) * ufl.dx x = ufl.SpatialCoordinate(mesh) - f = ufl.conditional(ufl.gt(x[0], 0.5), x[1], 2 * x[0]) + # NOTE: must stay exactly representable in the DG2 space (see tests/create_legacy_data.py) + # so the projection is quadrature-scheme independent when compared to legacy dolfin's. + f = x[0] ** 2 + 2 * x[1] * x[2] - x[1] L = ufl.inner(f, v) * ufl.dx if not dolfinx.has_petsc4py: @@ -152,7 +154,9 @@ def test_read_legacy_function_from_checkpoint(backend): v = ufl.TestFunction(V) a = ufl.inner(u, v) * ufl.dx x = ufl.SpatialCoordinate(mesh) - f = ufl.conditional(ufl.gt(x[0], 0.5), x[1], 2 * x[0]) + # NOTE: must stay exactly representable in the DG2 space (see tests/create_legacy_data.py) + # so the projection is quadrature-scheme independent when compared to legacy dolfin's. + f = x[0] ** 2 + 2 * x[1] * x[2] - x[1] L = ufl.inner(f, v) * ufl.dx if not dolfinx.has_petsc4py: