Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ Changelog
- :bug:`590` Worked around a performance regression in SymPy 1.13 that
caused ``examples/ipython/LaTeX.ipynb`` (``check('curvi_linear_latex')``) to
time out after 600 s on SymPy ≥ 1.13. SymPy PR #26390 added an O(N·M)
``.replace()`` traversal inside ``TR3``/``futrig`` that is a no-op for
galgebra's symbolic trig arguments but dominated each of the ~70
``Simp.apply`` calls during ``Ga.build(norm=True)`` for curvilinear
coordinates. The fix uses ``trigsimp(method='old')`` via ``Simp.profile``
for the affected example, cutting run time from > 600 s to < 6 s.
``.replace()`` traversal inside ``TR3``/``futrig`` that made simplification
of the prolate-spheroidal output stall during display. The fix uses
``trigsimp(method='old')`` via ``Simp.profile`` for the affected example,
cutting its run time from > 600 s to < 6 s.
A notebook note documents the two cosmetic output differences from the
pre-1.13 form; a proper upstream fix is tracked in :issue:`576`.
pre-1.13 form.

- :bug:`598` Multivector string and LaTeX display now avoid the same SymPy
regression outside that example. Large expressions with trigonometric and
hyperbolic functions under non-integral powers use the bounded
``trigsimp(method='old')`` path. Algebraic simplification and explicit
``Simp.profile`` modes remain unchanged.

- :support:`589` Added Step 0 to the release-process runbook
(``doc/dev/release-process.md``): open a release issue before preparing the
Expand Down
2 changes: 1 addition & 1 deletion examples/LaTeX/curvi_linear_latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ def main():
from sympy import trigsimp
from galgebra.metric import Simp

orig_modes = Simp.modes[:]
orig_modes = Simp.modes
Simp.profile([lambda e: trigsimp(e, method='old')])
try:
derivatives_in_spherical_coordinates()
Expand Down
76 changes: 76 additions & 0 deletions galgebra/_utils/simplify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Compatibility helpers for simplification across SymPy releases."""

import re

import sympy
from sympy import preorder_traversal, simplify, trigsimp
from sympy.functions.elementary.hyperbolic import HyperbolicFunction
from sympy.functions.elementary.trigonometric import TrigonometricFunction


def _major_minor(version):
"""Return the leading major and minor numbers from a version string."""
match = re.match(r'^(\d+)\.(\d+)', version)
if match is None:
return (0, 0)
return tuple(map(int, match.groups()))


_SYMPY_MAJOR_MINOR = _major_minor(sympy.__version__)

# SymPy 1.13's gh-26390 added a nested replace traversal to the FU
# simplifier. Match only the observed two-term prolate radical shape. A
# numerical tree-cost heuristic admitted benign expressions whose unrelated
# terms happened to produce the same score.


def _is_squared_function(term, function_type):
return (
term.is_Pow
and term.exp == 2
and isinstance(term.base, function_type)
)


def _is_mixed_squared_base(base):
"""Whether ``base`` is one trig square plus one hyperbolic square."""
if not base.is_Add or len(base.args) != 2:
return False
return (
any(
_is_squared_function(term, TrigonometricFunction)
for term in base.args
)
and any(
_is_squared_function(term, HyperbolicFunction)
for term in base.args
)
)


def _has_expensive_fu_traversal(expr):
"""Whether ``simplify`` is likely to hit SymPy's slow FU traversal."""
if _SYMPY_MAJOR_MINOR < (1, 13):
return False

return any(
(
node.is_Pow
and abs(node.exp) == sympy.S.Half
and _is_mixed_squared_base(node.base)
)
for node in preorder_traversal(expr)
)


def simplify_for_display(expr):
"""Simplify display output while avoiding a SymPy 1.13+ regression.

This helper is only for rendering. Algebraic operations retain ordinary
``simplify``. Remove the fallback after SymPy replaces the nested
traversal introduced by gh-26390 and galgebra's minimum supported SymPy
includes that fix.
"""
if _has_expensive_fu_traversal(expr):
return trigsimp(expr, method='old')
return simplify(expr)
21 changes: 20 additions & 1 deletion galgebra/metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from . import printer
from ._utils import cached_property as _cached_property
from ._utils.simplify import simplify_for_display
from .atoms import (
BasisVectorSymbol, DotProductSymbol, MatrixFunction, Determinant,
)
Expand Down Expand Up @@ -299,7 +300,9 @@ def symbols_list(s, indices=None, sub=True, commutative=False):


class Simp:
modes = [simplify]
_default_modes = (simplify,)
modes = list(_default_modes)
_default_modes_instance = modes

@staticmethod
def profile(s):
Expand All @@ -312,6 +315,22 @@ def apply(expr):
obj += apply_function_list(Simp.modes, coef) * base
return obj

@staticmethod
def apply_display(expr):
"""Apply the display fallback unless the user selected a profile."""
modes = (
[simplify_for_display]
if (
Simp.modes is Simp._default_modes_instance
and tuple(Simp.modes) == Simp._default_modes
)
else Simp.modes
)
obj = S.Zero
for coef, base in linear_expand_terms(expr):
obj += apply_function_list(modes, coef) * base
return obj

@staticmethod
def applymv(mv):
return Mv(Simp.apply(mv.obj), ga=mv.Ga)
Expand Down
4 changes: 2 additions & 2 deletions galgebra/mv.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@ def _sympystr(self, print_obj: printer.GaPrinter) -> str:

# note: this just replaces `self` for the rest of this function
obj = expand(self.obj)
obj = metric.Simp.apply(obj)
obj = metric.Simp.apply_display(obj)
self = Mv(obj, ga=self.Ga)

if self.i_grade == 0:
Expand Down Expand Up @@ -697,7 +697,7 @@ def append_plus(c_str):
# note: this just replaces `self` for the rest of this function
obj = expand(self.obj)
try:
obj = metric.Simp.apply(obj)
obj = metric.Simp.apply_display(obj)
except ZeroDivisionError:
pass # SymPy trigsimp regression; display without simplification
self = Mv(obj, ga=self.Ga)
Expand Down
Loading
Loading