Skip to main content

sympy_wolfram — translating Wolfram FullForm into SymPy

⚠️ Experimental — this package is under active development; APIs, rule content and behaviour may change without notice. Version 0.0.2 is a pre-alpha snapshot.

This package turns Mathematica expressions into SymPy ones. Its most delicate job is translating patterns, because Mathematica distinguishes three things that all print as the same letter, and collapsing any two of them silently changes which expressions a rule fires on.

Every example below is a doctest, executed by sympy_wolfram/tests/test_readme.py.

For what the resulting objects then mean when matching, see sympy_matching/README.md.


1. The entry point

ffl_to_sympy_code takes a Full-Form List (nested Python lists mirroring Mathematica's FullForm) and returns Python source, the wildcard definitions it needs, and the plain symbols it saw.

>>> from sympy_wolfram.interpreter import ffl_to_sympy_code
>>> code, wild_defs, symbols = ffl_to_sympy_code(['Power', 'x', '2'], {'x': 'x'})
>>> code
'(x)**(Integer(2))'

The returned code is meant to be eval'd in a namespace the converter fills in for you:

>>> ns = {}
>>> code, _, _ = ffl_to_sympy_code(['Plus', 'x', '1'], {'x': 'x'}, ns)
>>> eval(code, ns)
x + 1

2. The three pattern forms

Mathematica FullForm translation
d d Symbol('d') — a literal
d_ Pattern[d, Blank[]] d_WildSymbol('d')
d_. Optional[Pattern[d, Blank[]]] _d_WildSymbol('d', optional_value=IDENTITY_ELEMENT)

Note the naming convention: the plain Blank becomes d_ (trailing underscore) and the Optional Blank becomes _d_ (leading and trailing). Both carry the OmniMatch variable name d, which is what unifies them — see sympy_matching/README.md §2.

>>> blank = ['Pattern', 'd', ['Blank']]
>>> optional = ['Optional', ['Pattern', 'd', ['Blank']]]
>>> code, defs, _ = ffl_to_sympy_code(['Plus', blank, ['Times', optional, 'W']])
>>> code
"(d_ + (_d_ * Symbol('W')))"
>>> sorted(defs)
["_d_ = WildSymbol('d', optional_value=IDENTITY_ELEMENT)", "d_ = WildSymbol('d')"]

This is Rubi's d_ + d_.*ProductLog[...] shape: a denominator d*(1 + W) where both occurrences are the same variable.


3. Inside a pattern, a bare atom is a LITERAL

Mathematica keeps a literal symbol independent of a same-named pattern variable — MatchQ[d + 5 W, d + d_.*W] is True, because the literal d matches itself while the variable binds 5. So a bare atom appearing in a pattern must not be rewritten into the wildcard:

>>> code, _, _ = ffl_to_sympy_code(['Plus', 'd', ['Times', optional, 'W']])
>>> code
"(Symbol('d') + (_d_ * Symbol('W')))"

Plus is orderless, so the very same Mathematica expression can arrive with its arguments the other way round. The translation must be identical:

>>> code, _, _ = ffl_to_sympy_code(['Plus', ['Times', optional, 'W'], 'd'])
>>> code
"((_d_ * Symbol('W')) + Symbol('d'))"

Both keep Symbol('d') a literal. (This used to be order-dependent: the sets of discovered wildcards fill up as the converter walks the tree, so a literal appearing after the wildcard was rewritten into it, producing a pattern that demanded both be equal and no longer matched d + 5 W.)


4. Pattern side vs replacement side

A rule's replacement and constraints contain no Pattern[...] nodes at all — their wildcards appear as bare atoms, and there they must resolve to the bound values. The caller signals this by pre-seeding the names:

>>> code, _, _ = ffl_to_sympy_code(['Times', 'd', 'W'], wildcards={'d'})
>>> code
"(d_ * Symbol('W'))"
>>> code, _, _ = ffl_to_sympy_code(['Times', 'd', 'W'], optional_wildcards={'d'})
>>> code
"(_d_ * Symbol('W'))"

So the rule is:

  • no wildcards= / optional_wildcards= given → this is a pattern; bare atoms stay literal Symbols;
  • names given → this is a replacement/constraint; bare atoms resolve to wildcards.

5. reserved_symbols — names bound by the caller

A name the caller already binds (classically the integration variable, from x_Symbol) must never become a wildcard. Pass it in reserved_symbols:

>>> code, _, _ = ffl_to_sympy_code(['Times', 'a', 'x'], {'x': 'x'}, wildcards={'a'})
>>> code
'(a_ * x)'

x is emitted as the bare identifier x, not Symbol('x') and not a wildcard.

There is a subtlety worth knowing: x_. where x is also reserved. Both bind the same name, so the "absent" branch would have to give x the Times identity 1, which then fails x_Symbol. The optional branch is therefore unreachable and the factor is in fact mandatory — verified in Mathematica, where g[x_.*h[x_], x_Symbol] matches g[z h[z], z] but not g[h[z], z].


6. Constants, numbers and heads

Known constants and functions are emitted fully qualified, so the generated code is unambiguous no matter what the surrounding module has imported:

>>> ffl_to_sympy_code('Pi')[0]
'sympy.pi'
>>> ffl_to_sympy_code(['Sin', 'x'], {'x': 'x'})[0]
'sympy.sin(x)'
>>> ffl_to_sympy_code(['Power', 'x', '2'], {'x': 'x'})[0]
'(x)**(Integer(2))'

Integers become Integer(...) rather than Python ints, so exact arithmetic is preserved (Integer(1)/Integer(4) is a Rational, whereas 1/4 would be a float):

>>> ns = {}
>>> code, _, _ = ffl_to_sympy_code(['Times', '3', 'x'], {'x': 'x'}, ns)
>>> code
'(Integer(3) * x)'
>>> eval(code, ns)
3*x

7. Round trip: translate, then match

Putting §2–§4 together — translate a Wolfram pattern and use it. The values below are Mathematica's own answers for MatchQ[..., d + d_.*W]:

>>> from sympy import Symbol
>>> from omnimatch import ManyToOneMatcher, Pattern
>>> from sympy_matching.matching_rule import to_omnimatch_expression
>>> x, W, d = Symbol('x'), Symbol('W'), Symbol('d')
>>> ns = {}
>>> code, _, _ = ffl_to_sympy_code(['Plus', 'd', ['Times', optional, 'W']], namespace=ns)
>>> pattern = eval(code, ns)
>>> def matches(pat, subject):
...     m = ManyToOneMatcher()
...     m.add(Pattern(to_omnimatch_expression(pat)))
...     return bool(list(m.match(to_omnimatch_expression(subject))))
>>> matches(pattern, d + d*W)      # Mathematica: True
True
>>> matches(pattern, 5 + 5*W)      # Mathematica: False -- literal d is not 5
False
>>> matches(pattern, d + 5*W)      # Mathematica: True  -- variable binds 5
True

Why this matters

A rule that fires where Mathematica's would not produces a wrong antiderivative, not an error — the replacement derived for one shape gets applied to another. The three forms therefore have to stay distinct all the way from FullForm to the OmniMatch pattern, which is why they are pinned by tests at both layers:

  • sympy_wolfram/tests/test_blank_optional_semantics.py — 37 cases, every expected value read directly off Mathematica 12.2;
  • sympy_wolfram/tests/test_docs.py and sympy_matching/tests/test_docs.py — these documents.

See also

  • docs/translating-mathematica.md — the full pipeline: notation → Full-Form List → SymPy code → live rule, with a worked example.
  • docs/nodes-and-evaluation.md — deferred MathematicaExpr nodes, doit() vs rewrite_as_standard_sympy(), eager_<Name> functions, and the constraint classes.
  • sympy_matching/docs/ — the matching layer this package builds on.

Testing

pytest tests/ — a handful of tests exercise the re-exports that the higher-level rubi-integrate package builds on top of this one; they need rubi-integrate importable (it is not a runtime dependency of this package).

License

MIT License, Copyright (c) 2026 Francesco Bonazzi. See LICENSE.

Release files for sympy-wolfram 0.0.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sympy-wolfram 0.0.3
File Size Uploaded
sympy_wolfram-0.0.3.tar.gz 103.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sympy-wolfram 0.0.3
File Interpreter ABI Platform
sympy_wolfram-0.0.3-py3-none-any.whl Python 3 none any Details

Total release size: 166.0 kB

Release files / sympy_wolfram-0.0.3.tar.gz

Download URL sympy_wolfram-0.0.3.tar.gz
Size 103.3 kB
Tags Source
SHA-256 checksum
How to use checksums
a07f53e27041bd58a2dd68a106e89a3654c72bbd39d120530f52efa6096369a9
BLAKE2b-256 checksum
How to use checksums
227d9610c023cbfdabeb0715dbb273a00bd0f6e4436cb983efbd7a759f301902
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / sympy_wolfram-0.0.3-py3-none-any.whl

Download URL sympy_wolfram-0.0.3-py3-none-any.whl
Size 62.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4702c62ff3a281a6c867567e97774c0a5366c9742c64dbe14efd7f3c7c08352f
BLAKE2b-256 checksum
How to use checksums
5e0aafae62c12b2f12afe310cc64c118793e3a1a33b73db28fe8e8bdb8e7516f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.0.3 This release

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page