Skip to main content

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)

Source distribution for rubi-integrate 0.0.3
File Size Uploaded
rubi_integrate-0.0.3.tar.gz 673.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for rubi-integrate 0.0.3
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.0.3 This release

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page