From 461ae4ecedea7987b01fd0ff03edacb7706f07d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Thu, 18 Sep 2025 15:27:28 +0200 Subject: [PATCH 01/70] Allow `mpi_dims_mask` with geometry file --- psydac/api/discretization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index 1cea9397e..793ebd9f7 100644 --- a/psydac/api/discretization.py +++ b/psydac/api/discretization.py @@ -535,7 +535,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) + return Geometry(filename=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) From 1e46072d3152352ca396928f0f0a68b0f8866687 Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Fri, 19 Sep 2025 08:21:23 +0200 Subject: [PATCH 02/70] unit test for mpi_dims_mask --- psydac/cad/tests/test_geometry.py | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index 0f0f1a93d..10e24f0a8 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -169,6 +169,41 @@ def test_geometry_2d_4(): # export it geo.export('circle.h5') +#============================================================================== +@pytest.mark.parallel +def test_geometry_with_mpi_dims_mask(): + from mpi4py import MPI + comm = MPI.COMM_WORLD + rank = comm.Get_rank() + mpi_dims_mask = [False, True] + ncells = [4, 4] + degree = [2, 2] + # create an identity mapping + mapping = discrete_mapping('identity', ncells=ncells, degree=degree) + + # create a topological domain + F = Mapping('F', dim=2) + domain = F(Square(name='Omega')) + + # associate the mapping to the topological domain + mappings = {domain.name: mapping} + + # Define ncells as a dict + ncells = {domain.name: ncells} + + # create a geometry from a topological domain and the dict of mappings + geo = Geometry(domain=domain, ncells=ncells, mappings=mappings) + + # # export the geometry + if rank == 0: + geo.export('geo_mpi_dims.h5') + comm.Barrier() + geo_from_file = Geometry(filename='geo_mpi_dims.h5', comm=comm, mpi_dims_mask=mpi_dims_mask) + if rank == 0: + assert geo_from_file.ddm.starts == (0, 0) + elif rank == 1: + assert geo_from_file.ddm.starts == (0, 2) + #============================================================================== @pytest.mark.parametrize( 'ncells', [[8,8], [12,12], [14,14]] ) @pytest.mark.parametrize( 'degree', [[2,2], [3,2], [2,3], [3,3], [4,4]] ) From 3376e3115b1d5a3d76f61e0f13d78243795396b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Fri, 19 Sep 2025 09:39:26 +0200 Subject: [PATCH 03/70] Update test_geometry.py - Write 3D geometry in parallel without `mpi_dims_mask` - Read 3D geometry in parallel with `mpi_dims_mask` - Verify correct distribution of domain for any number of MPI processes --- psydac/cad/tests/test_geometry.py | 42 ++++++++++++++++++------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index 10e24f0a8..91116fcfc 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -173,17 +173,20 @@ def test_geometry_2d_4(): @pytest.mark.parallel def test_geometry_with_mpi_dims_mask(): from mpi4py import MPI + comm = MPI.COMM_WORLD - rank = comm.Get_rank() - mpi_dims_mask = [False, True] - ncells = [4, 4] - degree = [2, 2] + rank = comm.rank + size = comm.size + mpi_dims_mask = [False, True, False] # We will verify that this has an effect + ncells = [4, 2*size, 8] # Each process should have two cells along x2 + degree = [2, 2, 2] + # create an identity mapping mapping = discrete_mapping('identity', ncells=ncells, degree=degree) # create a topological domain - F = Mapping('F', dim=2) - domain = F(Square(name='Omega')) + F = Mapping('F', dim=3) + domain = F(Cube(name='Omega')) # associate the mapping to the topological domain mappings = {domain.name: mapping} @@ -191,18 +194,22 @@ def test_geometry_with_mpi_dims_mask(): # Define ncells as a dict ncells = {domain.name: ncells} - # create a geometry from a topological domain and the dict of mappings - geo = Geometry(domain=domain, ncells=ncells, mappings=mappings) + # 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=ncells, mappings=mappings, comm=comm) + geo.export('geo_mpi_dims.h5') - # # export the geometry - if rank == 0: - geo.export('geo_mpi_dims.h5') - comm.Barrier() + # 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) - if rank == 0: - assert geo_from_file.ddm.starts == (0, 0) - elif rank == 1: - assert geo_from_file.ddm.starts == (0, 2) + + # Verify that the domain is distributed as expected + assert geo_from_file.ddm.starts[0] == 0 + assert geo_from_file.ddm.starts[1] == 2 * rank + assert geo_from_file.ddm.starts[2] == 0 + + assert geo_from_file.ddm.ends[0] == ncells[0] - 1 + assert geo_from_file.ddm.ends[1] == 2 * rank + 1 + assert geo_from_file.ddm.ends[2] == ncells[2] - 1 #============================================================================== @pytest.mark.parametrize( 'ncells', [[8,8], [12,12], [14,14]] ) @@ -319,12 +326,13 @@ def teardown_module(): 'geo.h5', 'geo_0.h5', 'geo_1.h5', + 'geo_mpi_dims.h5', 'quart_circle.h5', 'quart_circle_0.h5', 'quart_circle_1.h5', 'circle.h5', 'pipe.h5', - 'L_shaped.h5' + 'L_shaped.h5', ] for fname in filenames: if os.path.exists(fname): From 270471687cf358b47b215ad48aea7c3547c2fbf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Fri, 19 Sep 2025 09:56:26 +0200 Subject: [PATCH 04/70] Update geometry.py Add parameter `mpi_dims_mask` to class method `from_discrete_mapping` and bound method `read`. --- psydac/cad/geometry.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/psydac/cad/geometry.py b/psydac/cad/geometry.py index 066dc8231..58d3dc3ba 100644 --- a/psydac/cad/geometry.py +++ b/psydac/cad/geometry.py @@ -30,7 +30,7 @@ from sympde.topology.basic import Union #============================================================================== -class Geometry( object ): +class Geometry: """ Distributed discrete geometry that works for single and multiple patches. The Geometry object can be created in two ways: @@ -75,7 +75,7 @@ def __init__(self, domain=None, ncells=None, periodic=None, mappings=None, # ... read the geometry if the filename is given if filename is not None: - self.read(filename, comm=comm) + self.read(filename, comm=comm, mpi_dims_mask=mpi_dims_mask) elif domain is not None: assert isinstance(domain, Domain) @@ -122,7 +122,7 @@ def __init__(self, domain=None, ncells=None, periodic=None, mappings=None, # Option [2]: from a discrete mapping #-------------------------------------------------------------------------- @classmethod - def from_discrete_mapping(cls, mapping, comm=None, name=None): + def from_discrete_mapping(cls, mapping, *, comm=None, mpi_dims_mask=None, name=None): """Create a geometry from one discrete mapping. Parameters @@ -132,7 +132,11 @@ def from_discrete_mapping(cls, mapping, comm=None, name=None): comm : MPI.Comm MPI intra-communicator. - + + mpi_dims_mask: list of bool + 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 @@ -150,7 +154,7 @@ def from_discrete_mapping(cls, mapping, comm=None, name=None): 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) + return Geometry(domain=domain, ncells=ncells, periodic=periodic, mappings=mappings, comm=comm, mpi_dims_mask=mpi_dims_mask) #-------------------------------------------------------------------------- @@ -223,7 +227,7 @@ def mappings(self): def __len__(self): return len(self.domain) - def read( self, filename, comm=None ): + 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'): @@ -287,7 +291,7 @@ def read( self, filename, comm=None ): self._cart = None if n_patches == 1: - self._ddm = DomainDecomposition(ncells[domain.name], periodic[domain.name], comm=comm) + self._ddm = DomainDecomposition(ncells[domain.name], periodic[domain.name], comm=comm, mpi_dims_mask=mpi_dims_mask) ddms = [self._ddm] else: ncells_ = [ncells[itr.name] for itr in interiors] From e1a13f8da451c8b6385928daee2e8530f3987bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Fri, 19 Sep 2025 11:36:51 +0200 Subject: [PATCH 05/70] Update test_geometry.py Use new variable for dictionary with number of cells for each patch. --- psydac/cad/tests/test_geometry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index 91116fcfc..914f4d24e 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -191,12 +191,12 @@ def test_geometry_with_mpi_dims_mask(): # associate the mapping to the topological domain mappings = {domain.name: mapping} - # Define ncells as a dict - ncells = {domain.name: ncells} + # Define d_ncells as a dict + d_ncells = {domain.name: ncells} # 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=ncells, mappings=mappings, comm=comm) + geo = Geometry(domain=domain, ncells=d_ncells, mappings=mappings, comm=comm) geo.export('geo_mpi_dims.h5') # Read geometry file in parallel, but using mpi_dims_mask From 51a32da547a0ebc096ab050a8627f820df7b940a Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Wed, 24 Sep 2025 08:47:01 +0200 Subject: [PATCH 06/70] new tests for class Geometry test methods from_discrete_mapping and from_topological_domain with mpi_dims_mask --- psydac/cad/tests/test_geometry.py | 63 +++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index 914f4d24e..95b3619d1 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -17,6 +17,8 @@ from psydac.utilities.utils import refine_array_1d from psydac.ddm.cart import DomainDecomposition +from mpi4py import MPI + base_dir = os.path.dirname(os.path.realpath(__file__)) #============================================================================== def test_geometry_2d_1(): @@ -172,7 +174,6 @@ def test_geometry_2d_4(): #============================================================================== @pytest.mark.parallel def test_geometry_with_mpi_dims_mask(): - from mpi4py import MPI comm = MPI.COMM_WORLD rank = comm.rank @@ -203,13 +204,61 @@ def test_geometry_with_mpi_dims_mask(): geo_from_file = Geometry(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[0] == 0 - assert geo_from_file.ddm.starts[1] == 2 * rank - assert geo_from_file.ddm.starts[2] == 0 + check_decomposition(geo_from_file, ncells, rank, mpi_dims_mask) + +# ============================================================================== +@pytest.mark.parallel +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 + ncells = [4, 8, 2 * size] # Each process should have two cells along x3 + degree = [3, 3, 3] + + # Create a mapping + mapping = discrete_mapping('identity', ncells=ncells, degree=degree) + + # Create geometry from the mapping using mpi_dims_mask + geo_from_mapping = Geometry.from_discrete_mapping(mapping, comm=comm, mpi_dims_mask=mpi_dims_mask) + + # Verify that the domain is distributed as expected + check_decomposition(geo_from_mapping, ncells, rank, mpi_dims_mask) + +# ============================================================================== +@pytest.mark.parallel +def test_from_topological_domain(): + + comm = MPI.COMM_WORLD + rank = comm.rank + size = comm.size + mpi_dims_mask = [False, True, False] # We will verify that this has an effect + ncells = [4, 2 * size, 8] # Each process should have two cells along x2 + + # Create a topological domain + F = Mapping('F', dim=3) + domain = F(Cube(name='Omega')) + + # Create geometry from topological domain using mpi_dims_mask + geo_from_domain = Geometry.from_topological_domain(domain, ncells, comm=comm, mpi_dims_mask=mpi_dims_mask) + + # Verify that the domain is distributed as expected + check_decomposition(geo_from_domain, ncells, rank, mpi_dims_mask) + +# ============================================================================== +# Check that each process has 2 cells along the axis where mpi_dims_mask is True +# Only 1 dimension should be decomposed +def check_decomposition(geometry, ncells, rank, mpi_dims_mask): + + for i in range(len(ncells)): + if mpi_dims_mask[i]: + assert geometry.ddm.starts[i] == 2 * rank + assert geometry.ddm.ends[i] == 2 * rank + 1 + else: + assert geometry.ddm.starts[i] == 0 + assert geometry.ddm.ends[i] == ncells[i] - 1 - assert geo_from_file.ddm.ends[0] == ncells[0] - 1 - assert geo_from_file.ddm.ends[1] == 2 * rank + 1 - assert geo_from_file.ddm.ends[2] == ncells[2] - 1 #============================================================================== @pytest.mark.parametrize( 'ncells', [[8,8], [12,12], [14,14]] ) From 505db3196d9cc66f5578b2313069bf1b28547ecb Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Wed, 24 Sep 2025 14:51:54 +0200 Subject: [PATCH 07/70] run only my tests --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index d8ff30b1c..ce810cfd6 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -224,7 +224,7 @@ jobs: - name: Run MPI tests with Pytest working-directory: ./pytest run: | - python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs psydac -m "parallel and not petsc" + python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs psydac/cad/tests -m "parallel and not petsc" - name: Run single-process PETSc tests with Pytest working-directory: ./pytest From 39c51a432cd370decd46f8c4011dd247f243a6ed Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Wed, 24 Sep 2025 15:02:06 +0200 Subject: [PATCH 08/70] run only my tests --- .github/workflows/testing.yml | 74 +++++++++++++++++------------------ 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index ce810cfd6..4571fa5af 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -192,49 +192,49 @@ jobs: mkdir pytest cp mpi_tester.py pytest - - name: Run coverage tests on macOS - if: matrix.os == 'macos-14' - working-directory: ./pytest - run: >- - python -m pytest -n auto - --cov psydac - --cov-config $GITHUB_WORKSPACE/pyproject.toml - --cov-report xml - --pyargs psydac -m "not parallel and not petsc" - - - name: Run single-process tests with Pytest on Ubuntu - if: matrix.os == 'ubuntu-24.04' - working-directory: ./pytest - run: | - python -m pytest -n auto --pyargs psydac -m "not parallel and not petsc" - - - name: Upload coverage report to Codacy - if: matrix.os == 'macos-14' - uses: codacy/codacy-coverage-reporter-action@v1.3.0 - with: - project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - coverage-reports: ./pytest/coverage.xml - - - name: Print detailed coverage results on macOS - if: matrix.os == 'macos-14' - working-directory: ./pytest - run: | - coverage report --ignore-errors --show-missing --sort=cover +# - name: Run coverage tests on macOS +# if: matrix.os == 'macos-14' +# working-directory: ./pytest +# run: >- +# python -m pytest -n auto +# --cov psydac +# --cov-config $GITHUB_WORKSPACE/pyproject.toml +# --cov-report xml +# --pyargs psydac -m "not parallel and not petsc" + +# - name: Run single-process tests with Pytest on Ubuntu +# if: matrix.os == 'ubuntu-24.04' +# working-directory: ./pytest +# run: | +# python -m pytest -n auto --pyargs psydac -m "not parallel and not petsc" + +# - name: Upload coverage report to Codacy +# if: matrix.os == 'macos-14' +# uses: codacy/codacy-coverage-reporter-action@v1.3.0 +# with: +# project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} +# coverage-reports: ./pytest/coverage.xml +# +# - name: Print detailed coverage results on macOS +# if: matrix.os == 'macos-14' +# working-directory: ./pytest +# run: | +# coverage report --ignore-errors --show-missing --sort=cover - name: Run MPI tests with Pytest working-directory: ./pytest run: | python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs psydac/cad/tests -m "parallel and not petsc" - - name: Run single-process PETSc tests with Pytest - working-directory: ./pytest - run: | - python -m pytest -n auto --pyargs psydac -m "not parallel and petsc" - - - name: Run MPI PETSc tests with Pytest - working-directory: ./pytest - run: | - python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs psydac -m "parallel and petsc" +# - name: Run single-process PETSc tests with Pytest +# working-directory: ./pytest +# run: | +# python -m pytest -n auto --pyargs psydac -m "not parallel and petsc" +# +# - name: Run MPI PETSc tests with Pytest +# working-directory: ./pytest +# run: | +# python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs psydac -m "parallel and petsc" - name: Remove test directory if: always() From 34e42e6e2951874ebdbaac68da6ddf9fa6264465 Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Wed, 24 Sep 2025 15:12:46 +0200 Subject: [PATCH 09/70] run only my tests --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 4571fa5af..7f028cff0 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -224,7 +224,7 @@ jobs: - name: Run MPI tests with Pytest working-directory: ./pytest run: | - python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs psydac/cad/tests -m "parallel and not petsc" + python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs ./cad/tests -m "parallel and not petsc" # - name: Run single-process PETSc tests with Pytest # working-directory: ./pytest From 90553f9ddd4573b30dcb231a48f81c02b5d14d2d Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Wed, 24 Sep 2025 15:19:56 +0200 Subject: [PATCH 10/70] run only my tests --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 7f028cff0..fedbbf955 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -224,7 +224,7 @@ jobs: - name: Run MPI tests with Pytest working-directory: ./pytest run: | - python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs ./cad/tests -m "parallel and not petsc" + python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs cad/tests -m "parallel and not petsc" # - name: Run single-process PETSc tests with Pytest # working-directory: ./pytest From 243d2b1f8f5a39cbadd80f05cb4739721e2bcae0 Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Wed, 24 Sep 2025 15:39:19 +0200 Subject: [PATCH 11/70] run only my tests --- .github/workflows/testing.yml | 2 +- psydac/cad/tests/test_geometry.py | 1 + pytest.ini | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index fedbbf955..a402b744b 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -224,7 +224,7 @@ jobs: - name: Run MPI tests with Pytest working-directory: ./pytest run: | - python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs cad/tests -m "parallel and not petsc" + python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs psydac -m "parallel and not petsc and geo" # - name: Run single-process PETSc tests with Pytest # working-directory: ./pytest diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index 95b3619d1..f8da1048c 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -173,6 +173,7 @@ def test_geometry_2d_4(): #============================================================================== @pytest.mark.parallel +@pytest.mark.geo def test_geometry_with_mpi_dims_mask(): comm = MPI.COMM_WORLD diff --git a/pytest.ini b/pytest.ini index 91ef13cfa..417753c5f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -10,6 +10,7 @@ markers = parallel: test to be run using 'mpiexec', petsc: test requiring a working PETSc installation with petsc4py Python bindings pyccel: test for checking Pyccel setup on machine + geo: geometry python_files = test_*.py python_classes = From 9bfda593920e75b7542f609031d405a86c228ab2 Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Wed, 24 Sep 2025 16:03:53 +0200 Subject: [PATCH 12/70] run only my tests --- psydac/cad/tests/test_geometry.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index f8da1048c..74856ca6e 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -207,6 +207,11 @@ def test_geometry_with_mpi_dims_mask(): # Verify that the domain is distributed as expected check_decomposition(geo_from_file, ncells, rank, mpi_dims_mask) + comm.Barrier() + if rank == 0: + os.remove('geo_mpi_dims.h5') + + # ============================================================================== @pytest.mark.parallel def test_from_discrete_mapping(): @@ -376,7 +381,6 @@ def teardown_module(): 'geo.h5', 'geo_0.h5', 'geo_1.h5', - 'geo_mpi_dims.h5', 'quart_circle.h5', 'quart_circle_0.h5', 'quart_circle_1.h5', From f75df55c67ebb5e9da00172967d1796181517dc9 Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Wed, 24 Sep 2025 16:17:03 +0200 Subject: [PATCH 13/70] fixed failing tests due to deleting file in parallel --- .github/workflows/testing.yml | 78 +++++++++++++++---------------- psydac/cad/tests/test_geometry.py | 2 +- pytest.ini | 3 +- 3 files changed, 41 insertions(+), 42 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index a402b744b..cf626f63e 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -192,51 +192,51 @@ jobs: mkdir pytest cp mpi_tester.py pytest -# - name: Run coverage tests on macOS -# if: matrix.os == 'macos-14' -# working-directory: ./pytest -# run: >- -# python -m pytest -n auto -# --cov psydac -# --cov-config $GITHUB_WORKSPACE/pyproject.toml -# --cov-report xml -# --pyargs psydac -m "not parallel and not petsc" - -# - name: Run single-process tests with Pytest on Ubuntu -# if: matrix.os == 'ubuntu-24.04' -# working-directory: ./pytest -# run: | -# python -m pytest -n auto --pyargs psydac -m "not parallel and not petsc" - -# - name: Upload coverage report to Codacy -# if: matrix.os == 'macos-14' -# uses: codacy/codacy-coverage-reporter-action@v1.3.0 -# with: -# project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} -# coverage-reports: ./pytest/coverage.xml -# -# - name: Print detailed coverage results on macOS -# if: matrix.os == 'macos-14' -# working-directory: ./pytest -# run: | -# coverage report --ignore-errors --show-missing --sort=cover + - name: Run coverage tests on macOS + if: matrix.os == 'macos-14' + working-directory: ./pytest + run: >- + python -m pytest -n auto + --cov psydac + --cov-config $GITHUB_WORKSPACE/pyproject.toml + --cov-report xml + --pyargs psydac -m "not parallel and not petsc" + + - name: Run single-process tests with Pytest on Ubuntu + if: matrix.os == 'ubuntu-24.04' + working-directory: ./pytest + run: | + python -m pytest -n auto --pyargs psydac -m "not parallel and not petsc" + + - name: Upload coverage report to Codacy + if: matrix.os == 'macos-14' + uses: codacy/codacy-coverage-reporter-action@v1.3.0 + with: + project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} + coverage-reports: ./pytest/coverage.xml + + - name: Print detailed coverage results on macOS + if: matrix.os == 'macos-14' + working-directory: ./pytest + run: | + coverage report --ignore-errors --show-missing --sort=cover - name: Run MPI tests with Pytest working-directory: ./pytest run: | - python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs psydac -m "parallel and not petsc and geo" + python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs psydac -m "parallel and not petsc" + + - name: Run single-process PETSc tests with Pytest + working-directory: ./pytest + run: | + python -m pytest -n auto --pyargs psydac -m "not parallel and petsc" -# - name: Run single-process PETSc tests with Pytest -# working-directory: ./pytest -# run: | -# python -m pytest -n auto --pyargs psydac -m "not parallel and petsc" -# -# - name: Run MPI PETSc tests with Pytest -# working-directory: ./pytest -# run: | -# python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs psydac -m "parallel and petsc" + - name: Run MPI PETSc tests with Pytest + working-directory: ./pytest + run: | + python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs psydac -m "parallel and petsc" - name: Remove test directory if: always() run: | - rm -rf pytest + rm -rf pytest \ No newline at end of file diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index 74856ca6e..7fd67662a 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -173,7 +173,6 @@ def test_geometry_2d_4(): #============================================================================== @pytest.mark.parallel -@pytest.mark.geo def test_geometry_with_mpi_dims_mask(): comm = MPI.COMM_WORLD @@ -207,6 +206,7 @@ def test_geometry_with_mpi_dims_mask(): # Verify that the domain is distributed as expected check_decomposition(geo_from_file, ncells, rank, mpi_dims_mask) + # Safely remove the file comm.Barrier() if rank == 0: os.remove('geo_mpi_dims.h5') diff --git a/pytest.ini b/pytest.ini index 417753c5f..f9bac0384 100644 --- a/pytest.ini +++ b/pytest.ini @@ -10,8 +10,7 @@ markers = parallel: test to be run using 'mpiexec', petsc: test requiring a working PETSc installation with petsc4py Python bindings pyccel: test for checking Pyccel setup on machine - geo: geometry python_files = test_*.py python_classes = -python_functions = test_* +python_functions = test_* \ No newline at end of file From 2a7bb4d6f6dcbaf1ce3adbe31205522fe9e45dc2 Mon Sep 17 00:00:00 2001 From: Alisa Kirkinskaia Date: Wed, 24 Sep 2025 16:28:53 +0200 Subject: [PATCH 14/70] revert files --- .github/workflows/testing.yml | 2 +- pytest.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index cf626f63e..d8ff30b1c 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -239,4 +239,4 @@ jobs: - name: Remove test directory if: always() run: | - rm -rf pytest \ No newline at end of file + rm -rf pytest diff --git a/pytest.ini b/pytest.ini index f9bac0384..91ef13cfa 100644 --- a/pytest.ini +++ b/pytest.ini @@ -13,4 +13,4 @@ markers = python_files = test_*.py python_classes = -python_functions = test_* \ No newline at end of file +python_functions = test_* From e7b6bfbdf2fb30d17a3e3493131953bc75909140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 30 Sep 2025 15:01:03 +0200 Subject: [PATCH 15/70] Avoid function check_decomposition in tests --- psydac/cad/tests/test_geometry.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index 7fd67662a..09c096134 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -181,6 +181,9 @@ def test_geometry_with_mpi_dims_mask(): mpi_dims_mask = [False, True, False] # We will verify that this has an effect ncells = [4, 2*size, 8] # Each process should have two cells along x2 degree = [2, 2, 2] + + expected_starts = (0, 2 * rank, 0) + expected_ends = (3, 2 * rank + 1, 7) # create an identity mapping mapping = discrete_mapping('identity', ncells=ncells, degree=degree) @@ -204,7 +207,8 @@ def test_geometry_with_mpi_dims_mask(): geo_from_file = Geometry(filename='geo_mpi_dims.h5', comm=comm, mpi_dims_mask=mpi_dims_mask) # Verify that the domain is distributed as expected - check_decomposition(geo_from_file, ncells, rank, mpi_dims_mask) + assert geo_from_file.ddm.starts == expected_starts + assert geo_from_file.ddm.ends == expected_ends # Safely remove the file comm.Barrier() @@ -223,6 +227,9 @@ def test_from_discrete_mapping(): ncells = [4, 8, 2 * size] # Each process should have two cells along x3 degree = [3, 3, 3] + expected_starts = (0, 0, 2 * rank) + expected_ends = (3, 7, 2 * rank + 1) + # Create a mapping mapping = discrete_mapping('identity', ncells=ncells, degree=degree) @@ -230,7 +237,8 @@ def test_from_discrete_mapping(): geo_from_mapping = Geometry.from_discrete_mapping(mapping, comm=comm, mpi_dims_mask=mpi_dims_mask) # Verify that the domain is distributed as expected - check_decomposition(geo_from_mapping, ncells, rank, mpi_dims_mask) + assert geo_from_mapping.ddm.starts == expected_starts + assert geo_from_mapping.ddm.ends == expected_ends # ============================================================================== @pytest.mark.parallel @@ -242,6 +250,8 @@ def test_from_topological_domain(): mpi_dims_mask = [False, True, False] # We will verify that this has an effect ncells = [4, 2 * size, 8] # Each process should have two cells along x2 + expected_starts = (0, 2 * rank, 0) + expected_ends = (3, 2 * rank + 1, 7) # Create a topological domain F = Mapping('F', dim=3) domain = F(Cube(name='Omega')) @@ -250,21 +260,8 @@ def test_from_topological_domain(): geo_from_domain = Geometry.from_topological_domain(domain, ncells, comm=comm, mpi_dims_mask=mpi_dims_mask) # Verify that the domain is distributed as expected - check_decomposition(geo_from_domain, ncells, rank, mpi_dims_mask) - -# ============================================================================== -# Check that each process has 2 cells along the axis where mpi_dims_mask is True -# Only 1 dimension should be decomposed -def check_decomposition(geometry, ncells, rank, mpi_dims_mask): - - for i in range(len(ncells)): - if mpi_dims_mask[i]: - assert geometry.ddm.starts[i] == 2 * rank - assert geometry.ddm.ends[i] == 2 * rank + 1 - else: - assert geometry.ddm.starts[i] == 0 - assert geometry.ddm.ends[i] == ncells[i] - 1 - + assert geometry.ddm.starts == expected_starts + assert geometry.ddm.ends == expected_ends #============================================================================== @pytest.mark.parametrize( 'ncells', [[8,8], [12,12], [14,14]] ) From f6afb5451015c11da54a8b7c86d99dee7c361fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 30 Sep 2025 17:52:37 +0200 Subject: [PATCH 16/70] Fix typo in test_geometry.py --- psydac/cad/tests/test_geometry.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index 09c096134..458fc8f50 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -252,6 +252,7 @@ def test_from_topological_domain(): expected_starts = (0, 2 * rank, 0) expected_ends = (3, 2 * rank + 1, 7) + # Create a topological domain F = Mapping('F', dim=3) domain = F(Cube(name='Omega')) @@ -260,8 +261,8 @@ def test_from_topological_domain(): geo_from_domain = Geometry.from_topological_domain(domain, ncells, comm=comm, mpi_dims_mask=mpi_dims_mask) # Verify that the domain is distributed as expected - assert geometry.ddm.starts == expected_starts - assert geometry.ddm.ends == expected_ends + assert geo_from_domain.ddm.starts == expected_starts + assert geo_from_domain.ddm.ends == expected_ends #============================================================================== @pytest.mark.parametrize( 'ncells', [[8,8], [12,12], [14,14]] ) From 81b1c6dbaf15af2e34772481a86dbfd3ec655ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 30 Sep 2025 18:39:13 +0200 Subject: [PATCH 17/70] Improve Geometry class: - Add new constructor Geometry.from_file - Remove `filename` from __init__ parameters --- psydac/cad/geometry.py | 249 ++++++++++++++++++++++++++--------------- 1 file changed, 156 insertions(+), 93 deletions(-) diff --git a/psydac/cad/geometry.py b/psydac/cad/geometry.py index 58d3dc3ba..8a4c7ddac 100644 --- a/psydac/cad/geometry.py +++ b/psydac/cad/geometry.py @@ -4,20 +4,18 @@ # 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 os +from typing import Iterable +from itertools import chain + import numpy as np -import string -import random import h5py import yaml -import os -import string -import random - - from mpi4py import MPI +from sympde.topology import Domain, Interface, Line, Square, Cube, NCubeInterior, Mapping, NCube +from sympde.topology.basic import Union + from psydac.fem.splines import SplineSpace from psydac.fem.tensor import TensorFemSpace from psydac.fem.partitioning import create_cart, construct_connectivity, construct_interface_spaces @@ -25,9 +23,15 @@ 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: @@ -51,12 +55,9 @@ class Geometry: mappings : dict The Mapping of each patch. - filename: str - The path to the geometry file. - - comm: MPI.Comm + comm: MPI.Intracomm MPI intra-communicator. - + mpi_dims_mask: list of bool 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. @@ -67,63 +68,125 @@ class Geometry: _patches = [] _topology = None + def __init__(self, + domain : Domain, + *, + 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(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 ncells + assert set(ncells.keys()) == set_interior_names + assert all(len(n) == ldim for n in ncells.values()) + assert all(isinstance(ni, int) for ni in chain(*ncells.values())) + assert all(ni > 0 for ni in chain(*ncells.values())) + + # Check sanity of periodic + if periodic is None: + periodic = {patch: [False]*len(ncells_i) for patch, ncells_i 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, (SplineMapping, NoneType)) for m in mappings.values()) + + # 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[name], + periodic[name], + comm = comm, + mpi_dims_mask = mpi_dims_mask, + ) + else: + ddm = MultiPatchDomainDecomposition( + ncells = [ ncells[itr] for itr in interior_names], + periodic = [periodic[itr] for itr in interior_names], + comm = comm, + ) + + # Add attributes to the new object + 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._comm = comm + self._ddm = ddm + self._cart = None + self._is_parallel = comm is not 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: list[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 ---------- @@ -137,9 +200,14 @@ 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 + name : str Optional name for the 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' @@ -156,7 +224,6 @@ def from_discrete_mapping(cls, mapping, *, comm=None, mpi_dims_mask=None, name=N return Geometry(domain=domain, ncells=ncells, periodic=periodic, mappings=mappings, comm=comm, mpi_dims_mask=mpi_dims_mask) - #-------------------------------------------------------------------------- # Option [3]: discrete topological line/square/cube #-------------------------------------------------------------------------- @@ -238,38 +305,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." ) # ... - # ... 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'] @@ -289,29 +354,27 @@ def read(self, filename, comm=None, mpi_dims_mask=None): 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 ): @@ -367,7 +430,6 @@ 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() # ... @@ -383,6 +445,8 @@ def read(self, filename, comm=None, mpi_dims_mask=None): self._mappings = mappings self._domain = domain self._comm = comm + self._ddm = ddm + self._cart = None self._ncells = ncells self._periodic = periodic self._is_parallel = comm is not None @@ -426,7 +490,6 @@ def export( self, filename ): yml['patches'] = patches_info # ... - # ... topology topo_yml = self.domain.todict() # ... From e3c30dbb10ea7aaab950c1ef26d475868156a8d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 30 Sep 2025 18:57:14 +0200 Subject: [PATCH 18/70] Update Geometry unit tests --- psydac/cad/tests/test_geometry.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index 458fc8f50..a5963a525 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -45,7 +45,7 @@ def test_geometry_2d_1(): 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') @@ -94,7 +94,7 @@ def test_geometry_2d_2(): 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') @@ -173,7 +173,7 @@ def test_geometry_2d_4(): #============================================================================== @pytest.mark.parallel -def test_geometry_with_mpi_dims_mask(): +def test_from_file_with_mpi_dims_mask(): comm = MPI.COMM_WORLD rank = comm.rank @@ -204,7 +204,7 @@ def test_geometry_with_mpi_dims_mask(): 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 @@ -223,7 +223,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] @@ -284,7 +284,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 @@ -333,7 +333,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 From 8716dcbfa3ccbbc0a8c743ccb36ded6a0534371a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Wed, 1 Oct 2025 12:33:48 +0200 Subject: [PATCH 19/70] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit ccbd6e3b029bf8e978780c73a643b7b1186b0942 Author: Yaman Güçlü Date: Wed Oct 1 06:25:42 2025 +0200 Allow `mpi_dims_mask` with geometry file (#526) Add the optional parameter `mpi_dims_mask` to the constructor of class `Geometry`, as well as its class methods `from_discrete_mapping` and `from_topological_domain`. Add unit tests to verify that the domain is correctly decomposed. --------- Co-authored-by: Alisa Kirkinskaia Co-authored-by: Alisa Kirkinskaia --- psydac/cad/tests/test_geometry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index a5963a525..07a30e77d 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -19,6 +19,8 @@ from mpi4py import MPI +from mpi4py import MPI + base_dir = os.path.dirname(os.path.realpath(__file__)) #============================================================================== def test_geometry_2d_1(): From 7111b8e4da96879f6afc91de7926fef1a0440c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Wed, 1 Oct 2025 12:57:10 +0200 Subject: [PATCH 20/70] Do not create __psydac__ directory in parallel --- psydac/api/fem_bilinear_form.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/psydac/api/fem_bilinear_form.py b/psydac/api/fem_bilinear_form.py index 8af05df83..44a15d187 100644 --- a/psydac/api/fem_bilinear_form.py +++ b/psydac/api/fem_bilinear_form.py @@ -1425,12 +1425,12 @@ def make_file(self, temps, ordered_stmts, field_derivatives, max_logical_derivat assembly_code += '\n return\n' #------------------------- MAKE FILE ------------------------- - import os - if not os.path.isdir('__psydac__'): - os.makedirs('__psydac__') # Root process writes the assembly code to a file if comm is None or comm.rank == 0: + import os + if not os.path.isdir('__psydac__'): + os.makedirs('__psydac__') filename = f'__psydac__/assemble_{file_id}.py' f = open(filename, 'w') f.writelines(assembly_code) @@ -1981,26 +1981,23 @@ def construct_arguments_generate_assembly_file(self): assembly_backend = self.backend if self._pyccelize_test_trial_computation and assembly_backend['name'] == 'pyccel': - import os - if not os.path.isdir('__psydac__'): - os.makedirs('__psydac__') - comm = self.comm - if comm is not None and comm.size > 1: - if comm.rank == 0: - filename = '__psydac__/test_trial_computation.py' - code = self.test_trial_template - f = open(filename, 'w') - f.writelines(code) - f.close() - else: + # Root process writes the assembly code to a file + if comm is None or comm.rank == 0: + import os + if not os.path.isdir('__psydac__'): + os.makedirs('__psydac__') filename = '__psydac__/test_trial_computation.py' code = self.test_trial_template f = open(filename, 'w') f.writelines(code) f.close() + # Parallel case: wait for the file to be closed before proceeding + if comm is not None and comm.size > 1: + _ = comm.bcast(None, root=0) + base_dirpath = os.getcwd() sys.path.insert(0, base_dirpath) From cc097a331b75440b3942bf84792e75c98b647765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Wed, 1 Oct 2025 13:05:04 +0200 Subject: [PATCH 21/70] Use Geometry.from_file in discretize_domain --- psydac/api/discretization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index d98ba1a51..9aeb4c87c 100644 --- a/psydac/api/discretization.py +++ b/psydac/api/discretization.py @@ -575,7 +575,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) From cd925fbafd47dc12d30c65c33f5fb6dcdc2aa368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Wed, 1 Oct 2025 13:07:30 +0200 Subject: [PATCH 22/70] Use correct parameter name (periods) in MultiPatchDomainDecomposition constructor --- psydac/cad/geometry.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/psydac/cad/geometry.py b/psydac/cad/geometry.py index 8a4c7ddac..46075f243 100644 --- a/psydac/cad/geometry.py +++ b/psydac/cad/geometry.py @@ -37,9 +37,12 @@ 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 ncells, periodicity and mapping 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 ---------- @@ -121,16 +124,16 @@ def __init__(self, #name = domain.name name = interior_names[0] ddm = DomainDecomposition( - ncells[name], - periodic[name], - comm = comm, + 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], - periodic = [periodic[itr] for itr in interior_names], - comm = comm, + ncells = [ ncells[itr] for itr in interior_names], + periods = [periodic[itr] for itr in interior_names], + comm = comm, ) # Add attributes to the new object From f1b6bb15d746161ac3393ca6eaae39deedb124c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 7 Oct 2025 11:33:12 +0200 Subject: [PATCH 23/70] Add mandatory parameter pdim to Geometry.__init__ --- psydac/cad/geometry.py | 82 ++++++++++++++++++++----------- psydac/cad/tests/test_geometry.py | 6 +-- 2 files changed, 55 insertions(+), 33 deletions(-) diff --git a/psydac/cad/geometry.py b/psydac/cad/geometry.py index 46075f243..fa4093216 100644 --- a/psydac/cad/geometry.py +++ b/psydac/cad/geometry.py @@ -15,6 +15,7 @@ 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 @@ -47,21 +48,24 @@ class Geometry: 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 + ncells : dict[str, Iterable[int]] + The number of cells of the discretized domain in each direction. + + periodic : dict[str, Iterable[bool]], optional The periodicity of the topological domain in each direction. - mappings : dict - The Mapping of each patch. + mappings : dict[str, BasicCallableMapping], optional + The discrete mappings of each patch. - comm: MPI.Intracomm + 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. @@ -74,6 +78,7 @@ class Geometry: def __init__(self, domain : Domain, *, + pdim : int, ncells : dict[str, Iterable[int]], mappings : dict[str, SplineMapping | None] = None, periodic : dict[str, Iterable[bool]] = None, @@ -81,6 +86,7 @@ def __init__(self, mpi_dims_mask : Iterable[bool] = None): # Type checks + assert isinstance(pdim, int) assert isinstance(domain, Domain) assert isinstance(ncells, dict) assert isinstance(mappings, dict) @@ -93,6 +99,9 @@ def __init__(self, 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()) @@ -112,7 +121,8 @@ def __init__(self, mappings = {itr.name : None for itr in domain.interior} else: assert set(mappings.keys()) == set_interior_names - assert all(isinstance(m, (SplineMapping, NoneType)) for m in mappings.values()) + 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: @@ -139,10 +149,10 @@ def __init__(self, # Add attributes to the new object self._domain = domain self._ldim = domain.dim - self._pdim = domain.dim # TODO must be given => only dim is defined for a Domain + self._pdim = pdim self._ncells = ncells - self._periodic = periodic self._mappings = mappings + self._periodic = periodic self._comm = comm self._ddm = ddm self._cart = None @@ -169,7 +179,7 @@ def from_file(cls, comm: MPI.Intracomm, optional The MPI intra-communicator. - mpi_dims_mask: list[bool], optional + 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. @@ -193,8 +203,8 @@ def from_discrete_mapping(cls, mapping, *, comm=None, mpi_dims_mask=None, name=N 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. @@ -204,8 +214,8 @@ def from_discrete_mapping(cls, mapping, *, comm=None, mpi_dims_mask=None, name=N If mpi_dims_mask[i]=False, the i-th dimension will not be decomposed. name : str - Optional name for the Mapping that will be created. - Needed to avoid conflicts in case several mappings are created + Optional name for the symbolic Mapping that will be created. + Needed to avoid conflicts in case several mappings are created. Returns ------- @@ -214,18 +224,25 @@ def from_discrete_mapping(cls, mapping, *, comm=None, mpi_dims_mask=None, name=N """ 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 @@ -238,24 +255,29 @@ def from_topological_domain(cls, domain, ncells, *, periodic=None, comm=None, mp 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 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 @@ -443,15 +465,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._ncells = ncells self._mappings = mappings - self._domain = domain + self._periodic = periodic self._comm = comm self._ddm = ddm self._cart = None - self._ncells = ncells - self._periodic = periodic self._is_parallel = comm is not None # ... diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index 07a30e77d..282f4db8f 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -41,7 +41,7 @@ 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') @@ -90,7 +90,7 @@ 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') @@ -202,7 +202,7 @@ def test_from_file_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 From 27160655cb4d83f484f128dc6f279ccc183edb6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 7 Oct 2025 12:42:20 +0200 Subject: [PATCH 24/70] Clean up Geometry.read() --- psydac/cad/geometry.py | 43 +++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/psydac/cad/geometry.py b/psydac/cad/geometry.py index fa4093216..7723fa904 100644 --- a/psydac/cad/geometry.py +++ b/psydac/cad/geometry.py @@ -321,8 +321,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') # ... @@ -351,7 +351,7 @@ def read(self, filename, comm=None, mpi_dims_mask=None): # ... if n_patches == 0: h5.close() - raise ValueError( "Input file contains no patches." ) + raise ValueError("Input file contains no patches.") # ... # ... read patches @@ -370,9 +370,9 @@ 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 @@ -413,26 +413,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])): @@ -454,6 +454,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() From 818be2a8a31a172e29a169f1444b6eba5df1cd6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 7 Oct 2025 12:43:50 +0200 Subject: [PATCH 25/70] Remove unused property is_parallel from Geometry --- psydac/cad/geometry.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/psydac/cad/geometry.py b/psydac/cad/geometry.py index 7723fa904..3176d995e 100644 --- a/psydac/cad/geometry.py +++ b/psydac/cad/geometry.py @@ -156,7 +156,6 @@ def __init__(self, self._comm = comm self._ddm = ddm self._cart = None - self._is_parallel = comm is not None #-------------------------------------------------------------------------- # Option [1]: from a file @@ -308,10 +307,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 @@ -475,7 +470,6 @@ def read(self, filename, comm=None, mpi_dims_mask=None): self._comm = comm self._ddm = ddm self._cart = None - self._is_parallel = comm is not None # ... def export( self, filename ): From cb8ccb5cead68499b5112658ccf242fe8beac847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Wed, 8 Oct 2025 11:09:51 +0200 Subject: [PATCH 26/70] Clean up module psydac.mapping.discrete_gallery: - Add function `get_available_mappings` - Clean up function `discrete_mapping` and add docstring to it --- psydac/mapping/discrete_gallery.py | 105 ++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 17 deletions(-) diff --git a/psydac/mapping/discrete_gallery.py b/psydac/mapping/discrete_gallery.py index 1a6f7a493..ef215ffdc 100644 --- a/psydac/mapping/discrete_gallery.py +++ b/psydac/mapping/discrete_gallery.py @@ -1,4 +1,5 @@ # coding: utf-8 +from typing import Iterable import numpy as np from mpi4py import MPI @@ -14,6 +15,11 @@ from psydac.ddm.cart import DomainDecomposition +__all__ = ( + 'get_available_mappings', + 'discrete_mapping', +) + class Collela3D( Mapping ): _expressions = {'x':'2.*(x1 + 0.1*sin(2.*pi*x1)*sin(2.*pi*x2)) - 1.', @@ -21,78 +27,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_analytic = SphericalMapping('M', dim=dim) + map_analytic = SphericalMapping('M', dim=ldim) limits = ((1, 4), (0, np.pi), (0, np.pi/2)) periodic = ( False, False, False) From 2e56b104feb1557376dd924f32cd2be15e0c87eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 21 Oct 2025 17:29:00 +0200 Subject: [PATCH 27/70] Run fewer tests, only on Ubuntu with Python 3.11 --- .github/workflows/testing.yml | 160 +++++++++++++++++----------------- 1 file changed, 81 insertions(+), 79 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index c3c2da8a4..86df467ed 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -36,7 +36,8 @@ jobs: fail-fast: false matrix: os: [ ubuntu-24.04, macos-14 ] - python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] +# python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] + python-version: [ '3.11' ] isMerge: - ${{ github.event_name == 'push' && github.ref == 'refs/heads/devel' }} exclude: @@ -121,46 +122,46 @@ jobs: echo $HDF5_DIR echo "HDF5_DIR=$HDF5_DIR" >> $GITHUB_ENV - - name: Cache PETSc - uses: actions/cache@v4 - id: cache-petsc - env: - cache-name: cache-PETSc - with: - path: "./petsc" - key: petsc-${{ matrix.os }}-${{ matrix.python-version }} - - - if: steps.cache-petsc.outputs.cache-hit != 'true' - name: Download a specific release of PETSc - run: | - git clone --depth 1 --branch v3.23.2 https://gitlab.com/petsc/petsc.git - - - if: steps.cache-petsc.outputs.cache-hit != 'true' - name: Install PETSc with complex support - working-directory: ./petsc - run: | - export PETSC_DIR=$(pwd) - export PETSC_ARCH=petsc-cmplx - ./configure --with-scalar-type=complex --with-fortran-bindings=0 --have-numpy=1 - make all - echo "PETSC_DIR=$PETSC_DIR" > petsc.env - echo "PETSC_ARCH=$PETSC_ARCH" >> petsc.env - - # This step is not really necessary and could be combined with PETSc install - # step; however it's good to verify if the cached PETSc installation really works! - - name: Test PETSc installation - working-directory: ./petsc - run: | - source petsc.env - make check - echo "PETSC_DIR=$PETSC_DIR" >> $GITHUB_ENV - echo "PETSC_ARCH=$PETSC_ARCH" >> $GITHUB_ENV - - - name: Install petsc4py - working-directory: ./petsc - run: | - python -m pip install wheel Cython numpy - python -m pip install src/binding/petsc4py +# - name: Cache PETSc +# uses: actions/cache@v4 +# id: cache-petsc +# env: +# cache-name: cache-PETSc +# with: +# path: "./petsc" +# key: petsc-${{ matrix.os }}-${{ matrix.python-version }} +# +# - if: steps.cache-petsc.outputs.cache-hit != 'true' +# name: Download a specific release of PETSc +# run: | +# git clone --depth 1 --branch v3.23.2 https://gitlab.com/petsc/petsc.git +# +# - if: steps.cache-petsc.outputs.cache-hit != 'true' +# name: Install PETSc with complex support +# working-directory: ./petsc +# run: | +# export PETSC_DIR=$(pwd) +# export PETSC_ARCH=petsc-cmplx +# ./configure --with-scalar-type=complex --with-fortran-bindings=0 --have-numpy=1 +# make all +# echo "PETSC_DIR=$PETSC_DIR" > petsc.env +# echo "PETSC_ARCH=$PETSC_ARCH" >> petsc.env +# +# # This step is not really necessary and could be combined with PETSc install +# # step; however it's good to verify if the cached PETSc installation really works! +# - name: Test PETSc installation +# working-directory: ./petsc +# run: | +# source petsc.env +# make check +# echo "PETSC_DIR=$PETSC_DIR" >> $GITHUB_ENV +# echo "PETSC_ARCH=$PETSC_ARCH" >> $GITHUB_ENV +# +# - name: Install petsc4py +# working-directory: ./petsc +# run: | +# python -m pip install wheel Cython numpy +# python -m pip install src/binding/petsc4py - name: Install h5py in parallel mode run: | @@ -192,49 +193,50 @@ jobs: mkdir pytest cp mpi_tester.py pytest - - name: Run coverage tests on macOS - if: matrix.os == 'macos-14' - working-directory: ./pytest - run: >- - python -m pytest -n auto - --cov psydac - --cov-config $GITHUB_WORKSPACE/pyproject.toml - --cov-report xml - --pyargs psydac -m "not parallel and not petsc" +# - name: Run single-process tests with Pytest and coverage on macOS +# if: matrix.os == 'macos-14' +# working-directory: ./pytest +# run: >- +# python -m pytest -n auto +# --cov psydac +# --cov-config $GITHUB_WORKSPACE/pyproject.toml +# --cov-report xml +# --pyargs psydac -m "not parallel and not petsc" - name: Run single-process tests with Pytest on Ubuntu if: matrix.os == 'ubuntu-24.04' working-directory: ./pytest run: | - python -m pytest -n auto --pyargs psydac -m "not parallel and not petsc" - - - name: Upload coverage report to Codacy - if: matrix.os == 'macos-14' - uses: codacy/codacy-coverage-reporter-action@v1.3.0 - with: - project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - coverage-reports: ./pytest/coverage.xml - - - name: Print detailed coverage results on macOS - if: matrix.os == 'macos-14' - working-directory: ./pytest - run: | - coverage report --ignore-errors --show-missing --sort=cover - - - name: Run MPI tests with Pytest - working-directory: ./pytest - run: | - python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs psydac -m "parallel and not petsc" - - - name: Run single-process PETSc tests with Pytest - working-directory: ./pytest - run: | - python -m pytest -n auto --pyargs psydac -m "not parallel and petsc" - - - name: Run MPI PETSc tests with Pytest - working-directory: ./pytest - run: | - python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs psydac -m "parallel and petsc" + python -m pytest --pyargs psydac.cad -x -ra -v +# python -m pytest -n auto --pyargs psydac -m "not parallel and not petsc" + +# - name: Upload coverage report to Codacy +# if: matrix.os == 'macos-14' +# uses: codacy/codacy-coverage-reporter-action@v1.3.0 +# with: +# project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} +# coverage-reports: ./pytest/coverage.xml +# +# - name: Print detailed coverage results on macOS +# if: matrix.os == 'macos-14' +# working-directory: ./pytest +# run: | +# coverage report --ignore-errors --show-missing --sort=cover +# +# - name: Run MPI tests with Pytest +# working-directory: ./pytest +# run: | +# python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs psydac -m "parallel and not petsc" +# +# - name: Run single-process PETSc tests with Pytest +# working-directory: ./pytest +# run: | +# python -m pytest -n auto --pyargs psydac -m "not parallel and petsc" +# +# - name: Run MPI PETSc tests with Pytest +# working-directory: ./pytest +# run: | +# python mpi_tester.py --mpirun="mpiexec -n 4 ${MPI_OPTS}" --pyargs psydac -m "parallel and petsc" - name: Remove test directory if: always() From b042a0a76088398939533357260187b6fcca92a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Mon, 26 Jan 2026 17:12:36 +0100 Subject: [PATCH 28/70] Reactivate single-process tests on Ubuntu and macOS --- .github/workflows/testing.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index fdbc88c2f..e977a7cf1 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -30,7 +30,7 @@ jobs: matrix: os: [ ubuntu-24.04, macos-14 ] # python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] - python-version: [ '3.11' ] + python-version: [ '3.11', '3.12' ] isMerge: - ${{ github.event_name == 'push' && github.ref == 'refs/heads/devel' }} exclude: @@ -139,13 +139,17 @@ jobs: # --cov-config $GITHUB_WORKSPACE/pyproject.toml # --cov-report xml # --pyargs psydac -m "not parallel and not petsc" +# +# - name: Run single-process tests with Pytest on Ubuntu +# if: matrix.os == 'ubuntu-24.04' +# working-directory: ./pytest +# run: | +# psydac test - - name: Run single-process tests with Pytest on Ubuntu - if: matrix.os == 'ubuntu-24.04' + - name: Run single-process tests with Pytest working-directory: ./pytest run: | psydac test --mod psydac.cad -x -v -# psydac test # - name: Upload coverage report to Codacy # if: matrix.os == 'macos-14' From 6b6653f75c6afacc20d0708e580f4c3d9a9b8b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Mon, 26 Jan 2026 17:17:54 +0100 Subject: [PATCH 29/70] Remove duplicated imports from test_geometry.py --- psydac/cad/tests/test_geometry.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index f7ead795f..bc8269606 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -9,9 +9,6 @@ import numpy as np from mpi4py import MPI -import pytest -import numpy as np -from mpi4py import MPI from sympde.topology import Domain, Line, Square, Cube, Mapping from psydac.cad.geometry import Geometry, export_nurbs_to_hdf5, refine_nurbs From 8ea76a8b46ddee7608ebe8d587caf5b7216f0ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 27 Jan 2026 13:44:33 +0100 Subject: [PATCH 30/70] Improve docstring of Geometry class --- psydac/cad/geometry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psydac/cad/geometry.py b/psydac/cad/geometry.py index 3f54026fb..384650789 100644 --- a/psydac/cad/geometry.py +++ b/psydac/cad/geometry.py @@ -44,7 +44,7 @@ class Geometry: Distributed discrete geometry that works for single and multiple patches. The Geometry object can be created in four ways: - - case 0 : providing a `Domain` to `__init__` with ncells, periodicity and mapping for each patch. + - 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). From 739aee8af1d9ec2f26edd9b4c940e960e4fee527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 27 Jan 2026 14:57:06 +0100 Subject: [PATCH 31/70] Add 'h5py' xdist_group to all unit tests in test_geometry.py --- psydac/cad/tests/test_geometry.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index bc8269606..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] @@ -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 @@ -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 @@ -270,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 @@ -323,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) From 66205e720cf328a1f5177a580ffcb3b18b35a56f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 27 Jan 2026 16:16:41 +0100 Subject: [PATCH 32/70] Handle NumPy integers passed in ncells to Geometry constructor --- psydac/cad/geometry.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/psydac/cad/geometry.py b/psydac/cad/geometry.py index 384650789..333248ef5 100644 --- a/psydac/cad/geometry.py +++ b/psydac/cad/geometry.py @@ -109,9 +109,13 @@ def __init__(self, # 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) for ni in chain(*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(ncells_i) for patch, ncells_i in ncells.items()} From ae6ebf87d0ae6dfb878c1f7e05f6ffddd960eb6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 27 Jan 2026 16:17:55 +0100 Subject: [PATCH 33/70] Minor cleanup in Geometry constructor --- psydac/cad/geometry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psydac/cad/geometry.py b/psydac/cad/geometry.py index 333248ef5..1bc0490d0 100644 --- a/psydac/cad/geometry.py +++ b/psydac/cad/geometry.py @@ -118,7 +118,7 @@ def __init__(self, # Check sanity of periodic if periodic is None: - periodic = {patch: [False]*len(ncells_i) for patch, ncells_i in ncells.items()} + 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()) @@ -276,12 +276,12 @@ def from_topological_domain(cls, domain, ncells, *, periodic=None, comm=None, mp 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} From 7dff0212a2030a725f967fba9200f41e709c1fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 27 Jan 2026 16:47:41 +0100 Subject: [PATCH 34/70] Run unit tests in mapping, fem, and feec folders --- .github/workflows/testing.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index e977a7cf1..319e205cb 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -146,11 +146,26 @@ jobs: # run: | # psydac test - - name: Run single-process tests with Pytest + - name: Run single-process tests for CAD module working-directory: ./pytest run: | psydac test --mod psydac.cad -x -v + - name: Run single-process tests for Mapping module + working-directory: ./pytest + run: | + psydac test --mod psydac.mapping -x -v + + - name: Run single-process tests for FEM module + working-directory: ./pytest + run: | + psydac test --mod psydac.fem -x -v + + - name: Run single-process tests for FEEC module + working-directory: ./pytest + run: | + psydac test --mod psydac.feec -x -v + # - name: Upload coverage report to Codacy # if: matrix.os == 'macos-14' # uses: codacy/codacy-coverage-reporter-action@v1.3.0 From fdd357a423e093edf0352612d9f080499bbe2f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 27 Jan 2026 20:42:29 +0100 Subject: [PATCH 35/70] Run unit tests in api folder --- .github/workflows/testing.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 319e205cb..4ead8d3e7 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -166,6 +166,11 @@ jobs: run: | psydac test --mod psydac.feec -x -v + - name: Run single-process tests for API module + working-directory: ./pytest + run: | + psydac test --mod psydac.api -x -v + # - name: Upload coverage report to Codacy # if: matrix.os == 'macos-14' # uses: codacy/codacy-coverage-reporter-action@v1.3.0 From 001e5924a0a8a4a5fb34b9d27529a46cc9af6965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 27 Jan 2026 23:17:50 +0100 Subject: [PATCH 36/70] Revert "Do not create __psydac__ directory in parallel" This reverts commit 7111b8e4da96879f6afc91de7926fef1a0440c73. These changes will be included in another PR, hence we remove them from here to avoid future conflicts. --- psydac/api/fem_bilinear_form.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/psydac/api/fem_bilinear_form.py b/psydac/api/fem_bilinear_form.py index a7f97fdef..06b50b946 100644 --- a/psydac/api/fem_bilinear_form.py +++ b/psydac/api/fem_bilinear_form.py @@ -1440,12 +1440,12 @@ def make_file(self, temps, ordered_stmts, field_derivatives, max_logical_derivat assembly_code += '\n return\n' #------------------------- MAKE FILE ------------------------- + import os + if not os.path.isdir('__psydac__'): + os.makedirs('__psydac__') # Root process writes the assembly code to a file if comm is None or comm.rank == 0: - import os - if not os.path.isdir('__psydac__'): - os.makedirs('__psydac__') filename = f'__psydac__/assemble_{file_id}.py' f = open(filename, 'w') f.writelines(assembly_code) @@ -1996,23 +1996,26 @@ def construct_arguments_generate_assembly_file(self): assembly_backend = self.backend if self._pyccelize_test_trial_computation and assembly_backend['name'] == 'pyccel': + import os + if not os.path.isdir('__psydac__'): + os.makedirs('__psydac__') + comm = self.comm - # Root process writes the assembly code to a file - if comm is None or comm.rank == 0: - import os - if not os.path.isdir('__psydac__'): - os.makedirs('__psydac__') + if comm is not None and comm.size > 1: + if comm.rank == 0: + filename = '__psydac__/test_trial_computation.py' + code = self.test_trial_template + f = open(filename, 'w') + f.writelines(code) + f.close() + else: filename = '__psydac__/test_trial_computation.py' code = self.test_trial_template f = open(filename, 'w') f.writelines(code) f.close() - # Parallel case: wait for the file to be closed before proceeding - if comm is not None and comm.size > 1: - _ = comm.bcast(None, root=0) - base_dirpath = os.getcwd() sys.path.insert(0, base_dirpath) From b807f32b93d2747c82f4c4e8e8aafec288de62ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Wed, 28 Jan 2026 12:05:14 +0100 Subject: [PATCH 37/70] Update `CHANGELOG.md` with new and previous changes --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9faae0e02..0043750a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,17 @@ All notable changes to this project will be documented in this file. ### Added +- #567 Improve `psydac test` command (with several new features) +- #565 Expand editable install info in `README.md` + ### Fixed -- #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 ### Changed +- #527 Improve `Geometry` class in module `psydac.cad.geometry` - [DEVELOPER] Run documentation workflow whenever `README.md` is modified - [DEVELOPER] Run testing workflow on PRs only when set to "ready for review" From a06a7d39bc8abc68f50e64e0511b1c6f23642f7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 10 Feb 2026 14:15:15 +0100 Subject: [PATCH 38/70] Use SymPDE branch clean-multipatch-domain --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1882383a0..4ceb24fdc 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.1.0', 'gelato == 0.12', From e968bc584b6e151199671f81497d12b3d91aa8dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 10 Feb 2026 22:56:50 +0100 Subject: [PATCH 39/70] Provide interface orientation to Domain.join --- psydac/api/tests/test_2d_complex.py | 4 +-- .../test_2d_multipatch_mapping_maxwell.py | 4 +-- .../test_2d_multipatch_mapping_poisson.py | 35 ++++++++++--------- .../api/tests/test_2d_multipatch_poisson.py | 22 ++++++------ psydac/api/tests/test_postprocessing.py | 11 +++--- 5 files changed, 39 insertions(+), 37 deletions(-) diff --git a/psydac/api/tests/test_2d_complex.py b/psydac/api/tests/test_2d_complex.py index ae916cd49..10eb5fdb8 100644 --- a/psydac/api/tests/test_2d_complex.py +++ b/psydac/api/tests/test_2d_complex.py @@ -367,7 +367,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 @@ -495,7 +495,7 @@ def test_maxwell_2d_2_patch_dirichlet_parallel_0(): D1 = mapping_1(A) D2 = mapping_2(B) - domain = Domain.join([D1, D2], [((0, 1, 1), (1, 1, -1))], 'domain') + domain = Domain.join([D1, D2], [((0, 1, 1), (1, 1, -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 5620e2979..daabf8220 100644 --- a/psydac/api/tests/test_2d_multipatch_mapping_maxwell.py +++ b/psydac/api/tests/test_2d_multipatch_mapping_maxwell.py @@ -126,7 +126,7 @@ def test_maxwell_2d_2_patch_dirichlet_0(): D1 = mapping_1(A) D2 = mapping_2(B) - connectivity = [((0,1,1),(1,1,-1))] + connectivity = [((0, 1, 1), (1, 1,-1), 1)] patches = [D1,D2] domain = Domain.join(patches, connectivity, 'domain') @@ -203,7 +203,7 @@ def test_maxwell_2d_2_patch_dirichlet_parallel_0(): D1 = mapping_1(A) D2 = mapping_2(B) - connectivity = [((0,1,1),(1,1,-1))] + connectivity = [((0, 1, 1), (1, 1,-1), 1)] patches = [D1,D2] domain = Domain.join(patches, connectivity, 'domain') 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 d657b1fe9..e99916d17 100644 --- a/psydac/api/tests/test_2d_multipatch_mapping_poisson.py +++ b/psydac/api/tests/test_2d_multipatch_mapping_poisson.py @@ -105,11 +105,11 @@ def test_poisson_2d_2_patches_dirichlet_0(): mapping_1 = PolarMapping('M1',2, c1= 0., c2= 0., rmin = 0., rmax=1.) mapping_2 = PolarMapping('M2',2, c1= 0., c2= 0., rmin = 0., rmax=1.) - D1 = mapping_1(A) - D2 = mapping_2(B) + D1 = mapping_1(A) + 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') x,y = domain.coordinates @@ -133,11 +133,11 @@ def test_poisson_2d_2_patches_dirichlet_1(): mapping_1 = PolarMapping('M1',2, c1= 0., c2= 0., rmin = 0., rmax=1.) mapping_2 = PolarMapping('M2',2, c1= 0., c2= 0., rmin = 0., rmax=1.) - D1 = mapping_1(A) - D2 = mapping_2(B) + D1 = mapping_1(A) + 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') x,y = domain.coordinates @@ -163,12 +163,12 @@ def test_poisson_2d_2_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 @@ -274,8 +274,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 @@ -311,11 +314,11 @@ def test_poisson_2d_2_patches_dirichlet_parallel_0(): mapping_1 = PolarMapping('M1',2, c1= 0., c2= 0., rmin = 0., rmax=1.) mapping_2 = PolarMapping('M2',2, c1= 0., c2= 0., rmin = 0., rmax=1.) - D1 = mapping_1(A) - D2 = mapping_2(B) + D1 = mapping_1(A) + D2 = mapping_2(B) - connectivity = [((0,1,1),(1,1,-1))] patches = [D1, D2] + connectivity = [((0, 1, 1), (1, 1,-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 c69a56c3f..c487a2b23 100644 --- a/psydac/api/tests/test_2d_multipatch_poisson.py +++ b/psydac/api/tests/test_2d_multipatch_poisson.py @@ -80,11 +80,11 @@ def run_poisson_2d(solution, f, domain, ncells, degree): #------------------------------------------------------------------------------ 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)) + 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))] - patches = [A,B] + patches = [A, B] + connectivity = [((0, 0, 1), (1, 0,-1), 1)] domain = Domain.join(patches, connectivity, 'domain') x,y = domain.coordinates @@ -105,8 +105,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))] - patches = [A,B] + patches = [A, B] + connectivity = [((0, 0, 1), (1, 0,-1), 1)] domain = Domain.join(patches, connectivity, 'domain') x,y = domain.coordinates @@ -126,7 +126,7 @@ 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))] + connectivity = [((0, 0, 1), (1, 0,-1), 1)] patches = [A,B] domain = Domain.join(patches, connectivity, 'domain') @@ -155,8 +155,8 @@ def test_poisson_2d_2_patch_dirichlet_3(): D1 = M1(A) D2 = M2(B) - connectivity = [((0,0,1),(1,0,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 @@ -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))] - 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 bdf482888..5501d9cd3 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 # ############################################################################### @@ -530,8 +529,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) From a31e07ab3b4113d19ed51ce97ad6bdff6a469a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 10 Feb 2026 22:59:22 +0100 Subject: [PATCH 40/70] Add interface orientation to multipatch geometry files --- psydac/cad/mesh/multipatch/magnet.h5 | Bin 17517 -> 17783 bytes psydac/cad/mesh/multipatch/square.h5 | Bin 10400 -> 10296 bytes .../mesh/multipatch/square_repeated_knots.h5 | Bin 18656 -> 18616 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/psydac/cad/mesh/multipatch/magnet.h5 b/psydac/cad/mesh/multipatch/magnet.h5 index c18c688ecbce06dcdfa9c708df97a09b7295914d..9e497c2a0cfa2f27e151c597d3f40177d213bfd6 100644 GIT binary patch delta 1022 zcmb`FJ!n%=6vyv9X_9M5Okzx4N^9~$G)ZGV3}P&;P2v{{McXY|BnhI)R>46LB#WRF zO8YqAnz2xX_W2Y$X&l^yj*5tJ(#3*X!6ETA_g-l;Wb!TNfA0Oi!}*l!eLvTKt#5u)d9bDr~bRHl399k#vlKHKWR?+MFW=X3#{N?&$RaH|NAC6W46u%Mn z_9&i+GXL;ooMe#@TsBL6w==VF&CDJZZ=-5ChMT?$xW}U?Ga( z)LsAZqseJ3&`}`W2@_<)IYyZ1ZRey!ipM3=`NY23J;mVKl%~Ixa1fs~>VXWUOSqWm z!)6HenHLTv%_}{m*qw?&J8Og^=9ERZ^c{v$jo9$!j3)p0vZGmc!b2>p8OieVjAYr% zNf!I;K^8?Fuz>xnq33!g>A3;T)8Zi-_a$R>HUQ8nm@|*zZ`-uiVdyGs#l()AE6#uT gA4If@wbE^ae@X@uFFQPX-tf)n-4N=XRQA#zyJmt*dVk8gklhtfG`-L{J#nihAmXw z11i21P2E#Xhy<$+gpPo!bG3#rVCLmQ_{(W8+_qROlw6p8w&no1nNov>dH|`p3JMpHE{>uWCdoS$&w-wOn)UNJKBp)?i88F zG*@`?M^!bZucDI!Mb(&C#Wxo^1u!zXN`e$@Ua2m@$k;pimw4~wcr5`gS!IZw4vdpE zmF1XXB_^L@Q=6QxY%#fBf&;9g78)WBj1xDyY}S=z;$Y0(Y@qm`5#m1uh<;f3V*1aU z7Z!Fn-F{XY>h>wzEDRhF0~ul`e^6Ct+NuWj#8LIhj%sR5S2ZSX+z0ovs0`dkOtCVP ze~Y(Gu3{CK9I2VXICt_P&8d_56&0Ay>TNbu6k}pi)q(oVK@>f%$#U~vd6=7d5N_V9 z3l4cv1F)l24Z)5!wL*3@s{+E&rba+Vw@f_1w^`IUhl8oqWb$FnH3D1hU{M00C-Vy` zFtIvrPP74fW34UF8*vj4a7?~$w?Pz|8i9!sq7>67;fz>Qi!ay};MDTf9_(gcC$O7i Qogt}ZDv~p&nu44O0Lj+I(EtDd diff --git a/psydac/cad/mesh/multipatch/square.h5 b/psydac/cad/mesh/multipatch/square.h5 index ae3f87a271eca786f42e2bb3184cc7a51ba7db8e..781c05feb1fb31842a191dc32aa304f02118a1d5 100644 GIT binary patch delta 455 zcmZ1wxFcYK2BXDB&7Vw+tdpx*H7ENpn=mslteCiQ?PdcOFGh$c(?90Pe#|nGQ#lyG z;2%&4G5{%`Tp%tnc>x;-hrxq^VX`Z`KO@8BsqEepcPMP) zh+}3^P|#J_9K$`2Ws`sc(_{^HDdqs6GF72)pt4$_WM)PN*2%wx(j_PJK4hnrxud0(2&;GRRMuPB!L+XvgK`$$TPQOb3)FZj|GM*vr5m bF}YAxok>G|;>KripEu(%2CQhZI?Oi!#{*mK delta 571 zcmdlHupn@P2IGQ_nm?Hs*(O)BYEJfHHW3zL;9vj&Mj&=HZ%Sfdm@#qV+|33oUW^b` zOh1?>`!UOCPKBsr_`wWT4<)c`XJ%yJoZQVO%Xnk*LMHLa2iP(uD+oz3doZv}o-6J> zc>+5NBg^Ee?B0_#*d;dqU{7P-oWebaW#R$0O#%u`tUL?>K!vtK;Xp;P%E^=esc=o) z!8f@;oMV!3l4dUt#OeTKXE0#8qhjL5+Q}DG1U5^ESb@CqTPR)gE-89}_DwcWZ<_o{ zObkmXMNHfnJGp~Rf@_8n#32(HCvI%pEFrG!em3$mr%dfl42gjuQPD?696plb&vo6 diff --git a/psydac/cad/mesh/multipatch/square_repeated_knots.h5 b/psydac/cad/mesh/multipatch/square_repeated_knots.h5 index 471b157f30a8b39e4862b03efdda05167d709cdb..a1734329025e3eead35d56d94538f4ccb2291522 100644 GIT binary patch delta 466 zcmaDbk#WaF#t9mXJ2q-MGBYx4&SvIiG-4270D}!|5ZVGlF%*bH7>rQ9gARm|pf`CH z%P%GdmdSq1GF($RpwcXp&#>NM+%dUOTz>Kiwgf!^sJRM45Cu1&>UbDDz$``vHU=IB z35Mc~#DY|Y$?x>ECNB^ZUJH|H3HF)=OWLv~)poE#GBYM@&SvIiG)fR)0D}!|5ZVGlF)(mI7>rQ<0v!mWLVWTn zmS0T2m?!%g$Z|pz0eNRw?=aq&ypTzJ@&UGt$qGUoqUa{`FnBPqO#Z6p&&V>_Rw&p? zfB|B4gAl~z2B=Mp3~US_TP2`;n7RXc5CI3qi5u-Ual|oiPT`)zGVuW0Bmo6Rfz65n z{~0Gw5MlwEK36|^vYjE<#2tK-14Of!-t$b(G1$r|JaMDkWCuge$&MoGOc`PmH$DT& zGS1(;TU4KsF=6s+vDV26dJ;@C#3!F(i<@|$W3z;WhZxu^2+u>Dfa;ZSOe_8QHs=_G eF){t;+q_3qkCADC$>cX;EfWvuZLTx15&;0cf?1gW From 326952bac0d314b5c7e126932308a949dbb201d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Thu, 12 Feb 2026 11:32:08 +0100 Subject: [PATCH 41/70] Use `ubuntu_install` action in `documentation` workflow Use the custom action `ubuntu_install` for installing non-Python dependencies, like in the `testing` workflow. Add a separate step to install the packages `graphviz` and `pandoc` required for documentation. --- .github/workflows/documentation.yml | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 499832421..5766202e0 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -25,9 +25,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 @@ -36,18 +38,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: | @@ -107,6 +100,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 From 800e5069fdfc26f17c87c1e034b1cd3c8588f02d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Thu, 12 Feb 2026 11:45:00 +0100 Subject: [PATCH 42/70] Clone the latest release of PETSc (see PR #539) --- .github/workflows/documentation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 5766202e0..27bedc081 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -61,9 +61,9 @@ jobs: key: petsc-${{ matrix.os }}-${{ matrix.python-version }} - if: steps.cache-petsc.outputs.cache-hit != 'true' - name: Download a specific release of PETSc + name: Download the latest release of PETSc run: | - git clone --depth 1 --branch v3.23.2 https://gitlab.com/petsc/petsc.git + git clone --depth 1 -b release https://gitlab.com/petsc/petsc.git - if: steps.cache-petsc.outputs.cache-hit != 'true' name: Install PETSc with complex support From 9f092d3bbb4e1fbc91290b18ebf6280e65736705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Thu, 12 Feb 2026 15:41:03 +0100 Subject: [PATCH 43/70] Install mpi4py >= 4.0.0 in `parallel_h5py` action --- .github/actions/parallel_h5py/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/parallel_h5py/action.yml b/.github/actions/parallel_h5py/action.yml index 192e857a1..8b88fed2d 100644 --- a/.github/actions/parallel_h5py/action.yml +++ b/.github/actions/parallel_h5py/action.yml @@ -20,6 +20,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 From 3702e70d8029670f2d25b65f2ab686ba3f8ceeb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 21 Apr 2026 17:12:47 +0200 Subject: [PATCH 44/70] Add missing orientation (-1 or +1) to connectivity in test_plot_field --- psydac/fem/tests/test_plot_field_2d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psydac/fem/tests/test_plot_field_2d.py b/psydac/fem/tests/test_plot_field_2d.py index a099aee02..0b247b40b 100644 --- a/psydac/fem/tests/test_plot_field_2d.py +++ b/psydac/fem/tests/test_plot_field_2d.py @@ -54,7 +54,7 @@ def test_plot_field(use_scalar_field, use_multipatch): mapping_2 = PolarMapping('M2',2, c1= 0., c2= 0., rmin = 0., rmax=1.) D2 = mapping_2(B) - connectivity = [((0,1,1),(1,1,-1))] + connectivity = [((0, 1, 1), (1, 1,-1), 1)] patches = [D1,D2] domain = Domain.join(patches, connectivity, 'domain') else: From d91b2bab6e569f0dd82fb80d2850efed55937d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 21 Apr 2026 17:13:59 +0200 Subject: [PATCH 45/70] Minor cleanup --- psydac/fem/tests/test_plot_field_2d.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/psydac/fem/tests/test_plot_field_2d.py b/psydac/fem/tests/test_plot_field_2d.py index 0b247b40b..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) + patches = [D1, D2] connectivity = [((0, 1, 1), (1, 1,-1), 1)] - patches = [D1,D2] domain = Domain.join(patches, connectivity, 'domain') else: domain = D1 From 0b426e2666801506c68456dc536e9433a8df5020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 21 Apr 2026 18:23:45 +0200 Subject: [PATCH 46/70] Avoid deprecation warning about scipy.odr --- psydac/feec/tests/test_feec_conf_projectors_cart_2d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 349eee8cab2d71f913b792a4e9d44fd421e29f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 21 Apr 2026 18:31:33 +0200 Subject: [PATCH 47/70] Check input domain type in Geometry.from_topological_domain --- psydac/cad/geometry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/psydac/cad/geometry.py b/psydac/cad/geometry.py index 1bc0490d0..5889231f0 100644 --- a/psydac/cad/geometry.py +++ b/psydac/cad/geometry.py @@ -256,6 +256,8 @@ def from_discrete_mapping(cls, mapping, *, comm=None, mpi_dims_mask=None, name=N #-------------------------------------------------------------------------- @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] From a84e24d55870699a7b4bcaf958b63f9c30e73875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Wed, 22 Apr 2026 16:16:51 +0200 Subject: [PATCH 48/70] Add docstring to translate() in psydac.cad.cad --- psydac/cad/cad.py | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/psydac/cad/cad.py b/psydac/cad/cad.py index abc39baab..27457fde8 100644 --- a/psydac/cad/cad.py +++ b/psydac/cad/cad.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 psydac.fem.splines import SplineSpace @@ -12,26 +14,48 @@ from psydac.ddm.cart import DomainDecomposition #============================================================================== -def translate(mapping, displ): +def translate(mapping : SplineMapping, displ : Iterable[float]): + """ + Translate a CAD geometry by a given vector displacement. + + Translate a single-patch CAD geometry (given as a spline or NURBS mapping) + by adding a given displacement vector to the control points of the mapping. + + Parameters + ---------- + mapping : SplineMapping + The discrete mapping to be translated, which represents a CAD geometry. + + displ : Iterable[float] + The vector displacement by which to translate the geometry. + + Returns + ------- + SplineMapping + A new discrete mapping representing the translated geometry. + """ + assert isinstance(mapping, SplineMapping) + assert isinstance(displ, Iterable) + displ = np.array(displ) - assert( mapping.pdim == len(displ) ) + assert mapping.pdim == len(displ) pdim = mapping.pdim space = mapping.space control_points = mapping.control_points - fields = [FemField( space ) for d in range( pdim )] + fields = [FemField(space) for d in range(pdim)] # Get spline coefficients for each coordinate X_i starts = space.coeff_space.starts ends = space.coeff_space.ends - idx_to = tuple( slice( s, e+1 ) for s,e in zip( starts, ends ) ) - for i,field in enumerate( fields ): + idx_to = tuple(slice(s, e+1) for s, e in zip(starts, ends)) + for i, field in enumerate(fields): idx_from = tuple(list(idx_to)+[i]) field.coeffs[idx_to] = control_points[idx_from] + displ[i] field.coeffs.update_ghost_regions() - return SplineMapping( *fields ) + return SplineMapping(*fields) #============================================================================== def elevate(mapping, axis, times): From b4038196bf3d934f429714c82e8330b41f9bcdc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Wed, 9 Sep 2026 15:30:11 +0200 Subject: [PATCH 49/70] Test SymPDE version 0.20.0 (from GitHub) --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e0ae1db32..a309be93a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,8 +31,7 @@ dependencies = [ 'termcolor', # Our packages from PyPi -# 'sympde == 0.19.2', - 'sympde @ https://github.com/pyccel/sympde/archive/refs/heads/clean-multipatch-domain.zip', + 'sympde @ https://github.com/pyccel/sympde/archive/refs/tags/v0.20.0.zip', 'pyccel >= 2.2.3', 'gelato == 0.12', From ca148d09a3a6e4e2d8bb1380c5e6cea6c5c01134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Fri, 11 Sep 2026 18:18:54 +0200 Subject: [PATCH 50/70] Fix wrong action filename in docs workflow --- .github/workflows/documentation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index bfa20018a..e8582463b 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -40,7 +40,7 @@ jobs: python-version: '3.10' - name: Install non-Python dependencies on Ubuntu - uses: ./.github/actions/ubuntu_install + uses: ./.github/actions/ubuntu_installations - name: Print information on MPI and HDF5 libraries run: | From 43082abeb3dfad4ccf27542882209d39e3c15a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Fri, 11 Sep 2026 18:21:24 +0200 Subject: [PATCH 51/70] Give name to checkout step in testing workflow --- .github/workflows/testing.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 1b4b33c85..af9c84d7c 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -41,7 +41,8 @@ jobs: OMP_NUM_THREADS: 2 steps: - - uses: actions/checkout@v6 + - name: Checkout + uses: actions/checkout@v6 with: submodules: true From 2cb29712633dd52b01ae56120ed190740415ab23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Fri, 11 Sep 2026 18:34:07 +0200 Subject: [PATCH 52/70] Fix module name in documentation: cmd.mesh >> cmd.psydac_mesh --- docs/source/modules/cmd.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/modules/cmd.rst b/docs/source/modules/cmd.rst index c1f33d93c..e927a384d 100644 --- a/docs/source/modules/cmd.rst +++ b/docs/source/modules/cmd.rst @@ -7,4 +7,4 @@ cmd :toctree: STUBDIR :template: autosummary/module.rst - cmd.mesh + cmd.psydac_mesh From 0d5d648d2bf01d6c48e49e48f6d8ed63e10cb5d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Sat, 3 Oct 2026 11:09:42 +0200 Subject: [PATCH 53/70] Use SymPDE 0.20.0 from PyPI --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fde9392b0..ad867dc80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ dependencies = [ 'termcolor', # Our packages from PyPi - 'sympde @ https://github.com/pyccel/sympde/archive/refs/tags/v0.20.0.zip', + 'sympde == 0.20.0', 'pyccel >= 2.2.3', 'gelato == 0.12', From 42f2b57f9fd4353faa4ed36a2840189e9dd3786a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Sat, 3 Oct 2026 11:11:43 +0200 Subject: [PATCH 54/70] Reactivate all Python versions in testing workflow --- .github/workflows/testing.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index af9c84d7c..428cf0d0c 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -24,8 +24,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-24.04, macos-14 ] -# python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] - python-version: [ '3.11', '3.12' ] + python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] isMerge: - ${{ github.event_name == 'push' && github.ref == 'refs/heads/devel' }} exclude: From 75eebd5c2c20c528b9dca2a217d862428fd36f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Sun, 4 Oct 2026 10:34:01 +0200 Subject: [PATCH 55/70] Fix bad merge (wrong Python versions) in testing.yml --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 428cf0d0c..9a05f63a5 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -24,7 +24,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-24.04, macos-14 ] - python-version: [ '3.9', '3.10', '3.11', '3.12', '3.13' ] + python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ] isMerge: - ${{ github.event_name == 'push' && github.ref == 'refs/heads/devel' }} exclude: From 6bb18ec5efa5ba0828e9595411cc14d5e9f5a27e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Sun, 4 Oct 2026 12:04:11 +0200 Subject: [PATCH 56/70] Mention SymPDE 0.20.0 in CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0120a82f..ad4d4aba0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,9 +17,10 @@ All notable changes to this project will be documented in this file. ### Fixed -- #576 : Fix bug in `TensorFemSpace.eval_field` caused by round-off at MPI subdomain boundaries +- #527 : Require `sympde==0.20.0` which changes how multipatch interfaces are defined - #576 : Require `sympde==0.19.3` which fixes a bug in the linearity checks - #579 : Require `h5py>=3.16` which installs correctly with `setuptools>=81.0` +- #576 : Fix bug in `TensorFemSpace.eval_field` caused by round-off at MPI subdomain boundaries - #579 : Don't run postprocessing unit tests with `pytest-xdist` because `h5py` is not thread-safe - #579 : Return error code on failure of the `psydac test` and `psydac compile` commands - #577 : Fix installation following release of Pyccel 2.2 From ec4dc64bb9bea2a8635de609603c88af836545cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Mon, 5 Oct 2026 12:47:28 +0200 Subject: [PATCH 57/70] First draft of CLAUDE.md configuration file --- CLAUDE.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..25c9dcfc6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,69 @@ +# Project: PSYDAC + +## Overview +Python 3 library for isogeometric analysis (IGA). Solve general systems of partial +differential equations (PDEs) in weak form, defined using the domain-specific +language provided by SymPDE. Supports finite element exterior calculus (FEEC) +with tensor-product spline spaces. Handles multi-patch geometries in various ways; +usually broken-FEEC a.k.a. CONGA (conforming/non-conforming Galerkin). +Python code is automatically generated for the assembly of user-defined functionals, +linear forms, and bilinear forms. This Python code is then accelerated to C/Fortran +speed using Pyccel. The library enables large parallel computations on distributed- +memory supercomputers using MPI and OpenMP. + +## Tech stack +- Python 3.10+, type hints required everywhere +- meson-python + mesonpy for building +- pip with submodules (for igakit) +- pytest + pytest-cov + pytest-mpi + pytest-xdist for testing +- sympde for symbolic definition of weak formulations +- pyccel for translation of Python kernels to Fortran or C +- mpi4py for MPI parallelization +- h5py for parallel I/O +- petsc4py for direct linear solvers (optional install) + +## Code standards +- PEP 8 conventions +- Docstrings of public functions and classes follow Numpydoc conventions +- pylint + black + isort for linting/typing +- Computational kernels to be accelerated with Pyccel follow `_kernels.py` + +## Testing conventions +- Each library subpackage contains a `tests/` folder with an `__init__.py` file +- Unit tests are part of the library and shipped with it +- Unit tests can be run with `psydac test` CLI command (see `README.md`) +- Test file names follow `test_.py` +- Test function names follow `test_` or `test__` +- Parametrize unit tests with `@pytest.parametrize` to minimize code duplication +- Keep runtime at a minimum +- Aim for 100% coverage on newly committed code + +## File structure +- psydac/api - high-level Python interface +- psydac/cad - computer-aided design (CAD) functionality +- psydac/cmd - command-line interface (CLI) commands +- psydac/core - splines functionality +- psydac/ddm - MPI decomposition of domain, vectors of spline coefficients, and matrices +- psydac/feec - finite element exterior calculus (FEEC) +- psydac/fem - "finite element method" middle-level Python interface (spaces and fields) +- psydac/linalg - "linear algebra" low-level Python interface (vectors, linear operators, iterative solvers) +- psydac/polar - implementation of polar splines for H1 spaces +- psydac/pyccel - distillation of an old version of Pyccel for Python code generation (to be removed) +- psydac/utilities - various generic functions used across the library +- docs - Sphinx documentation +- examples - Various examples of library usage (mostly Jupyter notebooks) +- scripts - Various scripts for library maintenance +- subprojects - Git submodules + +## Always do +- Before claiming done: + . Run `python -m black --check $(git ls-files "*.py")` + . Run `python -m isort --check $(git ls-files "*.py" | grep -v "FOUND_DUPLICATED_IMPORT.py")` + . Run `pylint --disable=all --enable=unused-import ./psydac --ignore-paths="__pyccel*" --ignore-paths="__epyccel*"` +- Unless specified differently, assume process with `rank=0` in MPI communicator is root +- Use root MPI process to perform serial operations (e.g. open/save image) + +## Never do +- Commit secrets or `.env` files +- Add new dependencies without team discussion +- Use `eval` function together with `input` From c27f72e7245f74ada11769688c9fbf17ae3507c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Mon, 5 Oct 2026 14:57:57 +0200 Subject: [PATCH 58/70] Improve CLAUDE.md --- CLAUDE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 25c9dcfc6..c2afe8461 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,6 +27,8 @@ memory supercomputers using MPI and OpenMP. - Docstrings of public functions and classes follow Numpydoc conventions - pylint + black + isort for linting/typing - Computational kernels to be accelerated with Pyccel follow `_kernels.py` +- Minimize code duplication. Never add code that already exists +- Strive for concise, clean, and human-readable code. Avoid useless verbosity ## Testing conventions - Each library subpackage contains a `tests/` folder with an `__init__.py` file @@ -35,7 +37,7 @@ memory supercomputers using MPI and OpenMP. - Test file names follow `test_.py` - Test function names follow `test_` or `test__` - Parametrize unit tests with `@pytest.parametrize` to minimize code duplication -- Keep runtime at a minimum +- Keep run time of tests at a minimum - Aim for 100% coverage on newly committed code ## File structure From d9af9adba5b2675bd934fbe17fb7c6e47739fceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Mon, 5 Oct 2026 15:47:44 +0200 Subject: [PATCH 59/70] Expand 'Code standards' section of CLAUDE.md --- CLAUDE.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index c2afe8461..cdddd7c4b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,12 +23,15 @@ memory supercomputers using MPI and OpenMP. - petsc4py for direct linear solvers (optional install) ## Code standards -- PEP 8 conventions +- Follow PEP 8 style guide whenever possible - Docstrings of public functions and classes follow Numpydoc conventions - pylint + black + isort for linting/typing - Computational kernels to be accelerated with Pyccel follow `_kernels.py` - Minimize code duplication. Never add code that already exists - Strive for concise, clean, and human-readable code. Avoid useless verbosity +- Self-explanatory names for variables, functions, and classes +- Do not reassign value to an existing variable, especially if the type changes +- Use short comments (one-liners or inline) to explain "why" rather than "what" or "how" ## Testing conventions - Each library subpackage contains a `tests/` folder with an `__init__.py` file From 9adfa83fe990a40a142e8f2aa245e0ec76ecb9a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Mon, 5 Oct 2026 16:32:22 +0200 Subject: [PATCH 60/70] Further improve CLAUDE.md --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index cdddd7c4b..d25c1bb03 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,7 +26,7 @@ memory supercomputers using MPI and OpenMP. - Follow PEP 8 style guide whenever possible - Docstrings of public functions and classes follow Numpydoc conventions - pylint + black + isort for linting/typing -- Computational kernels to be accelerated with Pyccel follow `_kernels.py` +- Computational kernels to be accelerated with Pyccel named as `_kernels.py` - Minimize code duplication. Never add code that already exists - Strive for concise, clean, and human-readable code. Avoid useless verbosity - Self-explanatory names for variables, functions, and classes From 10a5088bd9c11e41700d63679f08f8553006962f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Mon, 5 Oct 2026 17:33:38 +0200 Subject: [PATCH 61/70] Remove blank imports from psydac/cad/__init__.py --- psydac/cad/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/psydac/cad/__init__.py b/psydac/cad/__init__.py index 6f39d9a03..419109b64 100644 --- a/psydac/cad/__init__.py +++ b/psydac/cad/__init__.py @@ -3,10 +3,3 @@ # LICENSE file or go to https://github.com/pyccel/psydac/blob/devel/LICENSE # # for full license details. # #---------------------------------------------------------------------------# - -__all__ = ['geometry'] - -from psydac.cad import geometry -from psydac.cad import cad -from psydac.cad import gallery -from psydac.cad import utils From bc216cd15a5f6fc6507f8dc1ca33ae7f776d131c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Mon, 5 Oct 2026 17:37:18 +0200 Subject: [PATCH 62/70] Remove obsolete test_geometry_1() from psydac.cad.tests.test_geometry --- psydac/cad/tests/test_geometry.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index 18ad343b1..1811fde14 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -365,14 +365,6 @@ def test_import_geopdes_to_nurbs(ncells, degree): if isinstance(mapping, NurbsMapping): assert np.allclose(L_shaped.weights.flatten(), mapping._weights_field.coeffs.toarray(), 1e-15, 1e-15) -#============================================================================== -@pytest.mark.xfail -def test_geometry_1(): - - line = Geometry.as_line(ncells=[10]) - square = Geometry.as_square(ncells=[10, 10]) - cube = Geometry.as_cube(ncells=[10, 10, 10]) - #============================================================================== # CLEAN UP SYMPY NAMESPACE #============================================================================== From 89e0b6b0c3e4d1aefbd877de8846a39b7dc3c02f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Mon, 5 Oct 2026 18:24:28 +0200 Subject: [PATCH 63/70] Add Pylint configuration to pyproject.toml --- pyproject.toml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ad867dc80..36c290a3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,3 +76,37 @@ psydac = "psydac.cmd.main:psydac_command" setup = ['--default-library=static'] dist = ['--include-subprojects'] install = ['--only=python-modules'] + +[tool.pylint.main] +py-version = '3.10' +ignore-paths = ['psydac/pyccel/.*', '.*/__e?pyccel__/.*', '.*/__psydac__/.*'] +ignored-modules = ['mpi4py'] # C extension, members not visible to Pylint + +[tool.pylint.basic] +no-docstring-rgx = '^(_|test_|Test)' + +[tool.pylint.'messages control'] +disable = [ + # formatting owned by black / isort + 'line-too-long', + 'trailing-whitespace', + 'bad-indentation', + 'multiple-statements', + 'missing-final-newline', + 'trailing-newlines', + 'wrong-import-order', + # style choices for mathematical code + 'invalid-name', + 'too-many-arguments', + 'too-many-positional-arguments', + 'too-many-locals', + 'too-many-branches', + 'too-many-statements', + 'too-many-instance-attributes', + 'too-few-public-methods', + 'too-many-nested-blocks', + # pytest fixtures shadow outer names by design + 'redefined-outer-name', + 'duplicate-code', + 'fixme', +] From bc6e145c9b1a066d08848971d6bf7f80315d1ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Mon, 5 Oct 2026 18:28:01 +0200 Subject: [PATCH 64/70] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad4d4aba0..f9dc0e1c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ All notable changes to this project will be documented in this file. - #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 +- [DEVELOPER] Configure Pylint in `pyproject.toml` +- [DEVELOPER] Add `CLAUDE.md` with developer rules ### Fixed From a6e0484e519d2d8341903d1bdcadd67f2ec9eb39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 6 Oct 2026 11:29:23 +0200 Subject: [PATCH 65/70] Use Geometry.from_file in polar poisson_2d example `plot_solution` still called the old `Geometry(filename=...)` constructor, which raised a TypeError after the redesign of `Geometry.__init__` when running the example with a spline mapping (option -S). --- psydac/feec/polar/examples/poisson_2d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psydac/feec/polar/examples/poisson_2d.py b/psydac/feec/polar/examples/poisson_2d.py index 688f821d1..b0f33cf49 100644 --- a/psydac/feec/polar/examples/poisson_2d.py +++ b/psydac/feec/polar/examples/poisson_2d.py @@ -394,7 +394,7 @@ def plot_solution(use_spline_mapping, model, ncells, periodic, V0_h, refine=10): from psydac.utilities.utils import refine_array_1d if use_spline_mapping: - geometry = Geometry(filename="geo.h5", comm=MPI.COMM_SELF) + geometry = Geometry.from_file("geo.h5", comm=MPI.COMM_SELF) map_discrete = [*geometry.mappings.values()].pop() Vnew = map_discrete.space map_plot = map_discrete From 66c4041e9a299a9789f86f96396082278f4bcb78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 6 Oct 2026 12:28:07 +0200 Subject: [PATCH 66/70] Move imports into functions in polar poisson_2d example The example is meant to be run as a script, so imports are made local to the functions that use them. Only the base classes PolarModel2D and LinearOperator, needed at class definition, remain at module level. Silence Pylint's import-outside-toplevel check for the whole file, with a comment explaining the reason. --- psydac/feec/polar/examples/poisson_2d.py | 34 +++++++++++++++++------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/psydac/feec/polar/examples/poisson_2d.py b/psydac/feec/polar/examples/poisson_2d.py index b0f33cf49..1f1558d62 100644 --- a/psydac/feec/polar/examples/poisson_2d.py +++ b/psydac/feec/polar/examples/poisson_2d.py @@ -19,18 +19,14 @@ `mpirun -n 6 python poisson_2d.py -S -d 3 3 -t disk -D 0.2 -m 'C0conga'` """ -from dataclasses import dataclass -from time import sleep, time +# Imports are local to keep the CLI fast (e.g. --help) +# pylint: disable=import-outside-toplevel -import numpy as np -from mpi4py import MPI -from pyccel import lambdify -from sympy import Rational, cos, pi, sin, symbols +from dataclasses import dataclass +# Base classes are needed at class definition from psydac.feec.polar.examples.polar_model_2d import PolarModel2D from psydac.linalg.basic import LinearOperator -from psydac.linalg.stencil import StencilMatrix, StencilVector -from psydac.utilities.operators import Laplacian # ============================================================================== @@ -56,6 +52,8 @@ class Poisson2D(PolarModel2D): """ def __init__(self, domain_log, mapping, phi_log, rho_log): + from pyccel import lambdify + super().__init__(domain_log, mapping) self._domain_log = domain_log @@ -93,8 +91,10 @@ def disk(R, shift_D): and source term. """ + import numpy as np from sympde.topology.analytical_mapping import TargetMapping from sympde.topology.domain import Square + from sympy import cos, pi, sin, symbols domain_log = Square("Omega", bounds1=(0, R), bounds2=(0, 2 * np.pi)) params = dict(c1=shift_D * R * R, c2=0, k=0, D=shift_D) @@ -134,8 +134,12 @@ def target(): $\phi(x,y) = (1 - s^8)\sin(k_x(x - 0.5))\cos(k_y y)$. """ + import numpy as np from sympde.topology.analytical_mapping import TargetMapping from sympde.topology.domain import Square + from sympy import Rational, cos, pi, sin + + from psydac.utilities.operators import Laplacian domain_log = Square("Omega", bounds1=(0, 1), bounds2=(0, 2 * np.pi)) params = dict(c1=0, c2=0, k=Rational(3, 10), D=Rational(2, 10)) @@ -167,8 +171,12 @@ def czarny(): $\phi(x,y) = (1 - s^8)\sin(\pi x)\cos(\pi y)$. """ + import numpy as np from sympde.topology.analytical_mapping import CzarnyMapping from sympde.topology.domain import Square + from sympy import Rational, cos, pi, sin + + from psydac.utilities.operators import Laplacian domain_log = Square("Omega", bounds1=(0, 1), bounds2=(0, 2 * np.pi)) params = dict(c1=0, c2=0, eps=Rational(1, 5), b=Rational(7, 5)) @@ -240,6 +248,7 @@ def __init__(self, S, M, P, alpha): C0PolarProjection_V0, C1PolarProjection_U0, ) + from psydac.linalg.stencil import StencilMatrix assert isinstance(S, StencilMatrix) assert isinstance(M, StencilMatrix) @@ -257,6 +266,8 @@ def __init__(self, S, M, P, alpha): self.W0 = W0 def dot(self, x, out=None): + from psydac.linalg.stencil import StencilVector + if out is None: y = self.M._domain.zeros() else: @@ -339,7 +350,7 @@ def compute_errors(phi, phi_ref, M, S): ErrorDiagnostics Reference L2 and H1 norms and relative L2 and H1 errors. """ - + import numpy as np # L2 and H1 norms ref_l2_2 = M.dot_inner(phi_ref.coeffs, phi_ref.coeffs) @@ -386,6 +397,8 @@ def plot_solution(use_spline_mapping, model, ncells, periodic, V0_h, refine=10): Refinement factor for plotting. """ import matplotlib.pyplot as plt + import numpy as np + from mpi4py import MPI from psydac.cad.geometry import Geometry from psydac.ddm.cart import DomainDecomposition @@ -488,7 +501,10 @@ def run_poisson_2d( mpi_comm, # given by function 'parallel_run_from_cli' ): import os + from time import sleep, time + import numpy as np + from mpi4py import MPI from sympde.calculus import dot, grad from sympde.expr import BilinearForm, LinearForm, integral from sympde.topology import ScalarFunctionSpace, elements_of From 3b324b3821dca29be6b0526a28b82348ad32e293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 6 Oct 2026 12:42:08 +0200 Subject: [PATCH 67/70] Allow Geometry.__init__ without mappings The `mappings` parameter defaults to None, meaning that no patch has a mapping, but the type check rejected None. The code handling None was therefore never reached, and it would have failed anyway for single-patch domains, where `domain.interior` is not iterable. Accept None in the type check and build the default dictionary from `domain.interior_names`. Add `test_geometry_init_without_mappings` for single- and two-patch domains. --- psydac/cad/geometry.py | 4 ++-- psydac/cad/tests/test_geometry.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/psydac/cad/geometry.py b/psydac/cad/geometry.py index 5889231f0..539b5d092 100644 --- a/psydac/cad/geometry.py +++ b/psydac/cad/geometry.py @@ -93,7 +93,7 @@ def __init__(self, assert isinstance(pdim, int) assert isinstance(domain, Domain) assert isinstance(ncells, dict) - assert isinstance(mappings, dict) + assert isinstance(mappings, (NoneType, dict)) assert isinstance(periodic, (NoneType, dict)) assert isinstance(comm, (NoneType, MPI.Intracomm)) assert isinstance(mpi_dims_mask, (NoneType, Iterable)) @@ -126,7 +126,7 @@ def __init__(self, # Check sanity of mappings if mappings is None: - mappings = {itr.name : None for itr in domain.interior} + mappings = {name : None for name in interior_names} else: assert set(mappings.keys()) == set_interior_names assert all(isinstance(m, (BasicCallableMapping, NoneType)) for m in mappings.values()) diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index 1811fde14..b7a4dd804 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -271,6 +271,24 @@ def test_from_topological_domain(): assert geo_from_domain.ddm.starts == expected_starts assert geo_from_domain.ddm.ends == expected_ends +# ============================================================================== +@pytest.mark.parametrize('npatches', [1, 2]) +def test_geometry_init_without_mappings(npatches: int) -> None: + + if npatches == 1: + domain = Square(name='A') + else: + A = Square('A', bounds1=(0, 1), bounds2=(0, 1)) + B = Square('B', bounds1=(1, 2), bounds2=(0, 1)) + domain = Domain.join(patches=[A, B], + connectivity=[((0, 0, 1), (1, 0, -1), 1)], + name='Omega') + + ncells = {name: [4, 4] for name in domain.interior_names} + geo = Geometry(domain, pdim=2, ncells=ncells) + + assert geo.mappings == {name: None for name in domain.interior_names} + #============================================================================== @pytest.mark.parametrize( 'ncells', [[8,8], [12,12], [14,14]] ) @pytest.mark.parametrize( 'degree', [[2,2], [3,2], [2,3], [3,3], [4,4]] ) From 194e8659db1720298a65342ed634ef96935ec3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 6 Oct 2026 12:43:30 +0200 Subject: [PATCH 68/70] Remove obsolete __main__ demo from psydac.cad.cad The demo used an API that no longer exists (`Geometry(patches=...)`, `geo.patches`) and a geometry file that is not in the repository. The functions in this module will be covered by unit tests in `psydac.cad.tests.test_cad`. --- psydac/cad/cad.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/psydac/cad/cad.py b/psydac/cad/cad.py index 27457fde8..cd18ff4db 100644 --- a/psydac/cad/cad.py +++ b/psydac/cad/cad.py @@ -203,16 +203,3 @@ def refine(mapping, axis, values): return NurbsMapping( *fields ) return SplineMapping( *fields ) - - - -###################################### -if __name__ == '__main__': - from psydac.cad.geometry import Geometry - - geo = Geometry('square_0.h5') - mapping = geo.patches[0] - new = translate(mapping, [1., 0., 0.]) - - geo = Geometry(patches=[mapping, new]) - geo.export('square_mp.h5') From 2db333c8384b65456c6f56c7541b1ab03ab1fafb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 6 Oct 2026 17:49:39 +0200 Subject: [PATCH 69/70] Check user-given domain in Geometry.from_file `discretize(domain, filename=...)` ignored the user's domain and used the one read from the geometry file. Add an optional `domain` argument to `Geometry.from_file` (and `read`), which `discretize_domain` now passes. The given domain is kept as the geometry's domain, after checking that it matches the file: each patch must have a non-analytical mapping (an analytical one would be used in assembly instead of the spline mapping), and the dimension, patch names, parametric bounds, and interfaces with their orientations must agree. Properties are compared one by one to work around pyccel/sympde#197 and pyccel/sympde#198. Update the tests in `psydac.api.ast.tests`, which passed a domain that does not match the geometry file, and add tests for accepted and rejected domains on geometry files generated in a temporary directory. --- psydac/api/ast/tests/test_poisson.py | 8 +-- psydac/api/ast/tests/test_system_1.py | 6 +- psydac/api/ast/tests/test_system_2.py | 6 +- psydac/api/ast/tests/test_system_3.py | 6 +- psydac/api/discretization.py | 2 +- psydac/cad/geometry.py | 80 ++++++++++++++++++++++++--- psydac/cad/tests/test_geometry.py | 57 ++++++++++++++++++- 7 files changed, 142 insertions(+), 23 deletions(-) diff --git a/psydac/api/ast/tests/test_poisson.py b/psydac/api/ast/tests/test_poisson.py index d6470a32f..c6025cd32 100644 --- a/psydac/api/ast/tests/test_poisson.py +++ b/psydac/api/ast/tests/test_poisson.py @@ -12,8 +12,7 @@ from sympde.calculus import grad, dot from sympde.topology import ScalarFunctionSpace from sympde.topology import elements_of, LogicalExpr -from sympde.topology import Square -from sympde.topology import Mapping, IdentityMapping, PolarMapping +from sympde.topology import Domain from sympde.expr import integral from sympde.expr import LinearForm from sympde.expr import BilinearForm @@ -38,9 +37,8 @@ #============================================================================== def test_codegen(): - domain = Square() - M = Mapping('M',2) - domain = M(domain) + domain = Domain.from_file(filename) + M = domain.mapping V = ScalarFunctionSpace('V', domain) u,v = elements_of(V, names='u,v') diff --git a/psydac/api/ast/tests/test_system_1.py b/psydac/api/ast/tests/test_system_1.py index 95fc81744..3694b5503 100644 --- a/psydac/api/ast/tests/test_system_1.py +++ b/psydac/api/ast/tests/test_system_1.py @@ -12,7 +12,7 @@ from sympde.topology import VectorFunctionSpace from sympde.topology import element_of from sympde.topology import Square -from sympde.topology import Mapping#, IdentityMapping, PolarMapping +from sympde.topology import Domain, Mapping from sympde.expr import integral from sympde.expr import LinearForm from sympde.expr import BilinearForm @@ -61,7 +61,9 @@ def test_codegen(): h1norm_F = SemiNorm(error, domain, kind='h1') # Create computational domain from topological domain - domain_h = discretize(domain, filename=filename) + # The spaces are defined on the logical domain, but the geometry needs + # the mapped domain defined in the file + domain_h = discretize(Domain.from_file(filename), filename=filename) # Discrete spaces Vh = discretize(V, domain_h) diff --git a/psydac/api/ast/tests/test_system_2.py b/psydac/api/ast/tests/test_system_2.py index 72ea2dbcd..3aecbddeb 100644 --- a/psydac/api/ast/tests/test_system_2.py +++ b/psydac/api/ast/tests/test_system_2.py @@ -12,7 +12,7 @@ from sympde.topology import ScalarFunctionSpace, VectorFunctionSpace from sympde.topology import element_of from sympde.topology import Square -from sympde.topology import Mapping +from sympde.topology import Domain, Mapping from sympde.expr import integral from sympde.expr import LinearForm from sympde.expr import BilinearForm @@ -59,7 +59,9 @@ def test_codegen(): l = LinearForm((q,v), int_0(f1*q[0]+f2*q[1]+v)) # Create computational domain from topological domain - domain_h = discretize(domain, filename=filename) + # The spaces are defined on the logical domain, but the geometry needs + # the mapped domain defined in the file + domain_h = discretize(Domain.from_file(filename), filename=filename) # Discrete spaces Vh = discretize(V1*V2, domain_h) diff --git a/psydac/api/ast/tests/test_system_3.py b/psydac/api/ast/tests/test_system_3.py index d5bc276a4..576df45b4 100644 --- a/psydac/api/ast/tests/test_system_3.py +++ b/psydac/api/ast/tests/test_system_3.py @@ -12,7 +12,7 @@ from sympde.topology import VectorFunctionSpace from sympde.topology import element_of from sympde.topology import Square -from sympde.topology import Mapping#, IdentityMapping, PolarMapping +from sympde.topology import Domain, Mapping from sympde.expr import integral from sympde.expr import BilinearForm from sympde.expr.evaluation import TerminalExpr @@ -54,7 +54,9 @@ def test_codegen(): b = BilinearForm((u,v), int_0(div(v)*div(u))) # Create computational domain from topological domain - domain_h = discretize(domain, filename=filename) + # The spaces are defined on the logical domain, but the geometry needs + # the mapped domain defined in the file + domain_h = discretize(Domain.from_file(filename), filename=filename) # Discrete spaces Vh = discretize(V, domain_h) diff --git a/psydac/api/discretization.py b/psydac/api/discretization.py index 98a1967aa..00e645461 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.from_file(filename, comm=comm, mpi_dims_mask=mpi_dims_mask) + return Geometry.from_file(filename, domain=domain, 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/cad/geometry.py b/psydac/cad/geometry.py index 539b5d092..53f78d61e 100644 --- a/psydac/cad/geometry.py +++ b/psydac/cad/geometry.py @@ -172,6 +172,7 @@ def __init__(self, def from_file(cls, filename : str, *, + domain : Domain = None, comm : MPI.Intracomm = None, mpi_dims_mask : Iterable[bool] = None): @@ -183,6 +184,14 @@ def from_file(cls, filename: str The path to the geometry file. + domain: sympde.topology.Domain, optional + The topological domain of the geometry. It must match the domain + defined in the file: same dimension, patch names, patches and + interfaces. Each patch must have a non-analytical mapping, to which + the corresponding spline mapping is attached; hence the same domain + should not be used with different geometry files. If not given, the + domain is created from the file. + comm: MPI.Intracomm, optional The MPI intra-communicator. @@ -195,9 +204,15 @@ def from_file(cls, ------- Geometry The new instance. + + Raises + ------ + ValueError + If the given domain does not match the domain defined in the file, + or if one of its patches has no mapping or an analytical mapping. """ geo = super().__new__(cls) - geo.read(filename, comm=comm, mpi_dims_mask=mpi_dims_mask) + geo.read(filename, comm=comm, mpi_dims_mask=mpi_dims_mask, domain=domain) return geo #-------------------------------------------------------------------------- @@ -331,21 +346,22 @@ def mappings(self): def __len__(self): return len(self.domain) - def read(self, filename, comm=None, mpi_dims_mask=None): + def read(self, filename, comm=None, mpi_dims_mask=None, domain=None): # ... check extension of the file _, ext = os.path.splitext(filename) if ext != '.h5': raise ValueError('> Only h5 files are supported') # ... - # read the topological domain - domain = Domain.from_file(filename) - connectivity = construct_connectivity(domain) - - if len(domain) == 1: - interiors = [domain.interior] + # read the topological domain, or check the given one against it + file_domain = Domain.from_file(filename) + if domain is None: + domain = file_domain else: - interiors = list(domain.interior.args) + _check_domain_matches_file(domain, file_domain, filename) + + connectivity = construct_connectivity(domain) + interiors = _get_interiors(domain) if comm is not None: kwargs = dict(driver='mpio', comm=comm) if comm.size > 1 else {} @@ -972,3 +988,49 @@ def _read_patch(lines, i_patch, n_lines_per_patch, list_begin_line): nrb = NURBS(knots, control=points, weights=W) return nrb + +#============================================================================== +def _get_interiors(domain : Domain) -> list: + """Return the list of interior patches of a (multi-patch) domain.""" + return [domain.interior] if len(domain) == 1 else list(domain.interior.args) + +def _get_interface_keys(domain : Domain) -> set: + """Return the interfaces of a domain as hashable tuples, with orientation.""" + interfaces = domain.interfaces + if interfaces is None: + return set() + if not isinstance(interfaces, Union): + interfaces = [interfaces] + return {(i.minus.domain.name, i.minus.axis, i.minus.ext, + i.plus .domain.name, i.plus .axis, i.plus .ext, i.ornt) for i in interfaces} + +def _check_domain_matches_file(domain : Domain, file_domain : Domain, filename : str): + """Raise a ValueError if the domain cannot be used with the geometry file.""" + + # Properties are compared one by one, because domain == file_domain raises + # an error for multi-patch domains (see pyccel/sympde#197) + + # Analytical mappings would be used in assembly instead of the spline ones + for patch in _get_interiors(domain): + if patch.mapping is None or patch.mapping.is_analytical: + raise ValueError(f"Patch {patch.name} must have a non-analytical " + f"mapping to be used with geometry file {filename}") + + if domain.dim != file_domain.dim: + raise ValueError(f"Domain dimension {domain.dim} does not match " + f"dimension {file_domain.dim} in file {filename}") + + if domain.interior_names != file_domain.interior_names: + raise ValueError(f"Patch names {domain.interior_names} do not match " + f"{file_domain.interior_names} in file {filename}") + + # Patch equality ignores the parametric bounds (see pyccel/sympde#198) + for patch, file_patch in zip(_get_interiors(domain), _get_interiors(file_domain)): + bounds = (patch .logical_domain.min_coords, patch .logical_domain.max_coords) + file_bounds = (file_patch.logical_domain.min_coords, file_patch.logical_domain.max_coords) + if bounds != file_bounds: + raise ValueError(f"Parametric bounds {bounds} of patch {patch.name} " + f"do not match {file_bounds} in file {filename}") + + if _get_interface_keys(domain) != _get_interface_keys(file_domain): + raise ValueError(f"Interfaces of the domain do not match those in file {filename}") diff --git a/psydac/cad/tests/test_geometry.py b/psydac/cad/tests/test_geometry.py index b7a4dd804..1c9625488 100644 --- a/psydac/cad/tests/test_geometry.py +++ b/psydac/cad/tests/test_geometry.py @@ -9,11 +9,11 @@ import numpy as np from mpi4py import MPI -from sympde.topology import Domain, Line, Square, Cube, Mapping +from sympde.topology import Domain, Line, Square, Cube, Mapping, IdentityMapping from psydac.cad.geometry import Geometry, export_nurbs_to_hdf5, refine_nurbs from psydac.cad.geometry import import_geopdes_to_nurbs -from psydac.cad.cad import elevate, refine +from psydac.cad.cad import elevate, refine, translate from psydac.cad.gallery import quart_circle, circle from psydac.mapping.discrete import SplineMapping, NurbsMapping from psydac.mapping.discrete_gallery import discrete_mapping @@ -289,6 +289,59 @@ def test_geometry_init_without_mappings(npatches: int) -> None: assert geo.mappings == {name: None for name in domain.interior_names} +#============================================================================== +def make_domain(npatches: int, *, ornt: int = 1) -> Domain: + """Create a domain made of one or two unit squares, with generic mappings.""" + if npatches == 1: + return Mapping('G', dim=2)(Square('P')) + + A = Mapping('GA', dim=2)(Square('PA')) + B = Mapping('GB', dim=2)(Square('PB')) + return Domain.join([A, B], [((0, 0, 1), (1, 0, -1), ornt)], 'Omega') + +def export_geometry(domain: Domain, filename: str) -> None: + """Export a spline geometry on the domain, with patches side by side along x.""" + F = discrete_mapping('identity', ncells=[2, 2], degree=[2, 2]) + names = domain.interior_names + mappings = {name: translate(F, [float(i), 0.0]) for i, name in enumerate(names)} + ncells = {name: [2, 2] for name in names} + Geometry(domain, pdim=2, ncells=ncells, mappings=mappings).export(filename) + +#============================================================================== +@pytest.mark.parametrize('npatches', [1, 2]) +@pytest.mark.xdist_group('h5py') +def test_geometry_from_file_with_domain(npatches: int, tmp_path) -> None: + + domain = make_domain(npatches) + filename = str(tmp_path / 'geo.h5') + export_geometry(domain, filename) + + geo = Geometry.from_file(filename, domain=domain) + + # The given domain is kept, and the spline mappings are attached to it + assert geo.domain is domain + patches = [domain.interior] if npatches == 1 else domain.interior.args + for patch, F in zip(patches, geo.mappings.values()): + assert patch.mapping.get_callable_mapping() is F + +#============================================================================== +@pytest.mark.parametrize(('npatches', 'make_wrong_domain', 'message'), [ + (1, lambda: IdentityMapping('G', dim=2)(Square('P')), 'non-analytical'), + (1, lambda: Square('P'), 'non-analytical'), + (1, lambda: Mapping('G', dim=3)(Cube('P')), 'dimension'), + (1, lambda: Mapping('H', dim=2)(Square('P')), 'Patch names'), + (1, lambda: Mapping('G', dim=2)(Square('P', bounds1=(0, 2))), 'Parametric bounds'), + (2, lambda: make_domain(2, ornt=-1), 'Interfaces'), +], ids=['analytical', 'no-mapping', 'dimension', 'names', 'bounds', 'orientation']) +@pytest.mark.xdist_group('h5py') +def test_geometry_from_file_with_wrong_domain(npatches: int, make_wrong_domain, message: str, tmp_path) -> None: + + filename = str(tmp_path / 'geo.h5') + export_geometry(make_domain(npatches), filename) + + with pytest.raises(ValueError, match=message): + Geometry.from_file(filename, domain=make_wrong_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]] ) From d3a29737d218d15a655344ba2f4b51745a921ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yaman=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 6 Oct 2026 17:53:20 +0200 Subject: [PATCH 70/70] Write plot of multipatch Poisson test to temporary directory `test_poisson_2d_3_patches_dirichlet_2` saved `uh_multipatch_poisson.pdf` in the working directory, leaving an untracked file behind after each test run. Save it in pytest's `tmp_path` instead; the plot call still serves as a smoke test of `plot_field`. --- psydac/api/tests/test_2d_multipatch_mapping_poisson.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/psydac/api/tests/test_2d_multipatch_mapping_poisson.py b/psydac/api/tests/test_2d_multipatch_mapping_poisson.py index 448e1f4c4..3f7a0de19 100644 --- a/psydac/api/tests/test_2d_multipatch_mapping_poisson.py +++ b/psydac/api/tests/test_2d_multipatch_mapping_poisson.py @@ -131,7 +131,7 @@ def test_poisson_2d_2_patches_dirichlet_1(): assert ( abs(h1_error - expected_h1_error) < 1e-7 ) #------------------------------------------------------------------------------ -def test_poisson_2d_3_patches_dirichlet_2(): +def test_poisson_2d_3_patches_dirichlet_2(tmp_path): mapping_1 = IdentityMapping('M1', 2) mapping_2 = PolarMapping ('M2', 2, c1 = 0., c2 = 0.5, rmin = 0., rmax=1.) @@ -155,7 +155,7 @@ def test_poisson_2d_3_patches_dirichlet_2(): l2_error, h1_error, uh = run_poisson_2d(solution, f, domain, ncells=[2**2,2**2], degree=[2,2]) - plot_fn=f'uh_multipatch_poisson.pdf' + plot_fn = str(tmp_path / 'uh_multipatch_poisson.pdf') plot_field(fem_field=uh, Vh=uh.space, domain=domain, title='uh', filename=plot_fn, hide_plot=True) expected_l2_error = 0.0019402242901236006