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.
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 | |
|---|---|---|---|
| rubi_integrate-0.0.3.tar.gz | 673.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rubi_integrate-0.0.3-py3-none-any.whl | Python 3 | none | any | Details |
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
|