Skip to content

Update Multipatch API - #188

Open
FrederikSchnack wants to merge 13 commits into
masterfrom
new_multipatch_api
Open

FrederikSchnack wants to merge 13 commits into
masterfrom
new_multipatch_api

Conversation

@FrederikSchnack

@FrederikSchnack FrederikSchnack commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Generalize symbolic multipatch topology

Summary

This PR removes the assumption that connected faces have the same normal axis and
properly defines orientations for 1D, 2D, and 3D interfaces in SymPDE's multipatch topology.

It also adds stable topology inspection APIs, preserves interface metadata
through mapping and serialization, supports repeated and self-interfaces,
computes shared vertices in 2D and 3D, and introduces reusable topology
plotting and multipatch gallery modules, that were exclusively in PSYDAC before.

The changes are confined to SymPDE, this introduces necessary changes in PSYDAC that are already mostly prepared.

Fixes #73, fixes #140, fixes #143, fixes #144.

Interface topology

Domain.join() accepts each interface as:

(minus_side, plus_side, orientation)
(minus_side, plus_side, orientation, name)

Each side is (patch, normal_axis, extremity), where patch is a patch object
or its position in the patches sequence and extremity is -1 or +1.
interfaces= is an alias for connectivity=; passing both is an error.

For example, two cube faces with different normal axes can be joined as:

from sympde.api import Cube, Domain

A = Cube('A')
B = Cube('B')

domain = Domain.join(
    patches=[A, B],
    interfaces=[(
        (A, 0, +1),
        (B, 1, -1),
        (-1, +1, -1),
    )],
    name='Omega',
)

Orientation is read from the ordered minus side to the ordered plus side:

Patch dimension Orientation Meaning
1D None An endpoint has no tangential coordinate
2D +1 or -1 Preserve or reverse the edge coordinate
3D (flag, sign1, sign2) Preserve/exchange the two face axes and orient each correspondence

All 3D entries are +1 or -1. flag=+1 preserves the order of the two
tangential axes; flag=-1 exchanges them.

Interface.axis_map exposes the decoded, dimension-independent
correspondence as (minus_axis, plus_axis, direction) entries. For the
example above:

interface = next(iter(domain.interface_map.values()))

assert interface.orientation == (-1, +1, -1)
assert interface.axis_map == ((1, 2, +1), (2, 0, -1))

The face-normal correspondence remains available from
interface.minus.axis and interface.plus.axis; it is not repeated in
axis_map. This is the orientation format currently and later used in PSYDAC.

Supported topology

The joined-domain implementation now supports:

  • interfaces between faces with different normal axes;
  • all two edge orientations and all eight quadrilateral-face orientations;
  • more than one interface between the same pair of patches;
  • periodic and self-interfaces;
  • explicit interface names, preserved during serialization;
  • induced subdomains that retain internal interfaces and expose cut
    interfaces as exterior boundaries; and
  • transitive shared-vertex classes in both 2D and 3D.

Generated names for repeated patch pairs use A|B, A|B#2, and so on.
Explicit names are validated globally before generated names are assigned.

An empty one-patch join still returns the original patch. A one-patch join
with declared connectivity follows the normal join path, allowing a genuine
self-interface. Passing an already joined multipatch domain as one atomic
patch is rejected explicitly.

Mapped domains preserve the logical interface, compact orientation,
InterfaceMapping, and patch mappings. Interface measures use the minus side
as their canonical parameterization instead of assuming one shared normal
axis.

Stable inspection API

The following properties provide cardinality-stable views:

  • domain.patches: tuple of top-dimensional patches;
  • domain.interface_map: interface-name-to-Interface mapping;
  • domain.exterior_sides: tuple of exterior faces;
  • domain.shared_vertices: tuple of shared-vertex classes;
  • Boundary.patch: owning patch; and
  • Boundary.normal_axis: face-normal logical axis.

PatchVertex and SharedVertex are descriptive aliases for the existing
patch-corner and shared-corner symbolic types.

The established domain.interior, domain.boundary, and domain.corners
properties remain available.

Serialization

Serialized interfaces now always contain the minus side, plus side, and
compact orientation. Interface dictionary keys are restored as interface
names instead of being regenerated.

Exterior boundaries use one list representation, including an empty list for
fully closed domains. The reader still accepts the historical single-boundary
dictionary and can derive missing boundary metadata from connectivity.
Stored exterior boundaries are checked against the derived topology.

Files whose interfaces omit orientation are rejected because orientation
cannot be inferred reliably from axes or extremities. Such files must be
regenerated or migrated with an explicitly confirmed orientation for every
interface.

Topology inspection and plotting

The new module sympde.utilities.plotting provides:

  • collect_topology_vertices();
  • format_topology();
  • print_topology(); and
  • plot_domain().

collect_topology_vertices(), print_topology() and plot_domain() are also
available in sympde.api. The old plotting functions in
sympde.utilities.utils are removed.

plot_domain() handles ordinary and topology-annotated 2D and 3D plots and
returns its Matplotlib figure. Topology mode displays patch identities,
interface sides, signed tangential-axis correspondences, and vertex
incidence. Matplotlib is only imported when a domain is plotted, so importing
sympde.utilities.plotting or sympde.api does not load it.

Multipatch gallery

This formerly was in psydac/feec/multipatch_domain_utilities.py.

sympde.topology.multipatch_gallery provides explicit symbolic builders for various multipatch domains and includes a integrated plotting function.

The gallery can be plotted from Python with plot_multipatch_domain() or run
as a module:

python -m sympde.topology.multipatch_gallery two_cube \
    --topology --no-show --output two_cube.png

TransposedPolarMapping is now part of SymPDE's analytical mappings because it is
a reusable coordinate transformation rather than a discretization detail.

Breaking changes

Previous API or data Replacement
Boundary.join(other, ornt=...) Boundary.join(other, orientation)
Interface(..., ornt=...) Interface(..., orientation)
interface.ornt interface.orientation
interface.axis interface.minus.axis and interface.plus.axis
Edge Boundary
interface connectivity dictionaries (minus, plus, orientation[, name]) tuples
serialized interface without orientation explicit compact orientation
plot_2d(), plot_3d(), plot_2d_single_patch(), plot_3d_single_patch() plot_domain()
sympde.utilities.utils.plot_domain sympde.api.plot_domain

No default orientation is introduced. Callers must state the intended
topology explicitly.

Integration with the public API and the SymPy upgrade

This branch was merged with master after #193 (SymPy upgrade) and #194
(public API), and adapted to their conventions:

  • Interface stores mapping and logical_domain in its args and accepts
    them positionally, so that it can be reconstructed with
    interface.func(*interface.args).
  • The new modules, tests and documentation import objects from their defining
    modules or from sympde.api, and the package initializers stay empty.
  • The docstring of Domain.join() keeps the doctests of this branch, which
    replace the code block added in Change documentation to Github Pages #191.
  • The CHANGELOG describes the changes of this PR. Duplicated and outdated
    entries in its "Unreleased" section are removed.

Current limitations

  • SymPDE validates topology but does not numerically prove that mapped faces
    coincide or satisfy the declared parameterization.
  • Non-manifold boundary reuse is not rejected in every case.
  • Compact orientations are defined for logical dimensions 1, 2, and 3.

Some plots

Represent interface orientation explicitly and expose a signed tangential axis map that supports cross-axis 3D faces.

Support repeated and self interfaces, stable multipatch views, induced subdomains, dimension-independent shared vertices, mapped interface metadata, collision-safe names, and robust topology serialization. Tighten public validation and cover the new contract with logical, mapped, subset, periodic, and round-trip tests.
Move domain plotting into a dedicated lazy-loaded module and provide shared 2D/3D rendering with optional interface, axis-map, and vertex annotations.

Add textual topology inspection, reusable 2D/3D gallery domain builders, an executable gallery CLI, and integration tests without making Matplotlib an import-time dependency of sympde.
Use MathJax for rendered mathematics, configure the existing FEEC bibliography explicitly, and remove obsolete imgmath and LaTeX configuration.

Also normalize the TerminalExpr parameter documentation so strict autodoc builds remain warning-free.
Add a user guide for constructing, inspecting, validating, plotting, subsetting, and serializing logical and mapped multipatch domains in 1D, 2D, and 3D.

Document the downstream axis-map contract, intentional API and file-format migrations, Edge removal, current limitations, and validation responsibilities.
Comment thread sympde/topology/domain.py
Comment on lines +697 to +698
if not all(p.dim == patches[0].dim for p in patches):
raise ValueError('all patches must have the same logical dimension')

@yguclu yguclu Oct 15, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also check that all patches have the same physical dimension pdim? join currently accepts e.g. a planar square joined with a surface in 3D, which gives an inconsistent domain (see the updated issue #140, which this PR closes):

A = Mapping('FA', ldim=2, pdim=2)(Square('A'))
B = Mapping('FB', ldim=2, pdim=3)(Square('B'))
Domain.join([A, B], [((0, 0, 1), (1, 0, -1), 1)], 'Omega')   # accepted

A patch without a mapping has the same physical and logical dimension:

Suggested change
if not all(p.dim == patches[0].dim for p in patches):
raise ValueError('all patches must have the same logical dimension')
raise ValueError('all patches must have the same logical dimension')
pdims = {p.mapping.pdim if p.mapping is not None else p.dim for p in patches}
if len(pdims) > 1:
raise ValueError('all patches must have the same physical dimension')

Please also mention this requirement in the docstring, next to the one on the logical dimension.

A unit test in the PR's test suite, joining a pdim=2 and a pdim=3 patch and expecting ValueError, would also be a good idea.

@yguclu yguclu mentioned this pull request Oct 20, 2026
Adapt the multipatch changes to the conventions introduced by #193 and #194:

- Import objects from their defining modules in the tests and in
  multipatch_gallery.py, since the package initializers are now empty.
- Empty sympde/utilities/__init__.py again, and export plot_domain in
  sympde.api from sympde.utilities.plotting.
- Restore the version of sympde/utilities/utils.py from this branch, which
  only contains lambdify_sympde: the merge had brought back the old
  plotting functions.
- Allow passing mapping and logical_domain of an Interface positionally,
  so that it can be reconstructed from its args.
- Use orientation instead of ornt in test_structural_arguments_are_sympy_objects.
Move the Matplotlib imports from the top of sympde/utilities/plotting.py into
_plot_domain_2d and _plot_domain_3d, so that importing the module (or
sympde.api) does not load Matplotlib (see #180).
Import from sympde.api in the examples of Interface and Domain.join, and
refer to Domain by its defining module in the plotting docstrings, since
the package initializers are empty after #194.
Import from sympde.api in the examples of doc/multipatch.rst, and add
collect_topology_vertices and print_topology to sympde.api, since the
documentation uses them. Point the migration table to sympde.api.plot_domain.
Describe the changes of this PR, and remove duplicated and outdated entries
from the Unreleased section.
Resolve the merge conflict in the docstring of Domain.join: the doctests of
this branch replace the code block that #191 had added on master, since
they render correctly and show the same 2D connectivity.
@yguclu

yguclu commented Oct 20, 2026

Copy link
Copy Markdown
Member

Thanks for this PR!

I have merged master into your branch and tried to resolve all the conflicts... I hope I have not introduced big problems! Accordingly, I have updated the PR description (see the new section "Integration with the public API and the SymPy upgrade"). I also removed the disclaimer: since the PR is squash-merged, the description becomes the commit message, where it is not really useful. I left the section "Some plots" where it is, but I will make sure that it is not included in the final merge commit.

Could you also have a look at #140? Domain.join still does not check that all patches have the same physical dimension pdim

@yguclu

yguclu commented Oct 22, 2026

Copy link
Copy Markdown
Member

Will this PR also close #114?

@FrederikSchnack

Copy link
Copy Markdown
Contributor Author

Will this PR also close #114?

Yes, it should. See the two patch twisted strip example. I will update the description.
I will get back to this PR now, and also look into #140. And I'm not sure if this is also a good place for #200 .

@campospinto
campospinto self-requested a review October 22, 2026 12:48
@yguclu yguclu self-assigned this Oct 22, 2026
@yguclu

yguclu commented Oct 22, 2026

Copy link
Copy Markdown
Member

Will this PR also close #114?

Yes, it should. See the two patch twisted strip example. I will update the description. I will get back to this PR now, and also look into #140. And I'm not sure if this is also a good place for #200 .

Issue #200 is explicitly fixed by the small PR #201, which can be merged very soon. I will ask for a small check to be added to todict in the same PR, which should not conflict with your (much larger) PR

@yguclu

yguclu commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

I have just created the new issue #203. Naturally, that should be fixed in this PR once #201 is merged

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants