emltree
Compile elementary-function formulas into pure EML (Exp-Minus-Log) form, where the only operator is
eml(x, y) = exp(x) − ln(y)
and the only constant is 1.
Paper
This project is an implementation of ideas introduced in:
Andrzej Odrzywolek. All elementary functions from a single operator. arXiv:2603.21852 (2026). https://arxiv.org/abs/2603.21852
The paper proves that the single binary operator eml(x, y) = exp(x) − ln(y)
together with the constant 1 generates the entire scientific-calculator
basis (arithmetic, roots, logs, all trig and hyperbolic functions, the
constants e, π, i). Every elementary expression becomes a binary
tree whose only internal node is eml. This package is the inverse
direction: it takes an ordinary formula and compiles it into that
tree. A sibling Rust crate, OxiEML,
covers the Rust ecosystem; a zero-dependency JS/TypeScript port lives in
js/ (npm: emltree); this package is the Python side.
Quick start
uv venv --python 3.11
uv pip install -e ".[dev]"
# Compile a formula
uv run emltree "sin(x) + exp(y)" -v x -v y
# ASCII tree
uv run emltree "log(x)" -v x -f tree
# Numerical sanity check
uv run emltree "sin(x)**2 + cos(x)**2" -v x=0.9 --verify
Library use
from emltree import compile_formula, evaluate
tree = compile_formula("sqrt(x**2 + y**2)", variables=["x", "y"])
print(tree.to_nested())
print(evaluate(tree, {"x": 3.0, "y": 4.0})) # ≈ 5+0j
The returned tree is an immutable ADT (One / Var / Eml) — walk it,
hash it, render as RPN (exp(x) is x 1 E), or ship it into an
FPGA/analog circuit as the paper suggests.
Evaluation is vectorised: pass numpy arrays as bindings and the tree evaluates elementwise.
import numpy as np
xs = np.linspace(-2, 2, 1000)
evaluate(tree, {"x": xs, "y": xs}) # array of 1000 values
Why the trees are large
This compiler is compositional, not optimal. Every primitive bottoms
out in exp / ln / sub, so sin(x) produces a tree with hundreds of
nodes. The paper's direct-search results (Table 4) are vastly shorter —
integrating that search is one of the open directions below.
Numerical caveats
- Branch cuts: outside their real domains (
asin(2),acosh(-2),logof negatives, …) results flow through complex branch cuts and may land on a non-principal branch — or, where float fuzz compounds, off-sheet entirely (paper §4.1). On the usual real domains everything matches sympy to ~1e-7. - Addition overflow:
add_'s expansion appliesexp()to its second operand, so adding values past ~709 overflows float64. Integer/decimal constants avoid this internally (binary decomposition with a multiplicative odd step), butx + ywith hugeyis an inherent ceiling of the encoding.
Tests
uv run pytest # Python
cd js && node --test # JS port
vectors.json holds cross-language cases — formula,
bindings, and an expected value taken from Python's math module, so it is
independent of both implementations. tests/test_vectors.py and
js/vectors.test.js each run every case. A change that makes one port
disagree with the other fails there, rather than waiting to be spotted
during the next hand-port (which is how the 0.1.1 atan sign bug surfaced).
New primitives should land with a vector.
Contributing
Contributions are very welcome — this is an early-stage package and there's plenty of room to improve. A few directions that would make a great first PR:
- Shorter trees — hand-curated or searched EML identities to replace the naive compositional expansions (see paper Table 4).
- More primitives —
abs,sign,floor,ceil,erf, etc. (many require tricks; see the paper's supplementary). - Better output — LaTeX, GraphViz / Mermaid, Jupyter rich repr.
- Torch evaluation — numpy bindings already vectorise; a torch evaluator would make EML trees differentiable end-to-end.
- Optional Rust backend — PyO3 bindings to OxiEML for expensive search and symbolic regression.
- Docs & examples — walk-throughs of the paper's identities, notebooks showing symbolic-regression use cases.
Please open an issue to discuss before sending a large PR. Bug reports, documentation fixes, and additional test cases are also very welcome — no contribution is too small.
Development
uv venv --python 3.11
uv pip install -e ".[dev]"
uv run pytest
License
MIT — see LICENSE.
Citation
If you use emltree in academic work, please cite both the paper and
the software:
@article{odrzywolek2026eml,
title = {All elementary functions from a single operator},
author = {Odrzywolek, Andrzej},
journal = {arXiv preprint arXiv:2603.21852},
year = {2026}
}
@software{hsiao2026emltree,
author = {Hsiao, Wei-Chien},
title = {emltree: a Python compiler from elementary formulas to EML trees},
year = {2026},
url = {https://github.com/gba3124/emltree},
version = {0.1.2}
}
Metadata
Release files for emltree 0.1.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| emltree-0.1.3.tar.gz | 41.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| emltree-0.1.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 54.8 kB
Release files / emltree-0.1.3.tar.gz
| Download URL | emltree-0.1.3.tar.gz |
|---|---|
| Size | 41.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5fd4c8588467f6f31c4261228acb8af81fc7125d0497ba38ce990fb4c0374812
|
|
BLAKE2b-256 checksum How to use checksums |
2520ffabb83ace6035c523ca1b0232116e400f828bb274667305de91a7903f16
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.17
|
Release files / emltree-0.1.3-py3-none-any.whl
| Download URL | emltree-0.1.3-py3-none-any.whl |
|---|---|
| Size | 13.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f314f78191da6f0ef6d4afab182fcdb91b2548a98a9af7822827fa6c6388ec92
|
|
BLAKE2b-256 checksum How to use checksums |
ba18b057552dd21b499c0dfb62bb2246e60d16d0ebb381bc4a58932bd3cb3c18
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.17
|