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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sympy_wolfram-0.0.3.tar.gz (103.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sympy_wolfram-0.0.3-py3-none-any.whl (62.7 kB view details)

Uploaded Python 3

File details

Details for the file sympy_wolfram-0.0.3.tar.gz.

File metadata

  • Download URL: sympy_wolfram-0.0.3.tar.gz
  • Upload date:
  • Size: 103.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for sympy_wolfram-0.0.3.tar.gz
Algorithm Hash digest
SHA256 a07f53e27041bd58a2dd68a106e89a3654c72bbd39d120530f52efa6096369a9
MD5 6fa3bb232ae4ef91b89d3ea3b3481894
BLAKE2b-256 227d9610c023cbfdabeb0715dbb273a00bd0f6e4436cb983efbd7a759f301902

See more details on using hashes here.

File details

Details for the file sympy_wolfram-0.0.3-py3-none-any.whl.

File metadata

  • Download URL: sympy_wolfram-0.0.3-py3-none-any.whl
  • Upload date:
  • Size: 62.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.3

File hashes

Hashes for sympy_wolfram-0.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 4702c62ff3a281a6c867567e97774c0a5366c9742c64dbe14efd7f3c7c08352f
MD5 9bcb3f6e5a1f54a34cd2ea94b0dd7530
BLAKE2b-256 5e0aafae62c12b2f12afe310cc64c118793e3a1a33b73db28fe8e8bdb8e7516f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page