Skip to content

Upgrade SymPy - #193

Open
FrederikSchnack wants to merge 19 commits into
update_docsfrom
upgrade_sympy
Open

FrederikSchnack wants to merge 19 commits into
update_docsfrom
upgrade_sympy

Conversation

@FrederikSchnack

Copy link
Copy Markdown
Contributor

Summary

Update SymPDE to support the maintained SymPy release range:

SymPy >= 1.12, < 1.15

This PR addresses compatibility problems caused by stricter symbolic construction, traversal, reconstruction, hashing, and sympification behavior
in recent SymPy versions.

Closes #106.

Compatibility issues and fixes

1. Incorrect construction of custom Symbol subclasses

ScalarFunction, VectorFunction, and DifferentialForm were constructed through Expr.__new__ or Basic.__new__ instead of SymPy’s Symbol constructor.

Recent SymPy versions expect symbol subclasses to initialize Symbol-specific state, including assumptions, commutativity, and free-symbol behavior. Without this initialization:

  • free_symbols could be incorrect;
  • assumptions were not initialized consistently;
  • scalar and vector functions could have incorrect commutativity;
  • equality and hashing did not follow SymPy’s symbol semantics.

The affected objects are now constructed through Symbol.__xnew__.

SymPDE-specific metadata, such as the function space or differential-form degree and dimension, is included through _hashable_content rather than being passed as invalid raw symbol arguments.

Regression tests verify:

  • correct scalar and vector commutativity;
  • correct free_symbols;
  • stable equality and hashing;
  • distinct differential forms for different degree or dimension values.

2. Non-SymPy values stored in Basic.args

Several SymPDE expression classes stored ordinary Python objects directly in their structural arguments, including:

  • strings;
  • integers;
  • None;
  • lists and tuples;
  • dictionaries.

Modern SymPy traverses, hashes, prints, sympifies, and reconstructs every object exposed through Basic.args. Raw Python metadata could therefore cause failures during operations such as:

expr.func(*expr.args)
expr.xreplace(...)
preorder_traversal(expr)

A common structural-argument normalization layer was introduced:

  • strings are stored as SymPy Str objects;
  • integers are sympified as Integer;
  • tuples and lists are stored as SymPy Tuple objects;
  • dictionaries are stored as SymPy Dict objects;
  • optional None values use a private SymPy-compatible sentinel.

Matching restoration helpers convert these values back to their historical Python representation when accessed through public properties. This preserves the existing SymPDE API while keeping expression trees valid for SymPy.

The normalization is applied to objects including domains, boundaries, interfaces, equations, matrix elements, traces, one-dimensional forms, differential forms, and multipatch mappings.

3. Expressions could not be reconstructed from func and args

SymPy assumes that symbolic expressions can generally be rebuilt using:

expr.func(*expr.args)

This mechanism is used internally by substitution, xreplace, simplification, and other tree transformations.

A number of SymPDE constructors did not satisfy this contract because:

  • structural metadata was omitted from args;
  • constructors expected keyword-only arguments;
  • reconstructed arguments were SymPy containers instead of Python containers;
  • subclass constructors interpreted structural arguments as user-facing arguments;
  • optional arguments were not converted back from their symbolic representation.

Constructors were updated to accept and restore normalized structural arguments. Reconstruction now retains information such as:

  • domain interiors and boundaries;
  • mappings and logical domains;
  • interface orientations;
  • domain connectivity;
  • line, square, and cube bounds;
  • equation boundary conditions and constraints;
  • tensor-form names, axes, and weights;
  • multipatch mapping dictionaries.

Regression tests now exercise both reconstruction and substitution:

rebuilt = expr.func(*expr.args)
replaced = expr.xreplace({...})

4. Multipatch mappings used raw dictionaries as symbolic arguments

MultiPatchMapping previously placed a Python dictionary directly in Basic.args and implemented a custom __hash__.

Recent SymPy operations expect structural arguments themselves to be symbolic and reconstructible. This caused problems during hashing, replacement, and rebuilding.

The mapping dictionary is now stored structurally as a SymPy Dict, while the public mappings property continues to return a regular Python dictionary. Hashing is derived from the normalized structural arguments instead of a separate custom implementation.

Tests verify that multipatch mappings survive both reconstruction and xreplace.

5. Function symbols with the same name collapsed across spaces

Function symbols were primarily identified by their printed name. As a result, functions with the same name but belonging to different function spaces could compare as equal and collapse during SymPy simplification.

For example, an expression conceptually equivalent to:

u_from_V + u_from_W

could be simplified as though both functions were the same symbol.

Function spaces now define structural equality from their name, domain, shape, and kind. Scalar and vector function symbols include their function space in _hashable_content.

This ensures that:

  • equivalent spaces compare and hash equally;
  • different spaces remain distinct;
  • functions with the same name in different spaces remain distinct symbols;
  • additions do not incorrectly combine functions from different spaces;
  • mapped and logical test functions retain the correct domain identity.

6. Constructor postprocessing returned a Python list

The integral multiplication postprocessor wrapped a transformed IntAdd expression in a Python list before passing it back to Mul.

Recent SymPy versions strictly sympify constructor-postprocessor results and reject this list during form-linearity checks.

The postprocessor now returns the transformed IntAdd expression directly:

IntAdd(...)

instead of:

[IntAdd(...)]

7. Legacy SymPy compatibility helpers were outdated

SymPDE carried local copies of compatibility utilities for:

  • is_sequence;
  • Python 2-style metaclass construction.

These helpers were replaced with maintained functionality:

  • use sympy.utilities.iterables.is_sequence;
  • use native Python 3 metaclass declarations.

VectorFunction handling remains explicit where its indexed behavior differs from ordinary Python sequences.

The compatibility module itself remains temporarily available because it is still exported by the legacy package namespace. It is removed by the subsequent public-API PR.

8. Domain subclasses were not printed correctly in LaTeX

The custom LaTeX printer only implemented _print_Domain. Some reconstructed domain-related objects are subclasses of BasicDomain rather than Domain, causing modern SymPy’s printer dispatch to miss the custom representation.

A _print_BasicDomain implementation was added so domain interiors and interfaces consistently render using their symbolic names.

Regression tests cover multipatch interiors and interfaces.

Dependency and CI changes

  • Raise the minimum supported SymPy version from 1.5 to 1.12.
  • Support SymPy releases below 1.15.
  • Keep the normal CI matrix on the newest version allowed by the dependency range.
  • Add a dedicated Python 3.11 job pinned to SymPy 1.12.
  • Run the complete test suite in both minimum- and maximum-supported dependency configurations.

The upper bound remains intentionally below SymPy 1.15 until that release range receives a separate compatibility review.

Validation

The test suite passes with the upgraded symbolic implementation:

170 passed, 2 skipped

New regression coverage includes:

  • symbol assumptions and commutativity;
  • free_symbols behavior;
  • structural equality and hashing;
  • functions with identical names in different spaces;
  • SymPy-compatible expression arguments;
  • func(*args) reconstruction;
  • xreplace substitutions;
  • multipatch mapping reconstruction;
  • domain and interface LaTeX output;
  • integral multiplication postprocessing.

Delete the unmaintained old/ snapshots and bin/run_tests.sh. The script references test modules that no longer exist, so it advertises a broken workflow while the deleted snapshots duplicate history already preserved by Git.
Move pytest configuration and development dependencies into pyproject.toml, test installed packages across supported Python versions, and collect coverage as a CI artifact.

Keep the tested SymPy upper bound in place while #106 tracks the compatibility work needed for current releases.
Delete the unreferenced 2019 roadmap, which describes removed APIs and work that has since been implemented. Active work is tracked through GitHub issues.

Refs #181.
Separate test automation from documentation builds and run it for every pull request update.
Add reproducible GitHub Actions and Read the Docs builds, replace shell-based API generation, configure bibliography and MathJax support, and correct malformed API docstrings.

Addresses the failing documentation setup reported in #166.

Fixes the Domain.join documentation problem reported in #140.
Use the maintained Sphinx build layout while keeping documentation CI separate from the test matrix.
Publish strict Sphinx builds from master through GitHub Pages, matching the Psydac documentation workflow. Update project links and remove the obsolete Read the Docs configuration.

Addresses #166.
Build the documentation when pull requests are opened, reopened, or synchronized, in addition to ready-for-review transitions.
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.
Restore the detailed multipatch orientation reference and connectivity examples that were removed while repairing the documentation build.
Keep the full multipatch connectivity notes while marking their example as a Python code block so strict documentation builds remain warning-free.
@FrederikSchnack
FrederikSchnack added this pull request to stack #192 September 29, 2026 15:32
@FrederikSchnack FrederikSchnack mentioned this pull request Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use latest version of SymPy

1 participant