Skip to content

Repository files navigation

SimpliPy:
Efficient Simplification of Mathematical Expressions

PyPI version PyPI license Documentation Status

pytest quality checks CodeQL Advanced

Publications

  • Saegert & Köthe 2026, Breaking the Simplification Bottleneck in Amortized Neural Symbolic Regression (preprint, under review) https://arxiv.org/abs/2602.08885

Usage

pip install simplipy

The compiled Rust extension (simplipy._core) is required: the inline phase (simplify, conversions, validation) runs on it exclusively, and there is no pure-Python fallback. Prebuilt wheels are published for Linux (x86_64/aarch64), macOS (x86_64/arm64) and Windows (x64) on CPython ≥ 3.11, so pip install simplipy does not compile anything for most users. Installing from the source distribution (an unsupported platform, or --no-binary) requires a Rust toolchain (rustup, MSRV 1.83). If the extension is missing at runtime, constructing an engine raises ImportError.

import simplipy as sp

engine = sp.SimpliPyEngine.load("4-3", install=True)   # a published ruleset artifact

# Simplify prefix expressions
engine.simplify(('/', '<constant>', '*', '/', '*', 'x3', '<constant>', 'x3', 'log', 'x3'))
# > ('/', '<constant>', 'log', 'x3')

# Simplify infix expressions
engine.simplify('x3 * sin(<constant> + 1) / (x3 * x3)')
# > '<constant> / x3'

Normalization

The root-exported normalize_skeleton, normalize_expression, and normalize_variable_token helpers (also available as simplipy.normalization) canonicalize a prefix token sequence so that two expressions that are "the same" up to variable renaming / constant values compare equal. They are pure-string helpers with no engine state, so consumers such as holdout matching and symbolic-recovery scoring share identical behavior by construction.

import simplipy as sp

# Skeleton form: variables -> x{n}, numeric literals -> <constant>
sp.normalize_skeleton(['+', 'v1', '2.5'])
# > ['+', 'x1', '<constant>']

# Expression form: variables canonicalized, numeric literals kept intact
sp.normalize_expression(['+', 'V1', '2.5'])
# > ['+', 'x1', '2.5']

# Classify / canonicalize a single token -> (normalized_token, is_variable)
sp.normalize_variable_token('X3')
# > ('x3', True)
sp.normalize_variable_token('sin')
# > ('sin', False)

More examples can be found in the documentation.

Performance

As of 0.6.0 the simplify hot path defers match-time certificates to completed matches (memoized generationally, never stopping memoization), memoizes whole fixpoint passes and rule-normal subtrees, and runs on interned token ids (~20× fewer allocations per call) — all at byte-identical outputs. On a 65,536-expression training-prior benchmark, large certificate-bearing rulesets simplify ~59× faster than 0.5.0 and certificate-free rulesets ~2.3× faster; see the CHANGELOG for details. Since 0.7.0 there is a single compiled engine line; the published ruleset artifacts (2-1, 3-2, 4-3, …) are the distinguishing factor between engines. Rule application always considers every pattern in the loaded artifact (the former max_pattern_length knob was removed in 0.10.0). (To reproduce the historical dev_7-3 / v23.0-era behavior byte-for-byte, install simplipy<=0.6.0.)

Simplification time and ratio ECDFs: SimpliPy 0.9.1 vs SymPy across three axes (mined rulesets, safe vs aggressive, search budget) on 64k Lample-Charton expressions from the Flash-ANSR v23.0 prior

Empirical Cumulative Distribution Functions (ECDFs) of simplification wall-clock time (top row) and simplification ratio |simp| / |orig| in prefix tokens (bottom row, inset: zoom on the low-ratio tail), over 65,536 randomly generated Lample-Charton expressions sampled from the Flash-ANSR v23.0 training prior (0 to 17 unique variables, 1 to 35 symbols [Saegert & Köthe 2026]). Three axes vary SimpliPy (green) while SymPy [Meurer et al. 2017] (orange ratio=None / red ratio=1) is the fixed reference: Mined Rulesets — the published 2-1/3-2/4-3 artifacts, every pattern active (darker = larger); Safe vs Aggressive4-3 in the deployed SOUND mode vs the training-only LOSSY mode; Search Budget4-3 SOUND at node budgets 1 to 48. SimpliPy is timed per call (perf_counter, gc off); SymPy is given each <constant> as a free symbol and simplified symbolically inside a per-expression worker with a 1 s timeout, then scored by its native prefix length. The measured quantities are the two ECDFs; the plot, not this caption, reports what they show.

Development

Setup

To set up the development environment, run the following commands:

pip install -e .[dev]
pre-commit install

Tests

Test the package with pytest:

pytest tests --cov src --cov-report html

or to skip integration tests,

pytest tests --cov src --cov-report html -m "not integration"

Citation

@misc{saegert2026breakingsimplificationbottleneckamortized,
  title   = {Breaking the Simplification Bottleneck in Amortized Neural Symbolic Regression},
  author  = {Paul Saegert and Ullrich Köthe},
  year    = {2026},
  eprint  = {2602.08885},
  archivePrefix =  {arXiv},
  primaryClass  = {cs.LG},
  url     = {https://arxiv.org/abs/2602.08885},
}

% Optionally
@software{simplipy-2025,
    author = {Paul Saegert},
    title = {Efficient Simplification of Mathematical Expressions},
    year = 2026,
    publisher = {GitHub},
    version = {0.9.1},
    url = {https://github.com/psaegert/simplipy}
}

Releases

Packages

Contributors

Languages