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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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/47] 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]