rubi_integrate
⚠️ 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.
Symbolic integration for SymPy using the Rubi rule set (Rule-Based Integration), matched with OmniMatch.
The public entry point is rubi_integrate.
from sympy import Symbol, sin, exp
from rubi_integrate import rubi_integrate
x = Symbol('x')
rubi_integrate(1/x, x) # log(x)
rubi_integrate(x**2, x) # x**3/3
rubi_integrate(sin(x)*x, x) # -x*cos(x) + sin(x)
rubi_integrate(exp(x**2), x) # sqrt(pi)*erfi(x)/2
Results are ordinary SymPy expressions. Note that exponentials are returned in
E**(...) (i.e. Pow(E, ...)) form rather than exp(...) — the two are equal
(E**u == exp(u)); the port carries exponentials as Pow(E, ·) internally so
they match Rubi's F^(...) rule patterns.
rubi_integrate(expr, x, ...)
| argument | default | meaning |
|---|---|---|
expr |
— | the integrand (a SymPy expression) |
x |
— | the integration variable |
pattern |
'**' |
glob selecting which generated rule files to load (see below) |
return_matched_rules |
False |
also return the list of rules that were accepted |
return_trace |
False |
also return a full DFS trace of rules accepted and rejected |
If the integrand cannot be integrated with the available rules it is returned
unevaluated as Int(expr, x), or wrapped in CannotIntegrate(expr, x) when a
Rubi rule explicitly gives up.
Which rules ran: return_matched_rules=True
Returns (result, matched_rules). matched_rules is a list of (result, applied)
pairs (one per top-level integrand handled), where applied is a flat list of the
rules accepted on the winning path, each as (module_name, rule_number):
result, matched = rubi_integrate(exp(x)*sin(x**2 + x), x, return_matched_rules=True)
# matched == [(result, [
# ('4.7.7 F^(c (a+b x)) trig(d+e x)^n', 44),
# ('2.3 Miscellaneous exponentials', 43),
# ('2.3 Miscellaneous exponentials', 42),
# ('2.3 Miscellaneous exponentials', 11),
# ... ])]
This shows only the rules that stuck. It does not show rules that matched
but were rejected — for that, use return_trace.
Full search trace: return_trace=True
rubi_integrate reduces an integral with a depth-first search: at each
sub-integral it tries the matching rules and picks one, backing out of rules that
lead to a cycle or that fail their side conditions. return_trace=True returns
(result, trace), where trace is a list of one record per rule tried:
{'depth': int, # reduction depth (how nested the sub-integral is)
'integrand': Int(...), # the sub-integral the rule was tried on
'rule': (module, num), # the rule, or its label string
'status': str} # what happened — see below
Statuses:
| status | meaning |
|---|---|
accepted |
produced a clean antiderivative (no Int, no CannotIntegrate); taken |
accepted (fallback) |
no clean rule was available, so this non-clean result was taken |
rejected (cycle) |
its result re-entered a sub-integral already on the current path (a loop) |
rejected (condition failed) |
the rule's side condition (/;) failed at apply time |
candidate (non-clean) |
matched but produced an Int/CannotIntegrate; kept only as a fallback |
format_trace renders it as indented text (deeper reductions are indented more):
from rubi_integrate import rubi_integrate
from rubi_integrate.base_objects import format_trace
result, trace = rubi_integrate(exp(x)*sin(x**2 + x), x, return_trace=True)
print(format_trace(trace))
rejected (cycle) 2.3 Miscellaneous exponentials:[42] Int(E**(-I*x**2 + x*(1 - I) + 1/2))
candidate (non-clean) 9.3 Miscellaneous integration rules:[67] Int(E**(-I*x**2 + x*(1 - I) + 1/2))
candidate (non-clean) 2.3 Miscellaneous exponentials:[43] Int(E**(I*(-2*I*x + 1 - I)**2/4))
accepted 2.3 Miscellaneous exponentials:[11] Int(E**(I*(-2*I*x + 1 - I)**2/4))
accepted 2.3 Miscellaneous exponentials:[42] Int(E**(-I*x**2 + x*(1 - I)))
accepted 2.3 Miscellaneous exponentials:[43] Int(E**(x*(-I*x + 1 - I)))
accepted 4.7.7 F^(c (a+b x)) trig(d+e x)^n:[44] Int(E**x*sin(x**2 + x))
Records are appended in reduction order, so a sub-integral's rules appear
(indented) before the rule that produced it. rejected/candidate entries are
the branches the DFS explored and backed out of — e.g. above, rule [43] was
rejected in favour of [11] at the completed square, which is how the result
reaches Erf/Erfi instead of CannotIntegrate.
Scoping the rules: the pattern argument
pattern is a glob over rubi_integrate/rules/**. The default '**' loads the whole
rule set (~7700 rules; the first call builds the matcher and takes ~a minute, then
it is cached). Scope to a subsection for speed when you know which rules you need:
rubi_integrate(exp(x**2), x, pattern='r_2_exponentials/**')
rubi_integrate(1/x, x, pattern='r_1_algebraic_functions/**')
How it works (short version)
Rubi's .m rules are translated by codegen/generate.py into Python
SymPyReplacementPattern objects under rules/ (auto-generated — do not edit). Each rule
is a SymPy pattern + constraints + replacement. rubi_integrate converts the
integral to a OmniMatch expression, matches rules, and applies replacements,
reducing Int(...) nodes depth-first until the integral is solved. Rule match
order is not significant — the search prefers a fully-integrated result over a
CannotIntegrate/residual-Int one regardless of which rule matches first.
See the repository-root AGENTS.md for the full architecture (eager vs deferred
utility functions, active vs inert trig, the exp/Pow(E,·) representation, and the
code-generation pipeline).
License
MIT (Copyright (c) 2026 Francesco Bonazzi) for the Python port. The Rubi-derived
content (rules / test corpus) originates from Rubi
by Albert Rich, whose MIT license is reproduced in full in LICENSE.
Metadata
Release files for rubi-integrate 0.0.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 | Provenance | Actions |
|---|---|---|---|---|
| rubi_integrate-0.0.3.tar.gz | 673.3 kB | Without provenance | Details for rubi_integrate-0.0.3.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| rubi_integrate-0.0.3-py3-none-any.whl | Python 3 | none | any | Details for rubi_integrate-0.0.3-py3-none-any.whl |
Total release size: 1.5 MB
Release files / rubi_integrate-0.0.3.tar.gz
| Download URL | rubi_integrate-0.0.3.tar.gz |
|---|---|
| Size | 673.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0263c7e5352415d0db11a16abdfa81b1532b4b58b23344bd40b8dd3732e3aea2
|
|
BLAKE2b-256 checksum How to use checksums |
e05da63a616bc67d3545cbb73bc19a7093c8754e24043d6009b5b983f06868da
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / rubi_integrate-0.0.3-py3-none-any.whl
| Download URL | rubi_integrate-0.0.3-py3-none-any.whl |
|---|---|
| Size | 863.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c5ccd9b5a9a127a5c5c3b306ea2c2e3557f18162635397d94927a6f052f0808e
|
|
BLAKE2b-256 checksum How to use checksums |
6ca72cf2f9562ac5606c22f807302e4a9fc26f553d31ecfd7d589d1860cad7d5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Security for rubi-integrate 0.0.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.