Repository navigation
Update Multipatch API - #188
FrederikSchnack wants to merge 25 commits into
Conversation
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.
| if not all(p.dim == patches[0].dim for p in patches): | ||
| raise ValueError('all patches must have the same logical dimension') |
There was a problem hiding this comment.
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') # acceptedA patch without a mapping has the same physical and logical dimension:
| 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.
There was a problem hiding this comment.
Thanks, done in 1352c44: join now rejects patches with different physical dimensions, the requirement is in the docstring and in the multipatch docs page, and test_domain_join_rejects_different_physical_dimensions covers it.
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.
|
Thanks for this PR! I have merged Could you also have a look at #140? |
|
Will this PR also close #114? |
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 |
Joining a planar patch with a surface in 3D created an inconsistent domain. Check that all patches have the same physical dimension, which is the logical dimension for a patch without a mapping, and document the requirement in the docstring and in the multipatch documentation. Fixes #140.
A domain whose patches are InteriorDomain objects without a dtype was exported with `dtype: [None, None]`, and reading it back failed at `assert all(dtype)`. Such patches have no parametric box from which Domain.join could derive the boundary, and their boundaries may have no axis or extremity. Rebuild these domains directly from the stored interiors, boundaries and interfaces, keeping their names and orientations. A file in which only some patches have a dtype is rejected, and mapped patches without a dtype are not supported. Fixes #144.
The interfaces were stored in `args` in the order in which they were given, so that two joins of the same patches with the interfaces in a different order compared unequal. Since Domain.from_file reads them sorted by name, a domain also compared unequal to itself after an export/from_file round trip whenever the interface names were not in alphabetical order. Store the interfaces sorted by name, and reject an iterable of interfaces with duplicate names, which used to silently drop all but the last one.
The logical interface names (A|B, A|B#2) were generated separately from the physical ones (F(A)|G(B), ...), in the order of the connectivity, and were not serialized. Domain.from_file reads the interfaces sorted by name and passes their stored names as explicit names, so after a round trip the logical names could end up on different interfaces. Explicit names were also not given to the logical interfaces. Generate one name per interface from the logical patch names, and give it to both the mapped and the logical interface. The names are then preserved by export/from_file, and joining mapped patches names the interfaces like mapping a joined logical domain.
Domain.shared_vertices and Domain.corners failed with an AttributeError when the interfaces join InteriorDomain objects without a parametric box, such as the domains read by Domain.from_file without a dtype. The vertices of such patches are not defined, so raise a ValueError that says so.
Domain.join accepted an interface joining a face with itself, and a face used in two interfaces, which gave an inconsistent topology and wrong shared-vertex classes. Reject both, and document the rule in the docstring and in the multipatch documentation. The test of a single exterior side relied on a face joined with itself. Since every interface of a joined domain uses two faces, use a topological domain for it, and move the check of missing boundary metadata to its own test with a valid join.
Domain.join accepted patches of which only some have a mapping. The joined domain then had no mapping and no logical domain, so the mappings of the mapped patches were silently dropped from the domain and its interfaces. Require either all patches or none to have a mapping.
In the solid torus built by build_torus_3d(hollow=False), the faces at minor radius zero collapse onto the centreline of the tube, but they remain exterior sides of the domain. They cannot be left out of the boundary, since Domain.join and Domain.from_file derive it from the connectivity, and joining them as an interface would not describe the collapse. Say in the docstring and in the documentation that they must be excluded when imposing boundary conditions.
Repeated interfaces between the same pair of patches were numbered A|B, A|B#2, ... in the order of the connectivity, so that two joins with the same unnamed interfaces in a different order still gave different domains. Number them in the order of their faces instead.
The 2D and 3D plots were each drawn by one long function, which mixed the sampling of the geometry with Matplotlib calls, duplicated the figure, legend and vertex code, and spread colours, line widths and font sizes over the code. Several things went wrong in the plots: - the plus side of an interface was drawn dashed exactly on top of its minus side, in the same colour, so that it and its arrow were hidden, and the labels I0- and I0+ overlapped; - the text describing the axis maps was placed at a fixed position, so that it overlapped the legend, ran off the figure for many interfaces, and was cut off on user-supplied axes; - patch colours repeated after 9 patches, also between neighbours; - a NumPy integer refinement was rejected. Now a plot model is built first, with the sampled faces, the exterior faces, the interfaces and the vertices, and the 2D and 3D drawing functions only draw it, with shared helpers and one style dictionary. The mappings are evaluated on arrays instead of point by point. The plots look different: - each interface side is drawn inside its own patch, the minus side solid and the plus side dashed, with a visible arrow head, and with a single label when the two sides coincide; - the exterior boundary is drawn in black and the other edges in grey; - patches get colours from the Okabe-Ito palette, which is safe for colour-blind readers, and neighbouring patches get different colours; - boundary vertices are white and interior vertices black; - the legend explains the line and vertex styles below the plot, and the axis maps are left to print_topology(); - the 3D box keeps a minimal height, so that a torus is not flattened. The signature of plot_domain() and its return value are unchanged.
Codacy reported the bare attribute access inside pytest.raises as a statement without effect.
The arrows of both interface sides followed their own tangential coordinates, so that the arrows of a reversing interface, such as the one of the twisted strip, pointed the same way and the twist could not be seen. Draw the arrows of the plus side along the images of the coordinates of the minus side instead: in 2D the plus arrow is reversed for a reversing interface, and in 3D all arrows are labelled with the coordinates of the minus face, so that the arrows of the two faces coincide when the declared orientation matches the geometry.
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, fixes #114.
Interface topology
Domain.join()accepts each interface as:Each side is
(patch, normal_axis, extremity), wherepatchis a patch objector its position in the
patchessequence andextremityis-1or+1.interfaces=is an alias forconnectivity=; passing both is an error.For example, two cube faces with different normal axes can be joined as:
Orientation is read from the ordered minus side to the ordered plus side:
None+1or-1(flag, sign1, sign2)All 3D entries are
+1or-1.flag=+1preserves the order of the twotangential axes;
flag=-1exchanges them.Interface.axis_mapexposes the decoded, dimension-independentcorrespondence as
(minus_axis, plus_axis, direction)entries. For theexample above:
The face-normal correspondence remains available from
interface.minus.axisandinterface.plus.axis; it is not repeated inaxis_map. This is the orientation format currently and later used in PSYDAC.Supported topology
The joined-domain implementation now supports:
interfaces as exterior boundaries; and
Generated names for repeated patch pairs use
A|B,A|B#2, and so on.They are numbered in the order of the faces, so they do not depend on the
order of the connectivity. For mapped patches they are built from the logical
patch names, and a mapped interface and its logical interface have the same
name. Explicit names are validated globally before generated names are
assigned. The interfaces of a domain are ordered by name, so that a domain
does not depend on the order of its connectivity and equals itself after an
export/from_file round trip.
Domain.join()rejects patches with different physical dimensions (#140),patches of which only some have a mapping, interfaces that join a face with
itself, and faces that are part of more than one interface.
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 sideas 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-Interfacemapping;domain.exterior_sides: tuple of exterior faces;domain.shared_vertices: tuple of shared-vertex classes;Boundary.patch: owning patch; andBoundary.normal_axis: face-normal logical axis.PatchVertexandSharedVertexare descriptive aliases for the existingpatch-corner and shared-corner symbolic types.
The established
domain.interior,domain.boundary, anddomain.cornersproperties 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.
Domains whose patches are
InteriorDomainobjects without adtype(#144)have no parametric box from which the boundary could be derived. Their
exterior boundaries and interfaces are restored as they were stored,
including boundaries without an axis or extremity. Such patches are only
supported without a mapping.
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.plottingprovides:collect_topology_vertices();format_topology();print_topology(); andplot_domain().collect_topology_vertices(),print_topology()andplot_domain()are alsoavailable in
sympde.api. The old plotting functions insympde.utilities.utilsare removed.plot_domain()handles ordinary and topology-annotated 2D and 3D plots andreturns 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.plottingorsympde.apidoes not load it.Multipatch gallery
This formerly was in psydac/feec/multipatch_domain_utilities.py.
sympde.topology.multipatch_galleryprovides 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 runas a module:
TransposedPolarMappingis now part of SymPDE's analytical mappings because it isa reusable coordinate transformation rather than a discretization detail.
Breaking changes
Boundary.join(other, ornt=...)Boundary.join(other, orientation)Interface(..., ornt=...)Interface(..., orientation)interface.orntinterface.orientationinterface.axisinterface.minus.axisandinterface.plus.axisEdgeBoundary(minus, plus, orientation[, name])tuplesplot_2d(),plot_3d(),plot_2d_single_patch(),plot_3d_single_patch()plot_domain()sympde.utilities.utils.plot_domainsympde.api.plot_domainNo 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
masterafter #193 (SymPy upgrade) and #194(public API), and adapted to their conventions:
Interfacestoresmappingandlogical_domainin itsargsand acceptsthem positionally, so that it can be reconstructed with
interface.func(*interface.args).modules or from
sympde.api, and the package initializers stay empty.Domain.join()keeps the doctests of this branch, whichreplace the code block added in Change documentation to Github Pages #191.
entries in its "Unreleased" section are removed.
TODO
Domain.ldimandDomain.pdimand build the physical coordinates frompdim. The physical-dimension check inDomain.join()and_physical_dimension()insympde/utilities/plotting.pywill then usedomain.pdim.Current limitations
coincide or satisfy the declared parameterization.
remain exterior sides and must be excluded when imposing boundary
conditions.
Some plots