diff --git a/.github/actions/install_h5py/action.yml b/.github/actions/install_h5py/action.yml index caa37a512..e0a8fb30c 100644 --- a/.github/actions/install_h5py/action.yml +++ b/.github/actions/install_h5py/action.yml @@ -21,6 +21,7 @@ runs: run: | export CC="mpicc" export HDF5_MPI="ON" + pip install 'mpi4py>=4.0.0' pip install h5py --no-cache-dir --no-binary h5py pip list diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index c708fb6f6..6b19b18fb 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -23,9 +23,11 @@ permissions: jobs: build_docs: runs-on: ubuntu-latest + env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN}} OMP_NUM_THREADS: 2 + steps: - name: Checkout uses: actions/checkout@v5 @@ -34,18 +36,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.10' - - name: Install non-Python dependencies on Ubuntu - uses: awalsh128/cache-apt-pkgs-action@latest - with: - packages: gfortran openmpi-bin libopenmpi-dev libhdf5-openmpi-dev - version: 1.0 - execute_install_scripts: true - - name: Reconfigure non-Python dependencies on Ubuntu - run: | - sudo apt-get update - sudo apt-get install --reinstall openmpi-bin libhdf5-openmpi-dev liblapack-dev libblas-dev - sudo apt install graphviz pandoc + - name: Install non-Python dependencies on Ubuntu + uses: ./.github/actions/ubuntu_install - name: Print information on MPI and HDF5 libraries run: | @@ -67,6 +60,10 @@ jobs: pip install .[test] pip freeze + - name: Install non-Python dependencies for Documentation + run: | + sudo apt install graphviz pandoc + - name: Install Python dependencies for Documentation run: | pip install -r docs/requirements.txt diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index f482ff643..2293e912b 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -24,7 +24,8 @@ jobs: fail-fast: false matrix: os: [ ubuntu-24.04, macos-14 ] - python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ] +# python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] + python-version: [ '3.11', '3.12' ] isMerge: - ${{ github.event_name == 'push' && github.ref == 'refs/heads/devel' }} exclude: @@ -123,17 +124,17 @@ jobs: if: matrix.os == 'macos-14' working-directory: ./scratch run: | - coverage report --ignore-errors --show-missing --sort=cover + psydac test --mod psydac.mapping -x -v - name: Run MPI tests with Pytest working-directory: ./scratch run: | - psydac test --mpi + psydac test --mod psydac.fem -x -v - name: Run single-process PETSc tests with Pytest working-directory: ./scratch run: | - psydac test --petsc + psydac test --mod psydac.feec -x -v - name: Run MPI PETSc tests with Pytest working-directory: ./scratch diff --git a/CHANGELOG.md b/CHANGELOG.md index 3546db8cc..ea4c77276 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file. ### Added - #577 : Add an installation configuration option to choose the backend language +- #567 : Improve `psydac test` command (with several new features) +- #565 : Expand editable install info in `README.md` - [DEVELOPER] Create action `install_petsc4py` to install PETSc & `petsc4py` w/ complex support ### Fixed @@ -16,8 +18,7 @@ All notable changes to this project will be documented in this file. - #579 : Return error code on failure of the `psydac test` and `psydac compile` commands - #577 : Fix installation following release of Pyccel 2.2 - #571 : Fix correct application of the sum factorization algorithm -- #570 : Optimize PSYDAC logo -- #565 : Expand editable install info in `README.md` +- #567 : Fix parallel creation of folder `__psydac__` in `psydac.api.fem_bilinear_form` - #566 : Fix command `psydac test --mpi` on Ubuntu machines - [DEVELOPER] Add missing 'description' properties (required!) to our GitHub actions - [DEVELOPER] Update CI installation of `petsc4py` after release of `setuptools` 81.0 @@ -26,11 +27,13 @@ All notable changes to this project will be documented in this file. ### Changed +- #527 : Improve `Geometry` class in module `psydac.cad.geometry` - #580 : Use PETSc 3.25.0 whose Python bindings `petsc4py` install correctly with `setuptools>=81.0` - #579 : Require `pyccel>=2.2.3` which can compile all kernels with C - #579 : Require `numpy>=2.1` to support Python >= 3.10 - #579 : Require `pytest>=9.0` and use `pytest.toml` instead of `pytest.ini` for Pytest configuration - #579 : Move coverage configuration from `pyproject.toml` to `psydac/pytest.toml` +- #570 : Optimize PSYDAC logo - [DEVELOPER] Rename actions: `macos/ubuntu_install` -> `macos/ubuntu_installations` - [DEVELOPER] Do not check file changes to trigger testing workflow on PRs - [DEVELOPER] Run documentation workflow on pushes to `devel` whenever `README.md` is modified diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index a7a1a5e31..98a1967aa 100644 --- a/psydac/api/discretization.py +++ b/psydac/api/discretization.py @@ -578,7 +578,7 @@ def discretize_domain(domain, *, filename=None, ncells=None, periodic=None, comm raise ValueError("Cannot provide both 'filename' and 'ncells'") elif filename: - return Geometry(filename=filename, comm=comm, mpi_dims_mask=mpi_dims_mask) + return Geometry.from_file(filename, comm=comm, mpi_dims_mask=mpi_dims_mask) elif ncells: return Geometry.from_topological_domain(domain, ncells, periodic=periodic, comm=comm, mpi_dims_mask=mpi_dims_mask) diff --git a/psydac/api/tests/test_2d_complex.py b/psydac/api/tests/test_2d_complex.py index 85c2479dc..014ef355f 100644 --- a/psydac/api/tests/test_2d_complex.py +++ b/psydac/api/tests/test_2d_complex.py @@ -363,7 +363,7 @@ def test_complex_poisson_2d_multipatch(): A = Square('A',bounds1=(0, 0.5), bounds2=(0, 1)) B = Square('B',bounds1=(0.5, 1.), bounds2=(0, 1)) - domain = Domain.join([A, B], [((0, 0, 1), (1, 0, -1))], 'domain') + domain = Domain.join([A, B], [((0, 0, 1), (1, 0, -1), 1)], 'domain') x, y = domain.coordinates diff --git a/psydac/api/tests/test_2d_multipatch_mapping_maxwell.py b/psydac/api/tests/test_2d_multipatch_mapping_maxwell.py index 336066c71..2ae7ea7b5 100644 --- a/psydac/api/tests/test_2d_multipatch_mapping_maxwell.py +++ b/psydac/api/tests/test_2d_multipatch_mapping_maxwell.py @@ -7,25 +7,22 @@ from pathlib import Path import pytest -import numpy as np from mpi4py import MPI from sympy import pi, sin, cos, Tuple, Matrix -from sympde.calculus import grad, dot, curl, cross +from sympde.calculus import dot, curl, cross from sympde.calculus import minus, plus from sympde.topology import VectorFunctionSpace from sympde.topology import elements_of from sympde.topology import NormalVector -from sympde.topology import Square, Domain -from sympde.topology import IdentityMapping, PolarMapping +from sympde.topology import Domain from sympde.expr.expr import LinearForm, BilinearForm from sympde.expr.expr import integral from sympde.expr.expr import Norm -from sympde.expr.equation import find, EssentialBC +from sympde.expr.equation import find from psydac.api.discretization import discretize from psydac.api.tests.build_domain import build_11_patch_pretzel, build_2_patch_annulus -from psydac.fem.basic import FemField from psydac.api.settings import PSYDAC_BACKEND_GPYCCEL from psydac.feec.pull_push import pull_2d_hcurl @@ -204,10 +201,10 @@ def teardown_function(): if __name__ == '__main__': - from collections import OrderedDict - from sympy import lambdify + from collections import OrderedDict + from sympy import lambdify from psydac.fem.plotting_utilities import get_plotting_grid, get_grid_vals - from psydac.fem.plotting_utilities import get_patch_knots_gridlines, my_small_plot + from psydac.fem.plotting_utilities import my_small_plot domain = build_11_patch_pretzel() x,y = domain.coordinates diff --git a/psydac/api/tests/test_2d_multipatch_mapping_poisson.py b/psydac/api/tests/test_2d_multipatch_mapping_poisson.py index f730f6bc6..448e1f4c4 100644 --- a/psydac/api/tests/test_2d_multipatch_mapping_poisson.py +++ b/psydac/api/tests/test_2d_multipatch_mapping_poisson.py @@ -15,7 +15,7 @@ from sympde.calculus import minus, plus from sympde.topology import ScalarFunctionSpace from sympde.topology import elements_of -from sympde.topology import NormalVector, Union +from sympde.topology import NormalVector from sympde.topology import Square, Domain from sympde.topology import IdentityMapping, PolarMapping, AffineMapping from sympde.expr.expr import LinearForm, BilinearForm @@ -141,12 +141,12 @@ def test_poisson_2d_3_patches_dirichlet_2(): B = Square('B',bounds1=(0.5, 1.), bounds2=(0, np.pi)) C = Square('C',bounds1=(0.5, 1.), bounds2=(np.pi-0.5, np.pi + 1)) - D1 = mapping_1(A) - D2 = mapping_2(B) - D3 = mapping_3(C) + D1 = mapping_1(A) + D2 = mapping_2(B) + D3 = mapping_3(C) - connectivity = [((0,1,1),(1,1,-1)), ((1,1,1),(2,1,-1))] patches = [D1, D2, D3] + connectivity = [((0, 1, 1), (1, 1,-1), 1), ((1, 1, 1), (2, 1,-1), 1)] domain = Domain.join(patches, connectivity, 'domain') x,y = domain.coordinates @@ -252,8 +252,11 @@ def test_poisson_2d_4_patch_dirichlet_0(): D3 = mapping_3(C) D4 = mapping_4(D) - connectivity = [((0,1,1),(1,1,-1)), ((2,1,1),(3,1,-1)), ((0,0,1),(2,0,-1)),((1,0,1),(3,0,-1))] patches = [D1, D2, D3, D4] + connectivity = [((0, 1, 1), (1, 1,-1), 1), + ((2, 1, 1), (3, 1,-1), 1), + ((0, 0, 1), (2, 0,-1), 1), + ((1, 0, 1), (3, 0,-1), 1)] domain = Domain.join(patches, connectivity, 'domain') x,y = domain.coordinates diff --git a/psydac/api/tests/test_2d_multipatch_poisson.py b/psydac/api/tests/test_2d_multipatch_poisson.py index 297975eba..b4f230ebb 100644 --- a/psydac/api/tests/test_2d_multipatch_poisson.py +++ b/psydac/api/tests/test_2d_multipatch_poisson.py @@ -78,8 +78,8 @@ def test_poisson_2d_2_patch_dirichlet_0(): A = Square('A', bounds1=(0, 0.5), bounds2=(0, 1)) B = Square('B', bounds1=(0.5, 1), bounds2=(0, 1)) - connectivity = [((0,0,1), (1,0,-1), 1)] patches = [A, B] + connectivity = [((0, 0, 1), (1, 0,-1), 1)] domain = Domain.join(patches, connectivity, 'domain') x,y = domain.coordinates @@ -100,8 +100,8 @@ def test_poisson_2d_2_patch_dirichlet_1(): A = Square('A', bounds1=(0, 0.5), bounds2=(0, 1)) B = Square('B', bounds1=(0.5, 1), bounds2=(0, 1)) - connectivity = [((0,0,1), (1,0,-1), 1)] patches = [A, B] + connectivity = [((0, 0, 1), (1, 0,-1), 1)] domain = Domain.join(patches, connectivity, 'domain') x,y = domain.coordinates @@ -121,8 +121,8 @@ def test_poisson_2d_2_patch_dirichlet_2(): A = Square('A', bounds1=(0, 0.5), bounds2=(0, 1)) B = Square('B', bounds1=(0.5, 1), bounds2=(0, 1)) - connectivity = [((0,0,1), (1,0,-1), 1)] patches = [A, B] + connectivity = [((0, 0, 1), (1, 0,-1), 1)] domain = Domain.join(patches, connectivity, 'domain') x,y = domain.coordinates @@ -150,8 +150,8 @@ def test_poisson_2d_2_patch_dirichlet_3(): D1 = M1(A) D2 = M2(B) - connectivity = [((0,0,1), (1,0,1), 1)] patches = [D1, D2] + connectivity = [((0, 0, 1), (1, 0, 1), 1)] domain = Domain.join(patches, connectivity, 'domain') x,y = domain.coordinates @@ -182,8 +182,8 @@ def test_poisson_2d_2_patch_dirichlet_4(): D1 = M1(A) D2 = M2(B) - connectivity = [((0,0,-1), (1,0,-1), 1)] - patches = [D1,D2] + patches = [D1, D2] + connectivity = [((0, 0, -1), (1, 0, -1), 1)] domain = Domain.join(patches, connectivity, 'domain') x,y = domain.coordinates diff --git a/psydac/api/tests/test_postprocessing.py b/psydac/api/tests/test_postprocessing.py index 73cc941f9..b592f2070 100644 --- a/psydac/api/tests/test_postprocessing.py +++ b/psydac/api/tests/test_postprocessing.py @@ -57,7 +57,7 @@ def build_2_mapped_squares(): D2 = mapping_2(B) patches = [D1, D2] - connectivity = [((0,1,1),(1,1,-1))] + connectivity = [((0, 1, 1), (1, 1,-1), 1)] return Domain.join(patches, connectivity, 'domain') @@ -66,7 +66,7 @@ def build_2_squares(): B = Square('B',bounds1=(0.5, 1.), bounds2=(np.pi/2, np.pi)) patches = [A, B] - connectivity = [((0,1,1),(1,1,-1))] + connectivity = [((0, 1, 1), (1, 1,-1), 1)] return Domain.join(patches, connectivity, 'domain') @@ -75,10 +75,9 @@ def build_2_cubes(): B = Cube('B',bounds1=(0.5, 1.), bounds2=(np.pi/2, np.pi), bounds3=(0, 1)) patches = [A, B] - connectivity = [((0,1,1),(1,1,-1))] + connectivity = [((0, 1, 1), (1, 1,-1), (1, 1, 1))] return Domain.join(patches, connectivity, 'domain') - ############################################################################### # Output Manager tests # ############################################################################### @@ -538,8 +537,8 @@ def test_reconstruct_multipatch(dtype): A = Square('A',bounds1=bounds1, bounds2=bounds2_A) B = Square('B',bounds1=bounds1, bounds2=bounds2_B) - connectivity = [((0,1,1),(1,1,-1))] - patches = [A,B] + patches = [A, B] + connectivity = [((0, 1, 1), (1, 1,-1), 1)] domain = Domain.join(patches, connectivity, 'domain') Va = ScalarFunctionSpace('Va', A) diff --git a/psydac/cad/geometry.py b/psydac/cad/geometry.py index cf89c85d1..5889231f0 100644 --- a/psydac/cad/geometry.py +++ b/psydac/cad/geometry.py @@ -8,20 +8,19 @@ # the topology i.e. connectivity, boundaries # For the moment, it is used as a container, that can be loaded from a file # (hdf5) -from itertools import product -from collections import abc -import string -import random -import yaml import os -import string -import random -import warnings +from typing import Iterable +from itertools import chain import numpy as np import h5py +import yaml from mpi4py import MPI +from sympde.topology import Domain, Interface, Line, Square, Cube, NCubeInterior, Mapping, NCube +from sympde.topology.basic import Union +from sympde.topology.callable_mapping import BasicCallableMapping + from psydac.fem.splines import SplineSpace from psydac.fem.tensor import TensorFemSpace from psydac.fem.partitioning import create_cart, construct_connectivity, construct_interface_spaces @@ -29,39 +28,48 @@ from psydac.linalg.block import BlockVectorSpace, BlockVector from psydac.ddm.cart import DomainDecomposition, MultiPatchDomainDecomposition +__all__ = ( + 'Geometry', + 'export_nurbs_to_hdf5', + 'import_geopdes_to_nurbs', + 'refine_knots', + 'refine_nurbs', +) -from sympde.topology import Domain, Interface, Line, Square, Cube, NCubeInterior, Mapping, NCube -from sympde.topology.basic import Union +NoneType = type(None) #============================================================================== class Geometry: """ Distributed discrete geometry that works for single and multiple patches. - The Geometry object can be created in two ways: - - case 1 : through a geometry file whos name can be given to the constructor - - case 2 : provide the ncells, the periodicity and the mapping objects of each patch. + + The Geometry object can be created in four ways: + - case 0 : providing a `Domain` to `__init__` with detailed parameters for each patch. + - case 1 : passing the path to a geometry file to `from_file`. + - case 2 : passing a `SplineMapping` to `from_discrete_mapping` (single patch). + - case 3 : passing a `Domain`, ncells, and periodicity to `from_topological_domain` (single or multi-patch). Parameters ---------- domain : Sympde.topology.Domain - The symbolic domain to be discretized. + The symbolic topological domain to be discretized. - ncells : list | tuple | dict - The number of cells of the discretized topological domain in each direction. + pdim : int + Number of physical dimensions of the Geometry object (pdim >= ldim). - periodic : list | tuple | dict - The periodicity of the topological domain in each direction. + ncells : dict[str, Iterable[int]] + The number of cells of the discretized domain in each direction. - mappings : dict - The Mapping of each patch. + periodic : dict[str, Iterable[bool]], optional + The periodicity of the topological domain in each direction. - filename: str - The path to the geometry file. + mappings : dict[str, BasicCallableMapping], optional + The discrete mappings of each patch. - comm: MPI.Comm + comm: MPI.Intracomm, optional MPI intra-communicator. - - mpi_dims_mask: list of bool + + mpi_dims_mask: Iterable[bool], optional True if the dimension is to be used in the domain decomposition (=default for each dimension). If mpi_dims_mask[i]=False, the i-th dimension will not be decomposed. @@ -71,68 +79,139 @@ class Geometry: _patches = [] _topology = None + def __init__(self, + domain : Domain, + *, + pdim : int, + ncells : dict[str, Iterable[int]], + mappings : dict[str, SplineMapping | None] = None, + periodic : dict[str, Iterable[bool]] = None, + comm : MPI.Intracomm = None, + mpi_dims_mask : Iterable[bool] = None): + + # Type checks + assert isinstance(pdim, int) + assert isinstance(domain, Domain) + assert isinstance(ncells, dict) + assert isinstance(mappings, dict) + assert isinstance(periodic, (NoneType, dict)) + assert isinstance(comm, (NoneType, MPI.Intracomm)) + assert isinstance(mpi_dims_mask, (NoneType, Iterable)) + + # Extract info from domain + ldim : int = domain.dim + interior_names : list = domain.interior_names + set_interior_names = set(interior_names) + + # Check sanity of pdim + assert pdim >= ldim + + # Check sanity of ncells + assert set(ncells.keys()) == set_interior_names + assert all(len(n) == ldim for n in ncells.values()) + assert all(isinstance(ni, (int, np.integer)) for ni in chain(*ncells.values())) + assert all(ni > 0 for ni in chain(*ncells.values())) + + # Although we allow the iterable values in ncells to contain NumPy + # integers, we convert them to lists of Python integers for consistency + ncells = {patch: [int(ni) for ni in n] for patch, n in ncells.items()} + + # Check sanity of periodic + if periodic is None: + periodic = {patch: [False] * len(n) for patch, n in ncells.items()} + else: + assert set(periodic.keys()) == set_interior_names + assert all(len(p) == ldim for p in periodic.values()) + assert all(isinstance(pi, bool) for pi in chain(*periodic.values())) + + # Check sanity of mappings + if mappings is None: + mappings = {itr.name : None for itr in domain.interior} + else: + assert set(mappings.keys()) == set_interior_names + assert all(isinstance(m, (BasicCallableMapping, NoneType)) for m in mappings.values()) + assert all(m.pdim == pdim for m in mappings.values() if m is not None) + + # Check sanity of mpi_dims_mask + if mpi_dims_mask is not None: + assert len(mpi_dims_mask) == ldim + assert all(isinstance(mask, bool) for mask in mpi_dims_mask) + + # Create a (multi-patch) domain decomposition + if len(domain) == 1: + #name = domain.name + name = interior_names[0] + ddm = DomainDecomposition( + ncells = ncells[name], + periods = periodic[name], + comm = comm, + mpi_dims_mask = mpi_dims_mask, + ) + else: + ddm = MultiPatchDomainDecomposition( + ncells = [ ncells[itr] for itr in interior_names], + periods = [periodic[itr] for itr in interior_names], + comm = comm, + ) + + # Add attributes to the new object + self._domain = domain + self._ldim = domain.dim + self._pdim = pdim + self._ncells = ncells + self._mappings = mappings + self._periodic = periodic + self._comm = comm + self._ddm = ddm + self._cart = None + #-------------------------------------------------------------------------- - # Option [1]: from a (domain, mappings) or a file + # Option [1]: from a file #-------------------------------------------------------------------------- - def __init__(self, domain=None, ncells=None, periodic=None, mappings=None, - filename=None, comm=None, mpi_dims_mask=None): - - # ... read the geometry if the filename is given - if filename is not None: - self.read(filename, comm=comm, mpi_dims_mask=mpi_dims_mask) - - elif domain is not None: - assert isinstance(domain, Domain) - assert isinstance(ncells, dict) - assert isinstance(mappings, dict) - if periodic is not None: - assert isinstance(periodic, dict) - - # ... check sanity - interior_names = domain.interior_names - mappings_keys = sorted(list(mappings.keys())) - - assert sorted(interior_names) == mappings_keys - # ... - - if periodic is None: - periodic = {patch: [False]*len(ncells_i) for patch, ncells_i in ncells.items()} - - self._domain = domain - self._ldim = domain.dim - self._pdim = domain.dim # TODO must be given => only dim is defined for a Domain - self._ncells = ncells - self._periodic = periodic - self._mappings = mappings - self._cart = None - self._is_parallel = comm is not None - - if len(domain) == 1: - #name = domain.name - name = interior_names[0] - self._ddm = DomainDecomposition(ncells[name], periodic[name], comm=comm, mpi_dims_mask=mpi_dims_mask) - else: - ncells = [ncells[itr] for itr in interior_names] - periodic = [periodic[itr] for itr in interior_names] - self._ddm = MultiPatchDomainDecomposition(ncells, periodic, comm=comm) + @classmethod + def from_file(cls, + filename : str, + *, + comm : MPI.Intracomm = None, + mpi_dims_mask : Iterable[bool] = None): - else: - raise ValueError('Wrong input') - # ... + """ + Create a Geometry instance from an HDF5 input file in Psydac format. + + Parameters + ---------- + filename: str + The path to the geometry file. - self._comm = comm + comm: MPI.Intracomm, optional + The MPI intra-communicator. + + mpi_dims_mask: Iterable[bool], optional + True if the dimension is to be used in the domain decomposition + (=default for each dimension). If mpi_dims_mask[i]=False, the i-th + dimension will not be decomposed. + + Returns + ------- + Geometry + The new instance. + """ + geo = super().__new__(cls) + geo.read(filename, comm=comm, mpi_dims_mask=mpi_dims_mask) + return geo #-------------------------------------------------------------------------- # Option [2]: from a discrete mapping #-------------------------------------------------------------------------- @classmethod def from_discrete_mapping(cls, mapping, *, comm=None, mpi_dims_mask=None, name=None): - """Create a geometry from one discrete mapping. + """ + Create a single-patch Geometry instance from one discrete mapping. Parameters ---------- - mapping : SplineMapping - The Mapping from the unit square to the physical domain. + mapping : BasicCallableMapping + The mapping from the unit square to the physical domain. comm : MPI.Comm MPI intra-communicator. @@ -141,62 +220,80 @@ def from_discrete_mapping(cls, mapping, *, comm=None, mpi_dims_mask=None, name=N True if the dimension is to be used in the domain decomposition (=default for each dimension). If mpi_dims_mask[i]=False, the i-th dimension will not be decomposed. - name : string - Optional name for the Mapping that will be created. - Needed to avoid conflicts in case several mappings are created + name : str + Optional name for the symbolic Mapping that will be created. + Needed to avoid conflicts in case several mappings are created. + + Returns + ------- + Geometry + The new instance. """ mapping_name = name if name else 'mapping' - dim = mapping.ldim - M = Mapping(mapping_name, dim = dim) + dim = mapping.ldim + M = Mapping(mapping_name, dim = dim) # this is a symbolic mapping domain = M(NCube(name = 'Omega', dim = dim, min_coords = [0.] * dim, max_coords = [1.] * dim)) M.set_callable_mapping(mapping) + pdim = mapping.pdim mappings = {domain.name: mapping} ncells = {domain.name: mapping.space.domain_decomposition.ncells} periodic = {domain.name: mapping.space.domain_decomposition.periods} - return Geometry(domain=domain, ncells=ncells, periodic=periodic, mappings=mappings, comm=comm, mpi_dims_mask=mpi_dims_mask) - + return Geometry(domain = domain, + pdim = pdim, + ncells = ncells, + periodic = periodic, + mappings = mappings, + comm = comm, + mpi_dims_mask = mpi_dims_mask) #-------------------------------------------------------------------------- # Option [3]: discrete topological line/square/cube #-------------------------------------------------------------------------- @classmethod def from_topological_domain(cls, domain, ncells, *, periodic=None, comm=None, mpi_dims_mask=None): + assert isinstance(domain, Domain) + interior = domain.interior if not isinstance(interior, Union): interior = [interior] for itr in interior: if not isinstance(itr, NCubeInterior): - msg = "Topological domain must be an NCube;"\ + msg = "The topological domain of each patch must be an NCube;"\ " got {} instead.".format(type(itr)) raise TypeError(msg) - mappings = {itr.name:None for itr in interior} + mappings = {itr.name : None for itr in interior} + pdim = next(iter(interior)).dim if isinstance(ncells, (list, tuple)): - ncells = {itr.name:ncells for itr in interior} + ncells = {itr.name : ncells for itr in interior} if periodic is None: - periodic = [False]*domain.dim + periodic = [False] * domain.dim else: if len(interior) > 1 and True in periodic: + import warnings msg = "Discretizing a multipatch domain with a periodic flag is not advised -- continue at your own risk." # [MCP 18.12.2025] the following line may be causing a strange error in the CI (MPI tests for macos-14/Python 3.10) # warnings.warn(msg, Warning) warnings.warn(msg, UserWarning) - if isinstance(periodic, (list, tuple)): - periodic = {itr.name:periodic for itr in interior} - - geo = Geometry(domain=domain, mappings=mappings, ncells=ncells, periodic=periodic, comm=comm, mpi_dims_mask=mpi_dims_mask) + periodic = {itr.name : periodic for itr in interior} - return geo + return Geometry(domain = domain, + pdim = pdim, + ncells = ncells, + periodic = periodic, + mappings = mappings, + comm = comm, + mpi_dims_mask = mpi_dims_mask) #-------------------------------------------------------------------------- @property @@ -227,10 +324,6 @@ def domain(self): def ddm(self): return self._ddm - @property - def is_parallel(self): - return self._is_parallel - @property def mappings(self): return self._mappings @@ -240,8 +333,8 @@ def __len__(self): def read(self, filename, comm=None, mpi_dims_mask=None): # ... check extension of the file - basename, ext = os.path.splitext(filename) - if not(ext == '.h5'): + _, ext = os.path.splitext(filename) + if ext != '.h5': raise ValueError('> Only h5 files are supported') # ... @@ -249,38 +342,36 @@ def read(self, filename, comm=None, mpi_dims_mask=None): domain = Domain.from_file(filename) connectivity = construct_connectivity(domain) - if len(domain)==1: - interiors = [domain.interior] + if len(domain) == 1: + interiors = [domain.interior] else: - interiors = list(domain.interior.args) - - if not(comm is None): - kwargs = dict( driver='mpio', comm=comm ) if comm.size > 1 else {} + interiors = list(domain.interior.args) + if comm is not None: + kwargs = dict(driver='mpio', comm=comm) if comm.size > 1 else {} else: kwargs = {} - h5 = h5py.File( filename, mode='r', **kwargs ) - yml = yaml.load( h5['geometry.yml'][()], Loader=yaml.SafeLoader ) + h5 = h5py.File(filename, mode='r', **kwargs) + yml = yaml.load(h5['geometry.yml'][()], Loader=yaml.SafeLoader) ldim = yml['ldim'] pdim = yml['pdim'] - n_patches = len( yml['patches'] ) + n_patches = len(yml['patches']) # ... if n_patches == 0: - h5.close() - raise ValueError( "Input file contains no patches." ) + raise ValueError("Input file contains no patches.") # ... - # ... read patchs + # ... read patches mappings = {} ncells = {} periodic = {} - spaces = [None]*n_patches - for i_patch in range( n_patches ): + spaces = [None] * n_patches + for i_patch in range(n_patches): item = yml['patches'][i_patch] patch_name = item['name'] @@ -291,38 +382,36 @@ def read(self, filename, comm=None, mpi_dims_mask=None): degree = [int (p) for p in patch.attrs['degree' ]] periodic_i = [bool(b) for b in patch.attrs['periodic']] - knots = [patch['knots_{}'.format(d)][:] for d in range( ldim )] - space_i = [SplineSpace( degree=p, knots=k, periodic=P ) - for p,k,P in zip( degree, knots, periodic_i )] + knots = [patch['knots_{}'.format(d)][:] for d in range(ldim)] + space_i = [SplineSpace(degree=p, knots=k, periodic=P) + for p, k, P in zip(degree, knots, periodic_i)] spaces[i_patch] = space_i ncells [interiors[i_patch].name] = [sp.ncells for sp in space_i] periodic[interiors[i_patch].name] = periodic_i - self._cart = None if n_patches == 1: - self._ddm = DomainDecomposition(ncells[domain.name], periodic[domain.name], comm=comm, mpi_dims_mask=mpi_dims_mask) - ddms = [self._ddm] + ddm = DomainDecomposition(ncells[domain.name], periodic[domain.name], comm=comm, mpi_dims_mask=mpi_dims_mask) + ddms = [ddm] else: - ncells_ = [ncells[itr.name] for itr in interiors] - periodic = [periodic[itr.name] for itr in interiors] - self._ddm = MultiPatchDomainDecomposition(ncells_, periodic, comm=comm) - ddms = self._ddm.domains + ncells_ = [ncells[itr.name] for itr in interiors] + periodic = [periodic[itr.name] for itr in interiors] + ddm = MultiPatchDomainDecomposition(ncells_, periodic, comm=comm) + ddms = ddm.domains carts = create_cart(ddms, spaces) - g_spaces = {inter:TensorFemSpace( ddms[i], *spaces[i], cart=carts[i]) for i,inter in enumerate(interiors)} + g_spaces = {inter:TensorFemSpace(ddms[i], *spaces[i], cart=carts[i]) for i,inter in enumerate(interiors)} - for i,j in connectivity: - ((axis_i, ext_i), (axis_j , ext_j)) = connectivity[i, j] + for i, j in connectivity: minus = interiors[i] plus = interiors[j] - max_ncells = [max(ni,nj) for ni,nj in zip(ncells[minus.name],ncells[plus.name])] + max_ncells = [max(ni, nj) for ni, nj in zip(ncells[minus.name], ncells[plus.name])] g_spaces[minus].add_refined_space(ncells=max_ncells) - g_spaces[plus].add_refined_space(ncells=max_ncells) + g_spaces[plus ].add_refined_space(ncells=max_ncells) # ... construct interface spaces - construct_interface_spaces(self._ddm, g_spaces, carts, interiors, connectivity) + construct_interface_spaces(ddm, g_spaces, carts, interiors, connectivity) for i_patch in range( n_patches ): @@ -336,26 +425,26 @@ def read(self, filename, comm=None, mpi_dims_mask=None): tensor_space = g_spaces[interiors[i_patch]] if dtype == 'SplineMapping': - mapping = SplineMapping.from_control_points( tensor_space, - patch['points'][..., :pdim] ) + mapping = SplineMapping.from_control_points(tensor_space, + patch['points'][..., :pdim]) elif dtype == 'NurbsMapping': - mapping = NurbsMapping.from_control_points_weights( tensor_space, - patch['points'][..., :pdim], - patch['weights'] ) + mapping = NurbsMapping.from_control_points_weights(tensor_space, + patch['points'][..., :pdim], + patch['weights']) - mapping.set_name( item['name'] ) + mapping.set_name(item['name']) mappings[patch_name] = mapping - if n_patches>1: - coeffs = [[e._coeffs for e in mapping._fields] for mapping in mappings.values()] - spaces = [[coeffs_ij.space for coeffs_ij in coeffs_i] for coeffs_i in coeffs] - spaces = [BlockVectorSpace(*space) for space in spaces] - w_spaces = [sp.spaces[0] for sp in spaces] - space = BlockVectorSpace(*spaces, connectivity=connectivity) - w_space = BlockVectorSpace(*w_spaces, connectivity=connectivity) - v = BlockVector(space) - w = BlockVector(w_space) + # ... Update ghost regions within each patch and across interfaces + if n_patches > 1: + coeffs = [[e.coeffs for e in mapping.fields] for mapping in mappings.values()] + patch_spaces = [BlockVectorSpace(*[c_ij.space for c_ij in c_i]) for c_i in coeffs] + patch_spaces_w = [c_i[0].space for c_i in coeffs] + space = BlockVectorSpace(*patch_spaces , connectivity=connectivity) + space_w = BlockVectorSpace(*patch_spaces_w, connectivity=connectivity) + v = BlockVector(space) + w = BlockVector(space_w) mapping_list = list(mappings.values()) for i in range(n_patches): for j in range(len(coeffs[i])): @@ -377,7 +466,7 @@ def read(self, filename, comm=None, mpi_dims_mask=None): if isinstance(mapping, NurbsMapping): mapping.weights_field.coeffs.update_ghost_regions() - + # ... # ... close the h5 file h5.close() @@ -389,14 +478,15 @@ def read(self, filename, comm=None, mpi_dims_mask=None): patch.mapping.set_callable_mapping(F) # ... + self._domain = domain self._ldim = ldim self._pdim = pdim - self._mappings = mappings - self._domain = domain - self._comm = comm self._ncells = ncells + self._mappings = mappings self._periodic = periodic - self._is_parallel = comm is not None + self._comm = comm + self._ddm = ddm + self._cart = None # ... def export( self, filename ): @@ -437,7 +527,6 @@ def export( self, filename ): yml['patches'] = patches_info # ... - # ... topology topo_yml = self.domain.todict() # ... diff --git a/psydac/cad/mesh/multipatch/magnet.h5 b/psydac/cad/mesh/multipatch/magnet.h5 index c18c688ec..9e497c2a0 100644 Binary files a/psydac/cad/mesh/multipatch/magnet.h5 and b/psydac/cad/mesh/multipatch/magnet.h5 differ diff --git a/psydac/cad/mesh/multipatch/square.h5 b/psydac/cad/mesh/multipatch/square.h5 index ae3f87a27..781c05feb 100644 Binary files a/psydac/cad/mesh/multipatch/square.h5 and b/psydac/cad/mesh/multipatch/square.h5 differ diff --git a/psydac/cad/mesh/multipatch/square_repeated_knots.h5 b/psydac/cad/mesh/multipatch/square_repeated_knots.h5 index 471b157f3..a17343290 100644 Binary files a/psydac/cad/mesh/multipatch/square_repeated_knots.h5 and b/psydac/cad/mesh/multipatch/square_repeated_knots.h5 differ diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index 6671d1870..18ad343b1 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -25,6 +25,7 @@ base_dir = os.path.dirname(os.path.realpath(__file__)) #============================================================================== +@pytest.mark.xdist_group('h5py') def test_geometry_2d_1(): ncells = [1,1] @@ -43,13 +44,13 @@ def test_geometry_2d_1(): ncells = {domain.name:ncells} # create a geometry from a topological domain and the dict of mappings - geo = Geometry(domain=domain, ncells=ncells, mappings=mappings) + geo = Geometry(domain=domain, pdim=2, ncells=ncells, mappings=mappings) # export the geometry geo.export('geo.h5') # read it again - geo_0 = Geometry(filename='geo.h5') + geo_0 = Geometry.from_file('geo.h5') # export it again geo_0.export('geo_0.h5') @@ -61,6 +62,7 @@ def test_geometry_2d_1(): geo_1.export('geo_1.h5') #============================================================================== +@pytest.mark.xdist_group('h5py') def test_geometry_2d_2(): # create a nurbs mapping @@ -92,13 +94,13 @@ def test_geometry_2d_2(): periodic = {domain.name:[space.periodic for space in mapping.space.spaces]} # create a geometry from a topological domain and the dict of mappings - geo = Geometry(domain=domain, ncells=ncells, periodic=periodic, mappings=mappings) + geo = Geometry(domain=domain, pdim=2, ncells=ncells, periodic=periodic, mappings=mappings) # export the geometry geo.export('quart_circle.h5') # read it again - geo_0 = Geometry(filename='quart_circle.h5') + geo_0 = Geometry.from_file('quart_circle.h5') # export it again geo_0.export('quart_circle_0.h5') @@ -111,6 +113,7 @@ def test_geometry_2d_2(): #============================================================================== # TODO to be removed +@pytest.mark.xdist_group('h5py') def test_geometry_2d_3(): # create a nurbs mapping @@ -144,6 +147,7 @@ def test_geometry_2d_3(): #============================================================================== # TODO to be removed +@pytest.mark.xdist_group('h5py') def test_geometry_2d_4(): # create a nurbs mapping @@ -204,11 +208,11 @@ def test_geometry_with_mpi_dims_mask(): # Create a geometry from a topological domain and the dict of mappings # Here we allow for any distribution of the domain: mpi_dims_mask is not passed - geo = Geometry(domain=domain, ncells=d_ncells, mappings=mappings, comm=comm) + geo = Geometry(domain=domain, pdim=3, ncells=d_ncells, mappings=mappings, comm=comm) geo.export('geo_mpi_dims.h5') # Read geometry file in parallel, but using mpi_dims_mask - geo_from_file = Geometry(filename='geo_mpi_dims.h5', comm=comm, mpi_dims_mask=mpi_dims_mask) + geo_from_file = Geometry.from_file(filename='geo_mpi_dims.h5', comm=comm, mpi_dims_mask=mpi_dims_mask) # Verify that the domain is distributed as expected assert geo_from_file.ddm.starts == expected_starts @@ -219,7 +223,6 @@ def test_geometry_with_mpi_dims_mask(): if rank == 0: os.remove('geo_mpi_dims.h5') - # ============================================================================== @pytest.mark.mpi def test_from_discrete_mapping(): @@ -227,7 +230,7 @@ def test_from_discrete_mapping(): comm = MPI.COMM_WORLD rank = comm.rank size = comm.size - mpi_dims_mask = [False, False, True] # We swill verify that this has an effect + mpi_dims_mask = [False, False, True] # We will verify that this has an effect ncells = [4, 8, 2 * size] # Each process should have two cells along x3 degree = [3, 3, 3] @@ -271,6 +274,7 @@ def test_from_topological_domain(): #============================================================================== @pytest.mark.parametrize( 'ncells', [[8,8], [12,12], [14,14]] ) @pytest.mark.parametrize( 'degree', [[2,2], [3,2], [2,3], [3,3], [4,4]] ) +@pytest.mark.xdist_group('h5py') def test_export_nurbs_to_hdf5(ncells, degree): # create pipe geometry @@ -288,7 +292,7 @@ def test_export_nurbs_to_hdf5(ncells, degree): export_nurbs_to_hdf5(filename, new_pipe) # read the geometry - geo = Geometry(filename=filename) + geo = Geometry.from_file(filename) domain = geo.domain min_coords = domain.logical_domain.min_coords @@ -324,9 +328,9 @@ def test_export_nurbs_to_hdf5(ncells, degree): #============================================================================== @pytest.mark.parametrize( 'ncells', [[8,8], [12,12], [14,14]] ) @pytest.mark.parametrize( 'degree', [[2,2], [3,2], [2,3], [3,3], [4,4]] ) +@pytest.mark.xdist_group('h5py') def test_import_geopdes_to_nurbs(ncells, degree): - filename = os.path.join(base_dir, "geo_Lshaped_C1.txt") L_shaped = import_geopdes_to_nurbs(filename) @@ -337,7 +341,7 @@ def test_import_geopdes_to_nurbs(ncells, degree): export_nurbs_to_hdf5(filename, L_shaped) # read the geometry - geo = Geometry(filename=filename) + geo = Geometry.from_file(filename) domain = geo.domain min_coords = domain.logical_domain.min_coords diff --git a/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py index cf64ecf27..1c45c4b33 100644 --- a/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py +++ b/psydac/feec/tests/test_feec_conf_projectors_cart_2d.py @@ -43,7 +43,7 @@ def get_polynomial_function(degree, hom_bc_axes, domain): g0_y = (y - 0.75)**degree[1] expr = g0_x * g0_y - callable_function = lambdify(domain.coordinates, expr) + callable_function = lambdify(domain.coordinates, expr, modules=['numpy']) return expr, callable_function diff --git a/psydac/fem/tests/test_plot_field_2d.py b/psydac/fem/tests/test_plot_field_2d.py index a099aee02..bf870bdf7 100644 --- a/psydac/fem/tests/test_plot_field_2d.py +++ b/psydac/fem/tests/test_plot_field_2d.py @@ -46,16 +46,16 @@ def test_plot_field(use_scalar_field, use_multipatch): degree = [2, 2] A = Square('A',bounds1=(0.5, 1.), bounds2=(0, np.pi/2)) - mapping_1 = PolarMapping('M1',2, c1= 0., c2= 0., rmin = 0., rmax=1.) + mapping_1 = PolarMapping('M1', 2, c1= 0., c2= 0., rmin = 0., rmax=1.) D1 = mapping_1(A) if use_multipatch: B = Square('B',bounds1=(0.5, 1.), bounds2=(np.pi/2, np.pi)) - mapping_2 = PolarMapping('M2',2, c1= 0., c2= 0., rmin = 0., rmax=1.) - D2 = mapping_2(B) + mapping_2 = PolarMapping('M2', 2, c1= 0., c2= 0., rmin = 0., rmax=1.) + D2 = mapping_2(B) - connectivity = [((0,1,1),(1,1,-1))] - patches = [D1,D2] + patches = [D1, D2] + connectivity = [((0, 1, 1), (1, 1,-1), 1)] domain = Domain.join(patches, connectivity, 'domain') else: domain = D1 diff --git a/psydac/mapping/discrete_gallery.py b/psydac/mapping/discrete_gallery.py index 5075d8c85..e7a9f2820 100644 --- a/psydac/mapping/discrete_gallery.py +++ b/psydac/mapping/discrete_gallery.py @@ -3,6 +3,8 @@ # LICENSE file or go to https://github.com/pyccel/psydac/blob/devel/LICENSE # # for full license details. # #---------------------------------------------------------------------------# +from typing import Iterable + import numpy as np from mpi4py import MPI @@ -33,6 +35,11 @@ 'spherical_shell', ) +__all__ = ( + 'get_available_mappings', + 'discrete_mapping', +) + class Collela3D( Mapping ): _expressions = {'x':'2.*(x1 + 0.1*sin(2.*pi*x1)*sin(2.*pi*x2)) - 1.', @@ -40,78 +47,143 @@ class Collela3D( Mapping ): 'z':'2.*x3 - 1.'} #============================================================================== -def discrete_mapping(mapping, ncells, degree, **kwargs): +def get_available_mappings(ldim): + """ + Get a list of `mapping` values accepted as argument to `discrete_mapping`. + + Parameters + ---------- + ldim : int + The number of logical dimensions of the topological domain. + + Returns + ------- + tuple + All the accepted values for the `mapping` parameter. + """ + assert isinstance(ldim, int), f'ldim must be int, got {type(ldim).__name__} instead' + assert ldim > 0, f'ldim must be > 0, got {ldim} instead' + + if ldim == 2: + return ('identity', 'collela', 'circle', 'annulus', 'quarter_annulus', + 'target', 'czarny') + elif ldim == 3: + return ('identity', 'collela', 'spherical_shell') + else: + return () - comm = kwargs.pop('comm', MPI.COMM_WORLD) - return_space = kwargs.pop('return_space', False) +#============================================================================== +def discrete_mapping(mapping, ncells, degree, *, + comm = MPI.COMM_WORLD, + return_space = False): + """ + Create a SplineMapping by interpolating one of the available analytical mappings. + + Parameters + ---------- + mapping : str + The name of the mapping. See `available_mappings` to get the options. + + ncells : Iterable[int] + The number of cells along each logical dimension. + + degree : Iterable[int] + The spline degree along each logical dimension. + + comm : MPI.Intracomm, optional + The MPI intracommunicator. + + return_space : bool, optional + Whether this function should also return the discrete space it creates. + + Returns + ------- + map_discrete : SplineMapping + The spline mapping created. + + space : TensorFemSpace + The space of the components of the spline mapping. + Only returned if `return_space` is True. + """ + # Check types + assert isinstance(mapping, str) + assert isinstance(ncells, Iterable) + assert isinstance(degree, Iterable) + assert isinstance(comm, MPI.Intracomm) or comm is None + assert isinstance(return_space, bool) + + # Check consistency of ncells and degree + assert all(isinstance(n, int) and n >= 1 for n in ncells) + assert all(isinstance(d, int) and d >= 0 for d in degree) + assert len(ncells) == len(degree) mapping = mapping.lower() - dim = len(ncells) - if dim not in [2, 3]: + ldim = len(ncells) + if ldim not in [2, 3]: raise NotImplementedError('Only 2D and 3D mappings are available') # ... - if dim == 2: + if ldim == 2: # Input parameters if mapping == 'identity': - map_symbolic = IdentityMapping('M', dim=dim) + map_symbolic = IdentityMapping('M', dim=ldim) limits = ((0, 1), (0, 1)) periodic = (False, False) elif mapping == 'collela': default_params = dict(k1=1.0, k2=1.0, eps=0.1) - map_symbolic = CollelaMapping2D('M', dim=dim, **default_params) + map_symbolic = CollelaMapping2D('M', dim=ldim, **default_params) limits = ((0, 1), (0, 1)) periodic = (False, False) elif mapping == 'circle': default_params = dict(rmin=0.0, rmax=1.0, c1=0.0, c2=0.0) - map_symbolic = PolarMapping('M', dim=dim, **default_params) + map_symbolic = PolarMapping('M', dim=ldim, **default_params) limits = ((0, 1), (0, 2*np.pi)) periodic = (False, True) elif mapping == 'annulus': default_params = dict(rmin=0.0, rmax=1.0, c1=0.0, c2=0.0) - map_symbolic = PolarMapping('M', dim=dim, **default_params) + map_symbolic = PolarMapping('M', dim=ldim, **default_params) limits = ((1, 4), (0, 2*np.pi)) periodic = (False, True) elif mapping == 'quarter_annulus': default_params = dict(rmin=0.0, rmax=1.0, c1=0.0, c2=0.0) - map_symbolic = PolarMapping('M', dim=dim, **default_params) + map_symbolic = PolarMapping('M', dim=ldim, **default_params) limits = ((1, 4), (0, np.pi/2)) periodic = (False, False) elif mapping == 'target': default_params = dict(c1=0, c2=0, k=0.3, D=0.2) - map_symbolic = TargetMapping('M', dim=dim, **default_params) + map_symbolic = TargetMapping('M', dim=ldim, **default_params) limits = ((0, 1), (0, 2*np.pi)) periodic = (False, True) elif mapping == 'czarny': default_params = dict(c2=0, b=1.4, eps=0.3) - map_symbolic = CzarnyMapping('M', dim=dim, **default_params) + map_symbolic = CzarnyMapping('M', dim=ldim, **default_params) limits = ((0, 1), (0, 2*np.pi)) periodic = (False, True) else: raise ValueError("Required 2D mapping not available") - elif dim == 3: + elif ldim == 3: # Input parameters if mapping == 'identity': - map_symbolic = IdentityMapping('M', dim=dim) + map_symbolic = IdentityMapping('M', dim=ldim) limits = ((0, 1), (0, 1), (0, 1)) periodic = ( False, False, False) elif mapping == 'collela': - map_symbolic = Collela3D('M', dim=dim) + map_symbolic = Collela3D('M', dim=ldim) limits = ((0, 1), (0, 1), (0, 1)) periodic = ( False, False, False) elif mapping == 'spherical_shell': - map_symbolic = SphericalMapping('M', dim=dim) + map_symbolic = SphericalMapping('M', dim=ldim) limits = ((1, 4), (0, np.pi), (0, np.pi/2)) periodic = ( False, False, False) diff --git a/pyproject.toml b/pyproject.toml index 2a47c1260..e0ae1db32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,8 @@ dependencies = [ 'termcolor', # Our packages from PyPi - 'sympde == 0.19.2', +# 'sympde == 0.19.2', + 'sympde @ https://github.com/pyccel/sympde/archive/refs/heads/clean-multipatch-domain.zip', 'pyccel >= 2.2.3', 'gelato == 0.12',