Repository navigation
New mapping class hierarchy (sympde part) - #196
Draft
campospinto wants to merge 51 commits into
Draft
campospinto wants to merge 51 commits into
campospinto wants to merge 51 commits into
Conversation
…s a deprecation shell
campospinto
marked this pull request as draft
September 30, 2026 14:54
Construct function symbols and differential forms through Symbol so that SymPy initializes their assumptions and free-symbol state. Keep their SymPDE metadata in structural identity without exposing non-Basic values through args. Refs #106
Keep the transformed IntAdd as a SymPy expression instead of wrapping it in a Python list. Modern SymPy strictly sympifies constructor postprocessor results and rejected the list while checking form linearity. Refs #106
Convert string, numeric, container, and optional metadata stored in custom Basic.args to SymPy objects. Restore the public Python values through properties so callers keep the existing API while traversal, printing, hashing, and substitution can safely inspect expression trees. Also preserve integer DifferentialForm dimensions and the historical name-based identity of function symbols. Refs #106
Raise the minimum dependency to SymPy 1.12 for supported Python versions and cap it below 1.15 until the next compatibility review. Keep the regular CI matrix on the newest allowed release and add a Python 3.11 job pinned to the minimum. Refs #106
Use maintained SymPy utilities and native metaclass syntax. The compatibility module remains temporarily available to the legacy package exports until the public API layer removes them.
Accept normalized SymPy arguments in topology, equation, tensor-form, and multipatch-mapping constructors. Preserve structural metadata during rebuilding and cover func/xreplace round trips with regression tests.
Give function spaces structural equality and include them in scalar and vector function hashes. Equal names in different spaces now remain distinct symbols instead of collapsing during SymPy simplification.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR replaces the single
Mappingclass with a small hierarchy that keepstwo questions apart: can this mapping be used symbolically? and can it be evaluated at a point?
Today
Mappinganswers both. It is symbolic, and itbecomes point-evaluable only when it has
_expressionsor when a numericalmapping is attached to it after construction with
set_callable_mapping.The new root is
SymbolicMapping. Under it:DefinedMappingis for mappings that are symbolic and point-evaluable,with two concrete classes:
AnalyticMappingfor mappings given by formulas,DiscreteMappingfor mappings backed by a numerical mapping, such as apsydac spline.
StructuralMappingis for mappings that are symbolic only:InverseMapping,InterfaceMappingandMultiPatchMapping.The main user-facing gain: a spline geometry can now have its own symbolic
identity (
DiscreteMapping(F_h, 'F')). You can build a domain from it, joinpatches, and discretize, without mutating another object.
Companion PR: pyccel/psydac#TBD. It uses the new classes and depends on this
PR. sympde itself has no dependency on psydac.
Main changes
New classes (
sympde/topology/mapping.py, exported fromsympde.topology)SymbolicMapping(IndexedBase): the new root.SymbolicMapping('F', dim=2)is an undefined mapping. You can apply it to a domain and use it in forms,
and it carries the symbolic Jacobian and metric. It cannot be evaluated at a
point.
DefinedMapping(SymbolicMapping, BasicCallableMapping): an abstract class formappings that are symbolic and point-evaluable. As a shortcut,
DefinedMapping(callable, name)returns aDiscreteMapping.AnalyticMapping(DefinedMapping): the base class for mappings given byformulas. It turns its own
_expressionsinto NumPy functions the first timethey are needed, and evaluates both scalars and arrays. It is its own
numerical mapping, so
get_callable_mapping()returnsself. All 11 gallerymappings now derive from it (
IdentityMapping,PolarMapping,TargetMapping,CzarnyMapping,TorusMapping, …).DiscreteMapping(DefinedMapping): gives a symbolic name to any externalBasicCallableMapping, such as psydac'sSplineCallableMapping, andforwards every point evaluation to it.
is_analyticalisFalse, soassembly evaluates the geometry on the quadrature grid. The wrapped mapping
is fixed when the object is built and is part of its identity (it counts in
==andhash).has_callable_mapping()is a check that never raises.StructuralMapping(SymbolicMapping): an abstract class for the multipatchrelations
InverseMapping,InterfaceMappingandMultiPatchMapping. Youcan apply them to a domain, but evaluating them at a point raises
TypeError. They also gain a working.nameand.copy().BasicCallableMapping(the plain-Python interface for point evaluation) isunchanged.
Removed or deprecated
CallableMappingis removed, because anAnalyticMappingis its ownnumerical mapping.
sympde.topology.callable_mappingnow only re-exportsBasicCallableMapping.sympde.core.basic.BasicMappingis removed and merged intoSymbolicMapping.Mappingis kept for now, but constructing it directly emits aDeprecationWarning. Legacy subclasses of the formclass X(Mapping)with_expressionskeep working.set_callable_mappingis deprecated onSymbolicMappingand raisesTypeErroronAnalyticMapping,DiscreteMappingandStructuralMapping.Mapping.jacobianis deprecated in favour ofjacobian_symbol.Behaviour changes to watch for downstream
isinstance(x, Mapping)is nowFalsefor analytic and structural mappings.Use
isinstance(x, SymbolicMapping)to mean "any symbolic mapping".AnalyticMapping,F.jacobianis now the numerical methodF.jacobian(*eta). The symbolic Jacobian isF.jacobian_symbol.AnalyticMappingsubclass that has no_expressionsraisesTypeError. UseSymbolicMapping(name, dim=...)for an undefined mappinginstead.
Domain.from_filebuilds its patches fromSymbolicMappingobjects.Interfaceaccepts anySymbolicMapping.Bug fix
sympde/topology/derivatives.py: the chain rule for logical derivatives usedonly one of the mappings in an expression, picked arbitrarily from a set. It
now adds up the contributions of all of them.
Tests
sympde/topology/tests/test_mapping_hierarchy.py(about 1,080 lines)covers the hierarchy, point evaluation, identity and hashing, the guards and
the deprecations. Existing tests are migrated to the new names.
pytest -n auto --dist loadgroup --pyargs sympdegives251 passed, 2 skipped. The examples below run without warnings under
-W error::DeprecationWarning.Documentation
Before: the mapping classes on
master(sympde) anddevel(psydac)classDiagram class IndexedBase { <<sympy>> } class BasicMapping { <<sympde.core, empty>> } class Mapping { name ldim pdim _expressions __call__(domain) MappedDomain jacobian jacobian_expr metric_expr get_callable_mapping() set_callable_mapping(F) } class InverseMapping class InterfaceMapping class MultiPatchMapping class PolarMapping { <<analytic gallery>> } class BasicCallableMapping { <<abc.ABC>> __call__(*eta) jacobian(*eta) jacobian_inv(*eta) metric(*eta) metric_det(*eta) } class CallableMapping { lambdified _expressions } class SplineMapping { <<psydac>> } class NurbsMapping { <<psydac>> } IndexedBase <|-- BasicMapping BasicMapping <|-- Mapping Mapping <|-- InverseMapping Mapping <|-- InterfaceMapping Mapping <|-- MultiPatchMapping Mapping <|-- PolarMapping BasicCallableMapping <|-- CallableMapping BasicCallableMapping <|-- SplineMapping SplineMapping <|-- NurbsMapping Mapping ..> CallableMapping : builds from _expressions Mapping o-- BasicCallableMapping : set_callable_mappingThe problems with this design:
Mappingstands for a formula, a placeholder fora mapping defined elsewhere, and a relation between patches. Nothing in the
type says which one you have.
InverseMapping,InterfaceMappingandMultiPatchMappinginherit a point-evaluation API they cannot honour.Mappingwithout_expressionsbecomes point-evaluable only after something callsset_callable_mappingon it. psydac'sGeometry.read()did this to themappings returned by
Domain.from_file, so whether a mapping could beevaluated depended on which code had run before.
SplineMappingis purelynumerical. To use one in a symbolic domain, it had to be attached to some
other
Mapping.After: the new hierarchy
classDiagram class IndexedBase { <<sympy>> } class SymbolicMapping { name ldim pdim coordinates logical_coordinates __call__(domain) MappedDomain jacobian_symbol jacobian_expr metric_expr is_analytical get_callable_mapping() copy() } class BasicCallableMapping { <<abc.ABC>> __call__(*eta) jacobian(*eta) jacobian_inv(*eta) metric(*eta) metric_det(*eta) ldim pdim } class DefinedMapping { <<abstract>> symbolic + point-evaluable } class AnalyticMapping { _expressions, lambdified on demand } class DiscreteMapping { wraps a BasicCallableMapping has_callable_mapping() } class StructuralMapping { <<abstract>> symbolic only, no point evaluation } class InverseMapping class InterfaceMapping class MultiPatchMapping class PolarMapping { <<analytic gallery>> } class Mapping { <<deprecated>> } class SplineCallableMapping { <<psydac>> from_mapping(F, space=None, ...) to_defined_mapping(name) } class NurbsCallableMapping { <<psydac>> } IndexedBase <|-- SymbolicMapping SymbolicMapping <|-- DefinedMapping BasicCallableMapping <|-- DefinedMapping SymbolicMapping <|-- StructuralMapping SymbolicMapping <|-- Mapping DefinedMapping <|-- AnalyticMapping DefinedMapping <|-- DiscreteMapping AnalyticMapping <|-- PolarMapping StructuralMapping <|-- InverseMapping StructuralMapping <|-- InterfaceMapping StructuralMapping <|-- MultiPatchMapping BasicCallableMapping <|-- SplineCallableMapping SplineCallableMapping <|-- NurbsCallableMapping DiscreteMapping o-- BasicCallableMapping : wrapsThe psydac classes are shown for context; they come in the companion PR
(
SplineMappingis renamedSplineCallableMappingthere, and the old namestill works).
SymbolicMappingAnalyticMapping(+ gallery)_expressions)DiscreteMappingBasicCallableMappingInverseMapping,InterfaceMapping,MultiPatchMappingDomain.joinTypeError)BasicCallableMappingExample
This runs with sympde alone. Outputs are shown as comments.
With psydac (companion PR),
G_his a spline and step 3 readsSplineCallableMapping.from_mapping(P, ncells=..., degree=...).to_defined_mapping('G').discretize(domain)then builds the discrete geometry straight from thesplines, so no geometry file and no
ncellsare needed.Migration
Mapping('F', dim=2)SymbolicMapping('F', dim=2)class M(Mapping): _expressions = {...}class M(AnalyticMapping): _expressions = {...}isinstance(x, Mapping)isinstance(x, SymbolicMapping)F.jacobian(symbolic)F.jacobian_symbolF.get_callable_mapping()on an analytic mappingF;F(*eta),F.jacobian(*eta)can be called directlyCallableMapping(F)Fitself (anAnalyticMapping)M.set_callable_mapping(F_h)DiscreteMapping(F_h, 'M')from sympde.core.basic import BasicMappingfrom sympde.topology import SymbolicMappingMapping,SymbolicMapping.set_callable_mappingand the deprecatedMapping.jacobianare kept, with warnings, so downstream code has time tomigrate. They will be removed in a follow-up PR.