You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1859 lines
66 KiB
1859 lines
66 KiB
5 months ago
|
"""
|
||
|
The Risch Algorithm for transcendental function integration.
|
||
|
|
||
|
The core algorithms for the Risch algorithm are here. The subproblem
|
||
|
algorithms are in the rde.py and prde.py files for the Risch
|
||
|
Differential Equation solver and the parametric problems solvers,
|
||
|
respectively. All important information concerning the differential extension
|
||
|
for an integrand is stored in a DifferentialExtension object, which in the code
|
||
|
is usually called DE. Throughout the code and Inside the DifferentialExtension
|
||
|
object, the conventions/attribute names are that the base domain is QQ and each
|
||
|
differential extension is x, t0, t1, ..., tn-1 = DE.t. DE.x is the variable of
|
||
|
integration (Dx == 1), DE.D is a list of the derivatives of
|
||
|
x, t1, t2, ..., tn-1 = t, DE.T is the list [x, t1, t2, ..., tn-1], DE.t is the
|
||
|
outer-most variable of the differential extension at the given level (the level
|
||
|
can be adjusted using DE.increment_level() and DE.decrement_level()),
|
||
|
k is the field C(x, t0, ..., tn-2), where C is the constant field. The
|
||
|
numerator of a fraction is denoted by a and the denominator by
|
||
|
d. If the fraction is named f, fa == numer(f) and fd == denom(f).
|
||
|
Fractions are returned as tuples (fa, fd). DE.d and DE.t are used to
|
||
|
represent the topmost derivation and extension variable, respectively.
|
||
|
The docstring of a function signifies whether an argument is in k[t], in
|
||
|
which case it will just return a Poly in t, or in k(t), in which case it
|
||
|
will return the fraction (fa, fd). Other variable names probably come
|
||
|
from the names used in Bronstein's book.
|
||
|
"""
|
||
|
from types import GeneratorType
|
||
|
from functools import reduce
|
||
|
|
||
|
from sympy.core.function import Lambda
|
||
|
from sympy.core.mul import Mul
|
||
|
from sympy.core.numbers import ilcm, I, oo
|
||
|
from sympy.core.power import Pow
|
||
|
from sympy.core.relational import Ne
|
||
|
from sympy.core.singleton import S
|
||
|
from sympy.core.sorting import ordered, default_sort_key
|
||
|
from sympy.core.symbol import Dummy, Symbol
|
||
|
from sympy.functions.elementary.exponential import log, exp
|
||
|
from sympy.functions.elementary.hyperbolic import (cosh, coth, sinh,
|
||
|
tanh)
|
||
|
from sympy.functions.elementary.piecewise import Piecewise
|
||
|
from sympy.functions.elementary.trigonometric import (atan, sin, cos,
|
||
|
tan, acot, cot, asin, acos)
|
||
|
from .integrals import integrate, Integral
|
||
|
from .heurisch import _symbols
|
||
|
from sympy.polys.polyerrors import DomainError, PolynomialError
|
||
|
from sympy.polys.polytools import (real_roots, cancel, Poly, gcd,
|
||
|
reduced)
|
||
|
from sympy.polys.rootoftools import RootSum
|
||
|
from sympy.utilities.iterables import numbered_symbols
|
||
|
|
||
|
|
||
|
def integer_powers(exprs):
|
||
|
"""
|
||
|
Rewrites a list of expressions as integer multiples of each other.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
For example, if you have [x, x/2, x**2 + 1, 2*x/3], then you can rewrite
|
||
|
this as [(x/6) * 6, (x/6) * 3, (x**2 + 1) * 1, (x/6) * 4]. This is useful
|
||
|
in the Risch integration algorithm, where we must write exp(x) + exp(x/2)
|
||
|
as (exp(x/2))**2 + exp(x/2), but not as exp(x) + sqrt(exp(x)) (this is
|
||
|
because only the transcendental case is implemented and we therefore cannot
|
||
|
integrate algebraic extensions). The integer multiples returned by this
|
||
|
function for each term are the smallest possible (their content equals 1).
|
||
|
|
||
|
Returns a list of tuples where the first element is the base term and the
|
||
|
second element is a list of `(item, factor)` terms, where `factor` is the
|
||
|
integer multiplicative factor that must multiply the base term to obtain
|
||
|
the original item.
|
||
|
|
||
|
The easiest way to understand this is to look at an example:
|
||
|
|
||
|
>>> from sympy.abc import x
|
||
|
>>> from sympy.integrals.risch import integer_powers
|
||
|
>>> integer_powers([x, x/2, x**2 + 1, 2*x/3])
|
||
|
[(x/6, [(x, 6), (x/2, 3), (2*x/3, 4)]), (x**2 + 1, [(x**2 + 1, 1)])]
|
||
|
|
||
|
We can see how this relates to the example at the beginning of the
|
||
|
docstring. It chose x/6 as the first base term. Then, x can be written as
|
||
|
(x/2) * 2, so we get (0, 2), and so on. Now only element (x**2 + 1)
|
||
|
remains, and there are no other terms that can be written as a rational
|
||
|
multiple of that, so we get that it can be written as (x**2 + 1) * 1.
|
||
|
|
||
|
"""
|
||
|
# Here is the strategy:
|
||
|
|
||
|
# First, go through each term and determine if it can be rewritten as a
|
||
|
# rational multiple of any of the terms gathered so far.
|
||
|
# cancel(a/b).is_Rational is sufficient for this. If it is a multiple, we
|
||
|
# add its multiple to the dictionary.
|
||
|
|
||
|
terms = {}
|
||
|
for term in exprs:
|
||
|
for trm, trm_list in terms.items():
|
||
|
a = cancel(term/trm)
|
||
|
if a.is_Rational:
|
||
|
trm_list.append((term, a))
|
||
|
break
|
||
|
else:
|
||
|
terms[term] = [(term, S.One)]
|
||
|
|
||
|
# After we have done this, we have all the like terms together, so we just
|
||
|
# need to find a common denominator so that we can get the base term and
|
||
|
# integer multiples such that each term can be written as an integer
|
||
|
# multiple of the base term, and the content of the integers is 1.
|
||
|
|
||
|
newterms = {}
|
||
|
for term, term_list in terms.items():
|
||
|
common_denom = reduce(ilcm, [i.as_numer_denom()[1] for _, i in
|
||
|
term_list])
|
||
|
newterm = term/common_denom
|
||
|
newmults = [(i, j*common_denom) for i, j in term_list]
|
||
|
newterms[newterm] = newmults
|
||
|
|
||
|
return sorted(iter(newterms.items()), key=lambda item: item[0].sort_key())
|
||
|
|
||
|
|
||
|
class DifferentialExtension:
|
||
|
"""
|
||
|
A container for all the information relating to a differential extension.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
The attributes of this object are (see also the docstring of __init__):
|
||
|
|
||
|
- f: The original (Expr) integrand.
|
||
|
- x: The variable of integration.
|
||
|
- T: List of variables in the extension.
|
||
|
- D: List of derivations in the extension; corresponds to the elements of T.
|
||
|
- fa: Poly of the numerator of the integrand.
|
||
|
- fd: Poly of the denominator of the integrand.
|
||
|
- Tfuncs: Lambda() representations of each element of T (except for x).
|
||
|
For back-substitution after integration.
|
||
|
- backsubs: A (possibly empty) list of further substitutions to be made on
|
||
|
the final integral to make it look more like the integrand.
|
||
|
- exts:
|
||
|
- extargs:
|
||
|
- cases: List of string representations of the cases of T.
|
||
|
- t: The top level extension variable, as defined by the current level
|
||
|
(see level below).
|
||
|
- d: The top level extension derivation, as defined by the current
|
||
|
derivation (see level below).
|
||
|
- case: The string representation of the case of self.d.
|
||
|
(Note that self.T and self.D will always contain the complete extension,
|
||
|
regardless of the level. Therefore, you should ALWAYS use DE.t and DE.d
|
||
|
instead of DE.T[-1] and DE.D[-1]. If you want to have a list of the
|
||
|
derivations or variables only up to the current level, use
|
||
|
DE.D[:len(DE.D) + DE.level + 1] and DE.T[:len(DE.T) + DE.level + 1]. Note
|
||
|
that, in particular, the derivation() function does this.)
|
||
|
|
||
|
The following are also attributes, but will probably not be useful other
|
||
|
than in internal use:
|
||
|
- newf: Expr form of fa/fd.
|
||
|
- level: The number (between -1 and -len(self.T)) such that
|
||
|
self.T[self.level] == self.t and self.D[self.level] == self.d.
|
||
|
Use the methods self.increment_level() and self.decrement_level() to change
|
||
|
the current level.
|
||
|
"""
|
||
|
# __slots__ is defined mainly so we can iterate over all the attributes
|
||
|
# of the class easily (the memory use doesn't matter too much, since we
|
||
|
# only create one DifferentialExtension per integration). Also, it's nice
|
||
|
# to have a safeguard when debugging.
|
||
|
__slots__ = ('f', 'x', 'T', 'D', 'fa', 'fd', 'Tfuncs', 'backsubs',
|
||
|
'exts', 'extargs', 'cases', 'case', 't', 'd', 'newf', 'level',
|
||
|
'ts', 'dummy')
|
||
|
|
||
|
def __init__(self, f=None, x=None, handle_first='log', dummy=False, extension=None, rewrite_complex=None):
|
||
|
"""
|
||
|
Tries to build a transcendental extension tower from ``f`` with respect to ``x``.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
If it is successful, creates a DifferentialExtension object with, among
|
||
|
others, the attributes fa, fd, D, T, Tfuncs, and backsubs such that
|
||
|
fa and fd are Polys in T[-1] with rational coefficients in T[:-1],
|
||
|
fa/fd == f, and D[i] is a Poly in T[i] with rational coefficients in
|
||
|
T[:i] representing the derivative of T[i] for each i from 1 to len(T).
|
||
|
Tfuncs is a list of Lambda objects for back replacing the functions
|
||
|
after integrating. Lambda() is only used (instead of lambda) to make
|
||
|
them easier to test and debug. Note that Tfuncs corresponds to the
|
||
|
elements of T, except for T[0] == x, but they should be back-substituted
|
||
|
in reverse order. backsubs is a (possibly empty) back-substitution list
|
||
|
that should be applied on the completed integral to make it look more
|
||
|
like the original integrand.
|
||
|
|
||
|
If it is unsuccessful, it raises NotImplementedError.
|
||
|
|
||
|
You can also create an object by manually setting the attributes as a
|
||
|
dictionary to the extension keyword argument. You must include at least
|
||
|
D. Warning, any attribute that is not given will be set to None. The
|
||
|
attributes T, t, d, cases, case, x, and level are set automatically and
|
||
|
do not need to be given. The functions in the Risch Algorithm will NOT
|
||
|
check to see if an attribute is None before using it. This also does not
|
||
|
check to see if the extension is valid (non-algebraic) or even if it is
|
||
|
self-consistent. Therefore, this should only be used for
|
||
|
testing/debugging purposes.
|
||
|
"""
|
||
|
# XXX: If you need to debug this function, set the break point here
|
||
|
|
||
|
if extension:
|
||
|
if 'D' not in extension:
|
||
|
raise ValueError("At least the key D must be included with "
|
||
|
"the extension flag to DifferentialExtension.")
|
||
|
for attr in extension:
|
||
|
setattr(self, attr, extension[attr])
|
||
|
|
||
|
self._auto_attrs()
|
||
|
|
||
|
return
|
||
|
elif f is None or x is None:
|
||
|
raise ValueError("Either both f and x or a manual extension must "
|
||
|
"be given.")
|
||
|
|
||
|
if handle_first not in ('log', 'exp'):
|
||
|
raise ValueError("handle_first must be 'log' or 'exp', not %s." %
|
||
|
str(handle_first))
|
||
|
|
||
|
# f will be the original function, self.f might change if we reset
|
||
|
# (e.g., we pull out a constant from an exponential)
|
||
|
self.f = f
|
||
|
self.x = x
|
||
|
# setting the default value 'dummy'
|
||
|
self.dummy = dummy
|
||
|
self.reset()
|
||
|
exp_new_extension, log_new_extension = True, True
|
||
|
|
||
|
# case of 'automatic' choosing
|
||
|
if rewrite_complex is None:
|
||
|
rewrite_complex = I in self.f.atoms()
|
||
|
|
||
|
if rewrite_complex:
|
||
|
rewritables = {
|
||
|
(sin, cos, cot, tan, sinh, cosh, coth, tanh): exp,
|
||
|
(asin, acos, acot, atan): log,
|
||
|
}
|
||
|
# rewrite the trigonometric components
|
||
|
for candidates, rule in rewritables.items():
|
||
|
self.newf = self.newf.rewrite(candidates, rule)
|
||
|
self.newf = cancel(self.newf)
|
||
|
else:
|
||
|
if any(i.has(x) for i in self.f.atoms(sin, cos, tan, atan, asin, acos)):
|
||
|
raise NotImplementedError("Trigonometric extensions are not "
|
||
|
"supported (yet!)")
|
||
|
|
||
|
exps = set()
|
||
|
pows = set()
|
||
|
numpows = set()
|
||
|
sympows = set()
|
||
|
logs = set()
|
||
|
symlogs = set()
|
||
|
|
||
|
while True:
|
||
|
if self.newf.is_rational_function(*self.T):
|
||
|
break
|
||
|
|
||
|
if not exp_new_extension and not log_new_extension:
|
||
|
# We couldn't find a new extension on the last pass, so I guess
|
||
|
# we can't do it.
|
||
|
raise NotImplementedError("Couldn't find an elementary "
|
||
|
"transcendental extension for %s. Try using a " % str(f) +
|
||
|
"manual extension with the extension flag.")
|
||
|
|
||
|
exps, pows, numpows, sympows, log_new_extension = \
|
||
|
self._rewrite_exps_pows(exps, pows, numpows, sympows, log_new_extension)
|
||
|
|
||
|
logs, symlogs = self._rewrite_logs(logs, symlogs)
|
||
|
|
||
|
if handle_first == 'exp' or not log_new_extension:
|
||
|
exp_new_extension = self._exp_part(exps)
|
||
|
if exp_new_extension is None:
|
||
|
# reset and restart
|
||
|
self.f = self.newf
|
||
|
self.reset()
|
||
|
exp_new_extension = True
|
||
|
continue
|
||
|
|
||
|
if handle_first == 'log' or not exp_new_extension:
|
||
|
log_new_extension = self._log_part(logs)
|
||
|
|
||
|
self.fa, self.fd = frac_in(self.newf, self.t)
|
||
|
self._auto_attrs()
|
||
|
|
||
|
return
|
||
|
|
||
|
def __getattr__(self, attr):
|
||
|
# Avoid AttributeErrors when debugging
|
||
|
if attr not in self.__slots__:
|
||
|
raise AttributeError("%s has no attribute %s" % (repr(self), repr(attr)))
|
||
|
return None
|
||
|
|
||
|
def _rewrite_exps_pows(self, exps, pows, numpows,
|
||
|
sympows, log_new_extension):
|
||
|
"""
|
||
|
Rewrite exps/pows for better processing.
|
||
|
"""
|
||
|
from .prde import is_deriv_k
|
||
|
|
||
|
# Pre-preparsing.
|
||
|
#################
|
||
|
# Get all exp arguments, so we can avoid ahead of time doing
|
||
|
# something like t1 = exp(x), t2 = exp(x/2) == sqrt(t1).
|
||
|
|
||
|
# Things like sqrt(exp(x)) do not automatically simplify to
|
||
|
# exp(x/2), so they will be viewed as algebraic. The easiest way
|
||
|
# to handle this is to convert all instances of exp(a)**Rational
|
||
|
# to exp(Rational*a) before doing anything else. Note that the
|
||
|
# _exp_part code can generate terms of this form, so we do need to
|
||
|
# do this at each pass (or else modify it to not do that).
|
||
|
|
||
|
ratpows = [i for i in self.newf.atoms(Pow)
|
||
|
if (isinstance(i.base, exp) and i.exp.is_Rational)]
|
||
|
|
||
|
ratpows_repl = [
|
||
|
(i, i.base.base**(i.exp*i.base.exp)) for i in ratpows]
|
||
|
self.backsubs += [(j, i) for i, j in ratpows_repl]
|
||
|
self.newf = self.newf.xreplace(dict(ratpows_repl))
|
||
|
|
||
|
# To make the process deterministic, the args are sorted
|
||
|
# so that functions with smaller op-counts are processed first.
|
||
|
# Ties are broken with the default_sort_key.
|
||
|
|
||
|
# XXX Although the method is deterministic no additional work
|
||
|
# has been done to guarantee that the simplest solution is
|
||
|
# returned and that it would be affected be using different
|
||
|
# variables. Though it is possible that this is the case
|
||
|
# one should know that it has not been done intentionally, so
|
||
|
# further improvements may be possible.
|
||
|
|
||
|
# TODO: This probably doesn't need to be completely recomputed at
|
||
|
# each pass.
|
||
|
exps = update_sets(exps, self.newf.atoms(exp),
|
||
|
lambda i: i.exp.is_rational_function(*self.T) and
|
||
|
i.exp.has(*self.T))
|
||
|
pows = update_sets(pows, self.newf.atoms(Pow),
|
||
|
lambda i: i.exp.is_rational_function(*self.T) and
|
||
|
i.exp.has(*self.T))
|
||
|
numpows = update_sets(numpows, set(pows),
|
||
|
lambda i: not i.base.has(*self.T))
|
||
|
sympows = update_sets(sympows, set(pows) - set(numpows),
|
||
|
lambda i: i.base.is_rational_function(*self.T) and
|
||
|
not i.exp.is_Integer)
|
||
|
|
||
|
# The easiest way to deal with non-base E powers is to convert them
|
||
|
# into base E, integrate, and then convert back.
|
||
|
for i in ordered(pows):
|
||
|
old = i
|
||
|
new = exp(i.exp*log(i.base))
|
||
|
# If exp is ever changed to automatically reduce exp(x*log(2))
|
||
|
# to 2**x, then this will break. The solution is to not change
|
||
|
# exp to do that :)
|
||
|
if i in sympows:
|
||
|
if i.exp.is_Rational:
|
||
|
raise NotImplementedError("Algebraic extensions are "
|
||
|
"not supported (%s)." % str(i))
|
||
|
# We can add a**b only if log(a) in the extension, because
|
||
|
# a**b == exp(b*log(a)).
|
||
|
basea, based = frac_in(i.base, self.t)
|
||
|
A = is_deriv_k(basea, based, self)
|
||
|
if A is None:
|
||
|
# Nonelementary monomial (so far)
|
||
|
|
||
|
# TODO: Would there ever be any benefit from just
|
||
|
# adding log(base) as a new monomial?
|
||
|
# ANSWER: Yes, otherwise we can't integrate x**x (or
|
||
|
# rather prove that it has no elementary integral)
|
||
|
# without first manually rewriting it as exp(x*log(x))
|
||
|
self.newf = self.newf.xreplace({old: new})
|
||
|
self.backsubs += [(new, old)]
|
||
|
log_new_extension = self._log_part([log(i.base)])
|
||
|
exps = update_sets(exps, self.newf.atoms(exp), lambda i:
|
||
|
i.exp.is_rational_function(*self.T) and i.exp.has(*self.T))
|
||
|
continue
|
||
|
ans, u, const = A
|
||
|
newterm = exp(i.exp*(log(const) + u))
|
||
|
# Under the current implementation, exp kills terms
|
||
|
# only if they are of the form a*log(x), where a is a
|
||
|
# Number. This case should have already been killed by the
|
||
|
# above tests. Again, if this changes to kill more than
|
||
|
# that, this will break, which maybe is a sign that you
|
||
|
# shouldn't be changing that. Actually, if anything, this
|
||
|
# auto-simplification should be removed. See
|
||
|
# https://groups.google.com/group/sympy/browse_thread/thread/a61d48235f16867f
|
||
|
|
||
|
self.newf = self.newf.xreplace({i: newterm})
|
||
|
|
||
|
elif i not in numpows:
|
||
|
continue
|
||
|
else:
|
||
|
# i in numpows
|
||
|
newterm = new
|
||
|
# TODO: Just put it in self.Tfuncs
|
||
|
self.backsubs.append((new, old))
|
||
|
self.newf = self.newf.xreplace({old: newterm})
|
||
|
exps.append(newterm)
|
||
|
|
||
|
return exps, pows, numpows, sympows, log_new_extension
|
||
|
|
||
|
def _rewrite_logs(self, logs, symlogs):
|
||
|
"""
|
||
|
Rewrite logs for better processing.
|
||
|
"""
|
||
|
atoms = self.newf.atoms(log)
|
||
|
logs = update_sets(logs, atoms,
|
||
|
lambda i: i.args[0].is_rational_function(*self.T) and
|
||
|
i.args[0].has(*self.T))
|
||
|
symlogs = update_sets(symlogs, atoms,
|
||
|
lambda i: i.has(*self.T) and i.args[0].is_Pow and
|
||
|
i.args[0].base.is_rational_function(*self.T) and
|
||
|
not i.args[0].exp.is_Integer)
|
||
|
|
||
|
# We can handle things like log(x**y) by converting it to y*log(x)
|
||
|
# This will fix not only symbolic exponents of the argument, but any
|
||
|
# non-Integer exponent, like log(sqrt(x)). The exponent can also
|
||
|
# depend on x, like log(x**x).
|
||
|
for i in ordered(symlogs):
|
||
|
# Unlike in the exponential case above, we do not ever
|
||
|
# potentially add new monomials (above we had to add log(a)).
|
||
|
# Therefore, there is no need to run any is_deriv functions
|
||
|
# here. Just convert log(a**b) to b*log(a) and let
|
||
|
# log_new_extension() handle it from there.
|
||
|
lbase = log(i.args[0].base)
|
||
|
logs.append(lbase)
|
||
|
new = i.args[0].exp*lbase
|
||
|
self.newf = self.newf.xreplace({i: new})
|
||
|
self.backsubs.append((new, i))
|
||
|
|
||
|
# remove any duplicates
|
||
|
logs = sorted(set(logs), key=default_sort_key)
|
||
|
|
||
|
return logs, symlogs
|
||
|
|
||
|
def _auto_attrs(self):
|
||
|
"""
|
||
|
Set attributes that are generated automatically.
|
||
|
"""
|
||
|
if not self.T:
|
||
|
# i.e., when using the extension flag and T isn't given
|
||
|
self.T = [i.gen for i in self.D]
|
||
|
if not self.x:
|
||
|
self.x = self.T[0]
|
||
|
self.cases = [get_case(d, t) for d, t in zip(self.D, self.T)]
|
||
|
self.level = -1
|
||
|
self.t = self.T[self.level]
|
||
|
self.d = self.D[self.level]
|
||
|
self.case = self.cases[self.level]
|
||
|
|
||
|
def _exp_part(self, exps):
|
||
|
"""
|
||
|
Try to build an exponential extension.
|
||
|
|
||
|
Returns
|
||
|
=======
|
||
|
|
||
|
Returns True if there was a new extension, False if there was no new
|
||
|
extension but it was able to rewrite the given exponentials in terms
|
||
|
of the existing extension, and None if the entire extension building
|
||
|
process should be restarted. If the process fails because there is no
|
||
|
way around an algebraic extension (e.g., exp(log(x)/2)), it will raise
|
||
|
NotImplementedError.
|
||
|
"""
|
||
|
from .prde import is_log_deriv_k_t_radical
|
||
|
new_extension = False
|
||
|
restart = False
|
||
|
expargs = [i.exp for i in exps]
|
||
|
ip = integer_powers(expargs)
|
||
|
for arg, others in ip:
|
||
|
# Minimize potential problems with algebraic substitution
|
||
|
others.sort(key=lambda i: i[1])
|
||
|
|
||
|
arga, argd = frac_in(arg, self.t)
|
||
|
A = is_log_deriv_k_t_radical(arga, argd, self)
|
||
|
|
||
|
if A is not None:
|
||
|
ans, u, n, const = A
|
||
|
# if n is 1 or -1, it's algebraic, but we can handle it
|
||
|
if n == -1:
|
||
|
# This probably will never happen, because
|
||
|
# Rational.as_numer_denom() returns the negative term in
|
||
|
# the numerator. But in case that changes, reduce it to
|
||
|
# n == 1.
|
||
|
n = 1
|
||
|
u **= -1
|
||
|
const *= -1
|
||
|
ans = [(i, -j) for i, j in ans]
|
||
|
|
||
|
if n == 1:
|
||
|
# Example: exp(x + x**2) over QQ(x, exp(x), exp(x**2))
|
||
|
self.newf = self.newf.xreplace({exp(arg): exp(const)*Mul(*[
|
||
|
u**power for u, power in ans])})
|
||
|
self.newf = self.newf.xreplace({exp(p*exparg):
|
||
|
exp(const*p) * Mul(*[u**power for u, power in ans])
|
||
|
for exparg, p in others})
|
||
|
# TODO: Add something to backsubs to put exp(const*p)
|
||
|
# back together.
|
||
|
|
||
|
continue
|
||
|
|
||
|
else:
|
||
|
# Bad news: we have an algebraic radical. But maybe we
|
||
|
# could still avoid it by choosing a different extension.
|
||
|
# For example, integer_powers() won't handle exp(x/2 + 1)
|
||
|
# over QQ(x, exp(x)), but if we pull out the exp(1), it
|
||
|
# will. Or maybe we have exp(x + x**2/2), over
|
||
|
# QQ(x, exp(x), exp(x**2)), which is exp(x)*sqrt(exp(x**2)),
|
||
|
# but if we use QQ(x, exp(x), exp(x**2/2)), then they will
|
||
|
# all work.
|
||
|
#
|
||
|
# So here is what we do: If there is a non-zero const, pull
|
||
|
# it out and retry. Also, if len(ans) > 1, then rewrite
|
||
|
# exp(arg) as the product of exponentials from ans, and
|
||
|
# retry that. If const == 0 and len(ans) == 1, then we
|
||
|
# assume that it would have been handled by either
|
||
|
# integer_powers() or n == 1 above if it could be handled,
|
||
|
# so we give up at that point. For example, you can never
|
||
|
# handle exp(log(x)/2) because it equals sqrt(x).
|
||
|
|
||
|
if const or len(ans) > 1:
|
||
|
rad = Mul(*[term**(power/n) for term, power in ans])
|
||
|
self.newf = self.newf.xreplace({exp(p*exparg):
|
||
|
exp(const*p)*rad for exparg, p in others})
|
||
|
self.newf = self.newf.xreplace(dict(list(zip(reversed(self.T),
|
||
|
reversed([f(self.x) for f in self.Tfuncs])))))
|
||
|
restart = True
|
||
|
break
|
||
|
else:
|
||
|
# TODO: give algebraic dependence in error string
|
||
|
raise NotImplementedError("Cannot integrate over "
|
||
|
"algebraic extensions.")
|
||
|
|
||
|
else:
|
||
|
arga, argd = frac_in(arg, self.t)
|
||
|
darga = (argd*derivation(Poly(arga, self.t), self) -
|
||
|
arga*derivation(Poly(argd, self.t), self))
|
||
|
dargd = argd**2
|
||
|
darga, dargd = darga.cancel(dargd, include=True)
|
||
|
darg = darga.as_expr()/dargd.as_expr()
|
||
|
self.t = next(self.ts)
|
||
|
self.T.append(self.t)
|
||
|
self.extargs.append(arg)
|
||
|
self.exts.append('exp')
|
||
|
self.D.append(darg.as_poly(self.t, expand=False)*Poly(self.t,
|
||
|
self.t, expand=False))
|
||
|
if self.dummy:
|
||
|
i = Dummy("i")
|
||
|
else:
|
||
|
i = Symbol('i')
|
||
|
self.Tfuncs += [Lambda(i, exp(arg.subs(self.x, i)))]
|
||
|
self.newf = self.newf.xreplace(
|
||
|
{exp(exparg): self.t**p for exparg, p in others})
|
||
|
new_extension = True
|
||
|
|
||
|
if restart:
|
||
|
return None
|
||
|
return new_extension
|
||
|
|
||
|
def _log_part(self, logs):
|
||
|
"""
|
||
|
Try to build a logarithmic extension.
|
||
|
|
||
|
Returns
|
||
|
=======
|
||
|
|
||
|
Returns True if there was a new extension and False if there was no new
|
||
|
extension but it was able to rewrite the given logarithms in terms
|
||
|
of the existing extension. Unlike with exponential extensions, there
|
||
|
is no way that a logarithm is not transcendental over and cannot be
|
||
|
rewritten in terms of an already existing extension in a non-algebraic
|
||
|
way, so this function does not ever return None or raise
|
||
|
NotImplementedError.
|
||
|
"""
|
||
|
from .prde import is_deriv_k
|
||
|
new_extension = False
|
||
|
logargs = [i.args[0] for i in logs]
|
||
|
for arg in ordered(logargs):
|
||
|
# The log case is easier, because whenever a logarithm is algebraic
|
||
|
# over the base field, it is of the form a1*t1 + ... an*tn + c,
|
||
|
# which is a polynomial, so we can just replace it with that.
|
||
|
# In other words, we don't have to worry about radicals.
|
||
|
arga, argd = frac_in(arg, self.t)
|
||
|
A = is_deriv_k(arga, argd, self)
|
||
|
if A is not None:
|
||
|
ans, u, const = A
|
||
|
newterm = log(const) + u
|
||
|
self.newf = self.newf.xreplace({log(arg): newterm})
|
||
|
continue
|
||
|
|
||
|
else:
|
||
|
arga, argd = frac_in(arg, self.t)
|
||
|
darga = (argd*derivation(Poly(arga, self.t), self) -
|
||
|
arga*derivation(Poly(argd, self.t), self))
|
||
|
dargd = argd**2
|
||
|
darg = darga.as_expr()/dargd.as_expr()
|
||
|
self.t = next(self.ts)
|
||
|
self.T.append(self.t)
|
||
|
self.extargs.append(arg)
|
||
|
self.exts.append('log')
|
||
|
self.D.append(cancel(darg.as_expr()/arg).as_poly(self.t,
|
||
|
expand=False))
|
||
|
if self.dummy:
|
||
|
i = Dummy("i")
|
||
|
else:
|
||
|
i = Symbol('i')
|
||
|
self.Tfuncs += [Lambda(i, log(arg.subs(self.x, i)))]
|
||
|
self.newf = self.newf.xreplace({log(arg): self.t})
|
||
|
new_extension = True
|
||
|
|
||
|
return new_extension
|
||
|
|
||
|
@property
|
||
|
def _important_attrs(self):
|
||
|
"""
|
||
|
Returns some of the more important attributes of self.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Used for testing and debugging purposes.
|
||
|
|
||
|
The attributes are (fa, fd, D, T, Tfuncs, backsubs,
|
||
|
exts, extargs).
|
||
|
"""
|
||
|
return (self.fa, self.fd, self.D, self.T, self.Tfuncs,
|
||
|
self.backsubs, self.exts, self.extargs)
|
||
|
|
||
|
# NOTE: this printing doesn't follow the Python's standard
|
||
|
# eval(repr(DE)) == DE, where DE is the DifferentialExtension object,
|
||
|
# also this printing is supposed to contain all the important
|
||
|
# attributes of a DifferentialExtension object
|
||
|
def __repr__(self):
|
||
|
# no need to have GeneratorType object printed in it
|
||
|
r = [(attr, getattr(self, attr)) for attr in self.__slots__
|
||
|
if not isinstance(getattr(self, attr), GeneratorType)]
|
||
|
return self.__class__.__name__ + '(dict(%r))' % (r)
|
||
|
|
||
|
# fancy printing of DifferentialExtension object
|
||
|
def __str__(self):
|
||
|
return (self.__class__.__name__ + '({fa=%s, fd=%s, D=%s})' %
|
||
|
(self.fa, self.fd, self.D))
|
||
|
|
||
|
# should only be used for debugging purposes, internally
|
||
|
# f1 = f2 = log(x) at different places in code execution
|
||
|
# may return D1 != D2 as True, since 'level' or other attribute
|
||
|
# may differ
|
||
|
def __eq__(self, other):
|
||
|
for attr in self.__class__.__slots__:
|
||
|
d1, d2 = getattr(self, attr), getattr(other, attr)
|
||
|
if not (isinstance(d1, GeneratorType) or d1 == d2):
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
def reset(self):
|
||
|
"""
|
||
|
Reset self to an initial state. Used by __init__.
|
||
|
"""
|
||
|
self.t = self.x
|
||
|
self.T = [self.x]
|
||
|
self.D = [Poly(1, self.x)]
|
||
|
self.level = -1
|
||
|
self.exts = [None]
|
||
|
self.extargs = [None]
|
||
|
if self.dummy:
|
||
|
self.ts = numbered_symbols('t', cls=Dummy)
|
||
|
else:
|
||
|
# For testing
|
||
|
self.ts = numbered_symbols('t')
|
||
|
# For various things that we change to make things work that we need to
|
||
|
# change back when we are done.
|
||
|
self.backsubs = []
|
||
|
self.Tfuncs = []
|
||
|
self.newf = self.f
|
||
|
|
||
|
def indices(self, extension):
|
||
|
"""
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
extension : str
|
||
|
Represents a valid extension type.
|
||
|
|
||
|
Returns
|
||
|
=======
|
||
|
|
||
|
list: A list of indices of 'exts' where extension of
|
||
|
type 'extension' is present.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.integrals.risch import DifferentialExtension
|
||
|
>>> from sympy import log, exp
|
||
|
>>> from sympy.abc import x
|
||
|
>>> DE = DifferentialExtension(log(x) + exp(x), x, handle_first='exp')
|
||
|
>>> DE.indices('log')
|
||
|
[2]
|
||
|
>>> DE.indices('exp')
|
||
|
[1]
|
||
|
|
||
|
"""
|
||
|
return [i for i, ext in enumerate(self.exts) if ext == extension]
|
||
|
|
||
|
def increment_level(self):
|
||
|
"""
|
||
|
Increment the level of self.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
This makes the working differential extension larger. self.level is
|
||
|
given relative to the end of the list (-1, -2, etc.), so we do not need
|
||
|
do worry about it when building the extension.
|
||
|
"""
|
||
|
if self.level >= -1:
|
||
|
raise ValueError("The level of the differential extension cannot "
|
||
|
"be incremented any further.")
|
||
|
|
||
|
self.level += 1
|
||
|
self.t = self.T[self.level]
|
||
|
self.d = self.D[self.level]
|
||
|
self.case = self.cases[self.level]
|
||
|
return None
|
||
|
|
||
|
def decrement_level(self):
|
||
|
"""
|
||
|
Decrease the level of self.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
This makes the working differential extension smaller. self.level is
|
||
|
given relative to the end of the list (-1, -2, etc.), so we do not need
|
||
|
do worry about it when building the extension.
|
||
|
"""
|
||
|
if self.level <= -len(self.T):
|
||
|
raise ValueError("The level of the differential extension cannot "
|
||
|
"be decremented any further.")
|
||
|
|
||
|
self.level -= 1
|
||
|
self.t = self.T[self.level]
|
||
|
self.d = self.D[self.level]
|
||
|
self.case = self.cases[self.level]
|
||
|
return None
|
||
|
|
||
|
|
||
|
def update_sets(seq, atoms, func):
|
||
|
s = set(seq)
|
||
|
s = atoms.intersection(s)
|
||
|
new = atoms - s
|
||
|
s.update(list(filter(func, new)))
|
||
|
return list(s)
|
||
|
|
||
|
|
||
|
class DecrementLevel:
|
||
|
"""
|
||
|
A context manager for decrementing the level of a DifferentialExtension.
|
||
|
"""
|
||
|
__slots__ = ('DE',)
|
||
|
|
||
|
def __init__(self, DE):
|
||
|
self.DE = DE
|
||
|
return
|
||
|
|
||
|
def __enter__(self):
|
||
|
self.DE.decrement_level()
|
||
|
|
||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||
|
self.DE.increment_level()
|
||
|
|
||
|
|
||
|
class NonElementaryIntegralException(Exception):
|
||
|
"""
|
||
|
Exception used by subroutines within the Risch algorithm to indicate to one
|
||
|
another that the function being integrated does not have an elementary
|
||
|
integral in the given differential field.
|
||
|
"""
|
||
|
# TODO: Rewrite algorithms below to use this (?)
|
||
|
|
||
|
# TODO: Pass through information about why the integral was nonelementary,
|
||
|
# and store that in the resulting NonElementaryIntegral somehow.
|
||
|
pass
|
||
|
|
||
|
|
||
|
def gcdex_diophantine(a, b, c):
|
||
|
"""
|
||
|
Extended Euclidean Algorithm, Diophantine version.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Given ``a``, ``b`` in K[x] and ``c`` in (a, b), the ideal generated by ``a`` and
|
||
|
``b``, return (s, t) such that s*a + t*b == c and either s == 0 or s.degree()
|
||
|
< b.degree().
|
||
|
"""
|
||
|
# Extended Euclidean Algorithm (Diophantine Version) pg. 13
|
||
|
# TODO: This should go in densetools.py.
|
||
|
# XXX: Bettter name?
|
||
|
|
||
|
s, g = a.half_gcdex(b)
|
||
|
s *= c.exquo(g) # Inexact division means c is not in (a, b)
|
||
|
if s and s.degree() >= b.degree():
|
||
|
_, s = s.div(b)
|
||
|
t = (c - s*a).exquo(b)
|
||
|
return (s, t)
|
||
|
|
||
|
|
||
|
def frac_in(f, t, *, cancel=False, **kwargs):
|
||
|
"""
|
||
|
Returns the tuple (fa, fd), where fa and fd are Polys in t.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
This is a common idiom in the Risch Algorithm functions, so we abstract
|
||
|
it out here. ``f`` should be a basic expression, a Poly, or a tuple (fa, fd),
|
||
|
where fa and fd are either basic expressions or Polys, and f == fa/fd.
|
||
|
**kwargs are applied to Poly.
|
||
|
"""
|
||
|
if isinstance(f, tuple):
|
||
|
fa, fd = f
|
||
|
f = fa.as_expr()/fd.as_expr()
|
||
|
fa, fd = f.as_expr().as_numer_denom()
|
||
|
fa, fd = fa.as_poly(t, **kwargs), fd.as_poly(t, **kwargs)
|
||
|
if cancel:
|
||
|
fa, fd = fa.cancel(fd, include=True)
|
||
|
if fa is None or fd is None:
|
||
|
raise ValueError("Could not turn %s into a fraction in %s." % (f, t))
|
||
|
return (fa, fd)
|
||
|
|
||
|
|
||
|
def as_poly_1t(p, t, z):
|
||
|
"""
|
||
|
(Hackish) way to convert an element ``p`` of K[t, 1/t] to K[t, z].
|
||
|
|
||
|
In other words, ``z == 1/t`` will be a dummy variable that Poly can handle
|
||
|
better.
|
||
|
|
||
|
See issue 5131.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy import random_poly
|
||
|
>>> from sympy.integrals.risch import as_poly_1t
|
||
|
>>> from sympy.abc import x, z
|
||
|
|
||
|
>>> p1 = random_poly(x, 10, -10, 10)
|
||
|
>>> p2 = random_poly(x, 10, -10, 10)
|
||
|
>>> p = p1 + p2.subs(x, 1/x)
|
||
|
>>> as_poly_1t(p, x, z).as_expr().subs(z, 1/x) == p
|
||
|
True
|
||
|
"""
|
||
|
# TODO: Use this on the final result. That way, we can avoid answers like
|
||
|
# (...)*exp(-x).
|
||
|
pa, pd = frac_in(p, t, cancel=True)
|
||
|
if not pd.is_monomial:
|
||
|
# XXX: Is there a better Poly exception that we could raise here?
|
||
|
# Either way, if you see this (from the Risch Algorithm) it indicates
|
||
|
# a bug.
|
||
|
raise PolynomialError("%s is not an element of K[%s, 1/%s]." % (p, t, t))
|
||
|
d = pd.degree(t)
|
||
|
one_t_part = pa.slice(0, d + 1)
|
||
|
r = pd.degree() - pa.degree()
|
||
|
t_part = pa - one_t_part
|
||
|
try:
|
||
|
t_part = t_part.to_field().exquo(pd)
|
||
|
except DomainError as e:
|
||
|
# issue 4950
|
||
|
raise NotImplementedError(e)
|
||
|
# Compute the negative degree parts.
|
||
|
one_t_part = Poly.from_list(reversed(one_t_part.rep.rep), *one_t_part.gens,
|
||
|
domain=one_t_part.domain)
|
||
|
if 0 < r < oo:
|
||
|
one_t_part *= Poly(t**r, t)
|
||
|
|
||
|
one_t_part = one_t_part.replace(t, z) # z will be 1/t
|
||
|
if pd.nth(d):
|
||
|
one_t_part *= Poly(1/pd.nth(d), z, expand=False)
|
||
|
ans = t_part.as_poly(t, z, expand=False) + one_t_part.as_poly(t, z,
|
||
|
expand=False)
|
||
|
|
||
|
return ans
|
||
|
|
||
|
|
||
|
def derivation(p, DE, coefficientD=False, basic=False):
|
||
|
"""
|
||
|
Computes Dp.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Given the derivation D with D = d/dx and p is a polynomial in t over
|
||
|
K(x), return Dp.
|
||
|
|
||
|
If coefficientD is True, it computes the derivation kD
|
||
|
(kappaD), which is defined as kD(sum(ai*Xi**i, (i, 0, n))) ==
|
||
|
sum(Dai*Xi**i, (i, 1, n)) (Definition 3.2.2, page 80). X in this case is
|
||
|
T[-1], so coefficientD computes the derivative just with respect to T[:-1],
|
||
|
with T[-1] treated as a constant.
|
||
|
|
||
|
If ``basic=True``, the returns a Basic expression. Elements of D can still be
|
||
|
instances of Poly.
|
||
|
"""
|
||
|
if basic:
|
||
|
r = 0
|
||
|
else:
|
||
|
r = Poly(0, DE.t)
|
||
|
|
||
|
t = DE.t
|
||
|
if coefficientD:
|
||
|
if DE.level <= -len(DE.T):
|
||
|
# 'base' case, the answer is 0.
|
||
|
return r
|
||
|
DE.decrement_level()
|
||
|
|
||
|
D = DE.D[:len(DE.D) + DE.level + 1]
|
||
|
T = DE.T[:len(DE.T) + DE.level + 1]
|
||
|
|
||
|
for d, v in zip(D, T):
|
||
|
pv = p.as_poly(v)
|
||
|
if pv is None or basic:
|
||
|
pv = p.as_expr()
|
||
|
|
||
|
if basic:
|
||
|
r += d.as_expr()*pv.diff(v)
|
||
|
else:
|
||
|
r += (d.as_expr()*pv.diff(v).as_expr()).as_poly(t)
|
||
|
|
||
|
if basic:
|
||
|
r = cancel(r)
|
||
|
if coefficientD:
|
||
|
DE.increment_level()
|
||
|
|
||
|
return r
|
||
|
|
||
|
|
||
|
def get_case(d, t):
|
||
|
"""
|
||
|
Returns the type of the derivation d.
|
||
|
|
||
|
Returns one of {'exp', 'tan', 'base', 'primitive', 'other_linear',
|
||
|
'other_nonlinear'}.
|
||
|
"""
|
||
|
if not d.expr.has(t):
|
||
|
if d.is_one:
|
||
|
return 'base'
|
||
|
return 'primitive'
|
||
|
if d.rem(Poly(t, t)).is_zero:
|
||
|
return 'exp'
|
||
|
if d.rem(Poly(1 + t**2, t)).is_zero:
|
||
|
return 'tan'
|
||
|
if d.degree(t) > 1:
|
||
|
return 'other_nonlinear'
|
||
|
return 'other_linear'
|
||
|
|
||
|
|
||
|
def splitfactor(p, DE, coefficientD=False, z=None):
|
||
|
"""
|
||
|
Splitting factorization.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Given a derivation D on k[t] and ``p`` in k[t], return (p_n, p_s) in
|
||
|
k[t] x k[t] such that p = p_n*p_s, p_s is special, and each square
|
||
|
factor of p_n is normal.
|
||
|
|
||
|
Page. 100
|
||
|
"""
|
||
|
kinv = [1/x for x in DE.T[:DE.level]]
|
||
|
if z:
|
||
|
kinv.append(z)
|
||
|
|
||
|
One = Poly(1, DE.t, domain=p.get_domain())
|
||
|
Dp = derivation(p, DE, coefficientD=coefficientD)
|
||
|
# XXX: Is this right?
|
||
|
if p.is_zero:
|
||
|
return (p, One)
|
||
|
|
||
|
if not p.expr.has(DE.t):
|
||
|
s = p.as_poly(*kinv).gcd(Dp.as_poly(*kinv)).as_poly(DE.t)
|
||
|
n = p.exquo(s)
|
||
|
return (n, s)
|
||
|
|
||
|
if not Dp.is_zero:
|
||
|
h = p.gcd(Dp).to_field()
|
||
|
g = p.gcd(p.diff(DE.t)).to_field()
|
||
|
s = h.exquo(g)
|
||
|
|
||
|
if s.degree(DE.t) == 0:
|
||
|
return (p, One)
|
||
|
|
||
|
q_split = splitfactor(p.exquo(s), DE, coefficientD=coefficientD)
|
||
|
|
||
|
return (q_split[0], q_split[1]*s)
|
||
|
else:
|
||
|
return (p, One)
|
||
|
|
||
|
|
||
|
def splitfactor_sqf(p, DE, coefficientD=False, z=None, basic=False):
|
||
|
"""
|
||
|
Splitting Square-free Factorization.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Given a derivation D on k[t] and ``p`` in k[t], returns (N1, ..., Nm)
|
||
|
and (S1, ..., Sm) in k[t]^m such that p =
|
||
|
(N1*N2**2*...*Nm**m)*(S1*S2**2*...*Sm**m) is a splitting
|
||
|
factorization of ``p`` and the Ni and Si are square-free and coprime.
|
||
|
"""
|
||
|
# TODO: This algorithm appears to be faster in every case
|
||
|
# TODO: Verify this and splitfactor() for multiple extensions
|
||
|
kkinv = [1/x for x in DE.T[:DE.level]] + DE.T[:DE.level]
|
||
|
if z:
|
||
|
kkinv = [z]
|
||
|
|
||
|
S = []
|
||
|
N = []
|
||
|
p_sqf = p.sqf_list_include()
|
||
|
if p.is_zero:
|
||
|
return (((p, 1),), ())
|
||
|
|
||
|
for pi, i in p_sqf:
|
||
|
Si = pi.as_poly(*kkinv).gcd(derivation(pi, DE,
|
||
|
coefficientD=coefficientD,basic=basic).as_poly(*kkinv)).as_poly(DE.t)
|
||
|
pi = Poly(pi, DE.t)
|
||
|
Si = Poly(Si, DE.t)
|
||
|
Ni = pi.exquo(Si)
|
||
|
if not Si.is_one:
|
||
|
S.append((Si, i))
|
||
|
if not Ni.is_one:
|
||
|
N.append((Ni, i))
|
||
|
|
||
|
return (tuple(N), tuple(S))
|
||
|
|
||
|
|
||
|
def canonical_representation(a, d, DE):
|
||
|
"""
|
||
|
Canonical Representation.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Given a derivation D on k[t] and f = a/d in k(t), return (f_p, f_s,
|
||
|
f_n) in k[t] x k(t) x k(t) such that f = f_p + f_s + f_n is the
|
||
|
canonical representation of f (f_p is a polynomial, f_s is reduced
|
||
|
(has a special denominator), and f_n is simple (has a normal
|
||
|
denominator).
|
||
|
"""
|
||
|
# Make d monic
|
||
|
l = Poly(1/d.LC(), DE.t)
|
||
|
a, d = a.mul(l), d.mul(l)
|
||
|
|
||
|
q, r = a.div(d)
|
||
|
dn, ds = splitfactor(d, DE)
|
||
|
|
||
|
b, c = gcdex_diophantine(dn.as_poly(DE.t), ds.as_poly(DE.t), r.as_poly(DE.t))
|
||
|
b, c = b.as_poly(DE.t), c.as_poly(DE.t)
|
||
|
|
||
|
return (q, (b, ds), (c, dn))
|
||
|
|
||
|
|
||
|
def hermite_reduce(a, d, DE):
|
||
|
"""
|
||
|
Hermite Reduction - Mack's Linear Version.
|
||
|
|
||
|
Given a derivation D on k(t) and f = a/d in k(t), returns g, h, r in
|
||
|
k(t) such that f = Dg + h + r, h is simple, and r is reduced.
|
||
|
|
||
|
"""
|
||
|
# Make d monic
|
||
|
l = Poly(1/d.LC(), DE.t)
|
||
|
a, d = a.mul(l), d.mul(l)
|
||
|
|
||
|
fp, fs, fn = canonical_representation(a, d, DE)
|
||
|
a, d = fn
|
||
|
l = Poly(1/d.LC(), DE.t)
|
||
|
a, d = a.mul(l), d.mul(l)
|
||
|
|
||
|
ga = Poly(0, DE.t)
|
||
|
gd = Poly(1, DE.t)
|
||
|
|
||
|
dd = derivation(d, DE)
|
||
|
dm = gcd(d.to_field(), dd.to_field()).as_poly(DE.t)
|
||
|
ds, _ = d.div(dm)
|
||
|
|
||
|
while dm.degree(DE.t) > 0:
|
||
|
|
||
|
ddm = derivation(dm, DE)
|
||
|
dm2 = gcd(dm.to_field(), ddm.to_field())
|
||
|
dms, _ = dm.div(dm2)
|
||
|
ds_ddm = ds.mul(ddm)
|
||
|
ds_ddm_dm, _ = ds_ddm.div(dm)
|
||
|
|
||
|
b, c = gcdex_diophantine(-ds_ddm_dm.as_poly(DE.t),
|
||
|
dms.as_poly(DE.t), a.as_poly(DE.t))
|
||
|
b, c = b.as_poly(DE.t), c.as_poly(DE.t)
|
||
|
|
||
|
db = derivation(b, DE).as_poly(DE.t)
|
||
|
ds_dms, _ = ds.div(dms)
|
||
|
a = c.as_poly(DE.t) - db.mul(ds_dms).as_poly(DE.t)
|
||
|
|
||
|
ga = ga*dm + b*gd
|
||
|
gd = gd*dm
|
||
|
ga, gd = ga.cancel(gd, include=True)
|
||
|
dm = dm2
|
||
|
|
||
|
q, r = a.div(ds)
|
||
|
ga, gd = ga.cancel(gd, include=True)
|
||
|
|
||
|
r, d = r.cancel(ds, include=True)
|
||
|
rra = q*fs[1] + fp*fs[1] + fs[0]
|
||
|
rrd = fs[1]
|
||
|
rra, rrd = rra.cancel(rrd, include=True)
|
||
|
|
||
|
return ((ga, gd), (r, d), (rra, rrd))
|
||
|
|
||
|
|
||
|
def polynomial_reduce(p, DE):
|
||
|
"""
|
||
|
Polynomial Reduction.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Given a derivation D on k(t) and p in k[t] where t is a nonlinear
|
||
|
monomial over k, return q, r in k[t] such that p = Dq + r, and
|
||
|
deg(r) < deg_t(Dt).
|
||
|
"""
|
||
|
q = Poly(0, DE.t)
|
||
|
while p.degree(DE.t) >= DE.d.degree(DE.t):
|
||
|
m = p.degree(DE.t) - DE.d.degree(DE.t) + 1
|
||
|
q0 = Poly(DE.t**m, DE.t).mul(Poly(p.as_poly(DE.t).LC()/
|
||
|
(m*DE.d.LC()), DE.t))
|
||
|
q += q0
|
||
|
p = p - derivation(q0, DE)
|
||
|
|
||
|
return (q, p)
|
||
|
|
||
|
|
||
|
def laurent_series(a, d, F, n, DE):
|
||
|
"""
|
||
|
Contribution of ``F`` to the full partial fraction decomposition of A/D.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Given a field K of characteristic 0 and ``A``,``D``,``F`` in K[x] with D monic,
|
||
|
nonzero, coprime with A, and ``F`` the factor of multiplicity n in the square-
|
||
|
free factorization of D, return the principal parts of the Laurent series of
|
||
|
A/D at all the zeros of ``F``.
|
||
|
"""
|
||
|
if F.degree()==0:
|
||
|
return 0
|
||
|
Z = _symbols('z', n)
|
||
|
z = Symbol('z')
|
||
|
Z.insert(0, z)
|
||
|
delta_a = Poly(0, DE.t)
|
||
|
delta_d = Poly(1, DE.t)
|
||
|
|
||
|
E = d.quo(F**n)
|
||
|
ha, hd = (a, E*Poly(z**n, DE.t))
|
||
|
dF = derivation(F,DE)
|
||
|
B, _ = gcdex_diophantine(E, F, Poly(1,DE.t))
|
||
|
C, _ = gcdex_diophantine(dF, F, Poly(1,DE.t))
|
||
|
|
||
|
# initialization
|
||
|
F_store = F
|
||
|
V, DE_D_list, H_list= [], [], []
|
||
|
|
||
|
for j in range(0, n):
|
||
|
# jth derivative of z would be substituted with dfnth/(j+1) where dfnth =(d^n)f/(dx)^n
|
||
|
F_store = derivation(F_store, DE)
|
||
|
v = (F_store.as_expr())/(j + 1)
|
||
|
V.append(v)
|
||
|
DE_D_list.append(Poly(Z[j + 1],Z[j]))
|
||
|
|
||
|
DE_new = DifferentialExtension(extension = {'D': DE_D_list}) #a differential indeterminate
|
||
|
for j in range(0, n):
|
||
|
zEha = Poly(z**(n + j), DE.t)*E**(j + 1)*ha
|
||
|
zEhd = hd
|
||
|
Pa, Pd = cancel((zEha, zEhd))[1], cancel((zEha, zEhd))[2]
|
||
|
Q = Pa.quo(Pd)
|
||
|
for i in range(0, j + 1):
|
||
|
Q = Q.subs(Z[i], V[i])
|
||
|
Dha = (hd*derivation(ha, DE, basic=True).as_poly(DE.t)
|
||
|
+ ha*derivation(hd, DE, basic=True).as_poly(DE.t)
|
||
|
+ hd*derivation(ha, DE_new, basic=True).as_poly(DE.t)
|
||
|
+ ha*derivation(hd, DE_new, basic=True).as_poly(DE.t))
|
||
|
Dhd = Poly(j + 1, DE.t)*hd**2
|
||
|
ha, hd = Dha, Dhd
|
||
|
|
||
|
Ff, _ = F.div(gcd(F, Q))
|
||
|
F_stara, F_stard = frac_in(Ff, DE.t)
|
||
|
if F_stara.degree(DE.t) - F_stard.degree(DE.t) > 0:
|
||
|
QBC = Poly(Q, DE.t)*B**(1 + j)*C**(n + j)
|
||
|
H = QBC
|
||
|
H_list.append(H)
|
||
|
H = (QBC*F_stard).rem(F_stara)
|
||
|
alphas = real_roots(F_stara)
|
||
|
for alpha in list(alphas):
|
||
|
delta_a = delta_a*Poly((DE.t - alpha)**(n - j), DE.t) + Poly(H.eval(alpha), DE.t)
|
||
|
delta_d = delta_d*Poly((DE.t - alpha)**(n - j), DE.t)
|
||
|
return (delta_a, delta_d, H_list)
|
||
|
|
||
|
|
||
|
def recognize_derivative(a, d, DE, z=None):
|
||
|
"""
|
||
|
Compute the squarefree factorization of the denominator of f
|
||
|
and for each Di the polynomial H in K[x] (see Theorem 2.7.1), using the
|
||
|
LaurentSeries algorithm. Write Di = GiEi where Gj = gcd(Hn, Di) and
|
||
|
gcd(Ei,Hn) = 1. Since the residues of f at the roots of Gj are all 0, and
|
||
|
the residue of f at a root alpha of Ei is Hi(a) != 0, f is the derivative of a
|
||
|
rational function if and only if Ei = 1 for each i, which is equivalent to
|
||
|
Di | H[-1] for each i.
|
||
|
"""
|
||
|
flag =True
|
||
|
a, d = a.cancel(d, include=True)
|
||
|
_, r = a.div(d)
|
||
|
Np, Sp = splitfactor_sqf(d, DE, coefficientD=True, z=z)
|
||
|
|
||
|
j = 1
|
||
|
for s, _ in Sp:
|
||
|
delta_a, delta_d, H = laurent_series(r, d, s, j, DE)
|
||
|
g = gcd(d, H[-1]).as_poly()
|
||
|
if g is not d:
|
||
|
flag = False
|
||
|
break
|
||
|
j = j + 1
|
||
|
return flag
|
||
|
|
||
|
|
||
|
def recognize_log_derivative(a, d, DE, z=None):
|
||
|
"""
|
||
|
There exists a v in K(x)* such that f = dv/v
|
||
|
where f a rational function if and only if f can be written as f = A/D
|
||
|
where D is squarefree,deg(A) < deg(D), gcd(A, D) = 1,
|
||
|
and all the roots of the Rothstein-Trager resultant are integers. In that case,
|
||
|
any of the Rothstein-Trager, Lazard-Rioboo-Trager or Czichowski algorithm
|
||
|
produces u in K(x) such that du/dx = uf.
|
||
|
"""
|
||
|
|
||
|
z = z or Dummy('z')
|
||
|
a, d = a.cancel(d, include=True)
|
||
|
_, a = a.div(d)
|
||
|
|
||
|
pz = Poly(z, DE.t)
|
||
|
Dd = derivation(d, DE)
|
||
|
q = a - pz*Dd
|
||
|
r, _ = d.resultant(q, includePRS=True)
|
||
|
r = Poly(r, z)
|
||
|
Np, Sp = splitfactor_sqf(r, DE, coefficientD=True, z=z)
|
||
|
|
||
|
for s, _ in Sp:
|
||
|
# TODO also consider the complex roots which should
|
||
|
# turn the flag false
|
||
|
a = real_roots(s.as_poly(z))
|
||
|
|
||
|
if not all(j.is_Integer for j in a):
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
def residue_reduce(a, d, DE, z=None, invert=True):
|
||
|
"""
|
||
|
Lazard-Rioboo-Rothstein-Trager resultant reduction.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Given a derivation ``D`` on k(t) and f in k(t) simple, return g
|
||
|
elementary over k(t) and a Boolean b in {True, False} such that f -
|
||
|
Dg in k[t] if b == True or f + h and f + h - Dg do not have an
|
||
|
elementary integral over k(t) for any h in k<t> (reduced) if b ==
|
||
|
False.
|
||
|
|
||
|
Returns (G, b), where G is a tuple of tuples of the form (s_i, S_i),
|
||
|
such that g = Add(*[RootSum(s_i, lambda z: z*log(S_i(z, t))) for
|
||
|
S_i, s_i in G]). f - Dg is the remaining integral, which is elementary
|
||
|
only if b == True, and hence the integral of f is elementary only if
|
||
|
b == True.
|
||
|
|
||
|
f - Dg is not calculated in this function because that would require
|
||
|
explicitly calculating the RootSum. Use residue_reduce_derivation().
|
||
|
"""
|
||
|
# TODO: Use log_to_atan() from rationaltools.py
|
||
|
# If r = residue_reduce(...), then the logarithmic part is given by:
|
||
|
# sum([RootSum(a[0].as_poly(z), lambda i: i*log(a[1].as_expr()).subs(z,
|
||
|
# i)).subs(t, log(x)) for a in r[0]])
|
||
|
|
||
|
z = z or Dummy('z')
|
||
|
a, d = a.cancel(d, include=True)
|
||
|
a, d = a.to_field().mul_ground(1/d.LC()), d.to_field().mul_ground(1/d.LC())
|
||
|
kkinv = [1/x for x in DE.T[:DE.level]] + DE.T[:DE.level]
|
||
|
|
||
|
if a.is_zero:
|
||
|
return ([], True)
|
||
|
_, a = a.div(d)
|
||
|
|
||
|
pz = Poly(z, DE.t)
|
||
|
|
||
|
Dd = derivation(d, DE)
|
||
|
q = a - pz*Dd
|
||
|
|
||
|
if Dd.degree(DE.t) <= d.degree(DE.t):
|
||
|
r, R = d.resultant(q, includePRS=True)
|
||
|
else:
|
||
|
r, R = q.resultant(d, includePRS=True)
|
||
|
|
||
|
R_map, H = {}, []
|
||
|
for i in R:
|
||
|
R_map[i.degree()] = i
|
||
|
|
||
|
r = Poly(r, z)
|
||
|
Np, Sp = splitfactor_sqf(r, DE, coefficientD=True, z=z)
|
||
|
|
||
|
for s, i in Sp:
|
||
|
if i == d.degree(DE.t):
|
||
|
s = Poly(s, z).monic()
|
||
|
H.append((s, d))
|
||
|
else:
|
||
|
h = R_map.get(i)
|
||
|
if h is None:
|
||
|
continue
|
||
|
h_lc = Poly(h.as_poly(DE.t).LC(), DE.t, field=True)
|
||
|
|
||
|
h_lc_sqf = h_lc.sqf_list_include(all=True)
|
||
|
|
||
|
for a, j in h_lc_sqf:
|
||
|
h = Poly(h, DE.t, field=True).exquo(Poly(gcd(a, s**j, *kkinv),
|
||
|
DE.t))
|
||
|
|
||
|
s = Poly(s, z).monic()
|
||
|
|
||
|
if invert:
|
||
|
h_lc = Poly(h.as_poly(DE.t).LC(), DE.t, field=True, expand=False)
|
||
|
inv, coeffs = h_lc.as_poly(z, field=True).invert(s), [S.One]
|
||
|
|
||
|
for coeff in h.coeffs()[1:]:
|
||
|
L = reduced(inv*coeff.as_poly(inv.gens), [s])[1]
|
||
|
coeffs.append(L.as_expr())
|
||
|
|
||
|
h = Poly(dict(list(zip(h.monoms(), coeffs))), DE.t)
|
||
|
|
||
|
H.append((s, h))
|
||
|
|
||
|
b = not any(cancel(i.as_expr()).has(DE.t, z) for i, _ in Np)
|
||
|
|
||
|
return (H, b)
|
||
|
|
||
|
|
||
|
def residue_reduce_to_basic(H, DE, z):
|
||
|
"""
|
||
|
Converts the tuple returned by residue_reduce() into a Basic expression.
|
||
|
"""
|
||
|
# TODO: check what Lambda does with RootOf
|
||
|
i = Dummy('i')
|
||
|
s = list(zip(reversed(DE.T), reversed([f(DE.x) for f in DE.Tfuncs])))
|
||
|
|
||
|
return sum(RootSum(a[0].as_poly(z), Lambda(i, i*log(a[1].as_expr()).subs(
|
||
|
{z: i}).subs(s))) for a in H)
|
||
|
|
||
|
|
||
|
def residue_reduce_derivation(H, DE, z):
|
||
|
"""
|
||
|
Computes the derivation of an expression returned by residue_reduce().
|
||
|
|
||
|
In general, this is a rational function in t, so this returns an
|
||
|
as_expr() result.
|
||
|
"""
|
||
|
# TODO: verify that this is correct for multiple extensions
|
||
|
i = Dummy('i')
|
||
|
return S(sum(RootSum(a[0].as_poly(z), Lambda(i, i*derivation(a[1],
|
||
|
DE).as_expr().subs(z, i)/a[1].as_expr().subs(z, i))) for a in H))
|
||
|
|
||
|
|
||
|
def integrate_primitive_polynomial(p, DE):
|
||
|
"""
|
||
|
Integration of primitive polynomials.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Given a primitive monomial t over k, and ``p`` in k[t], return q in k[t],
|
||
|
r in k, and a bool b in {True, False} such that r = p - Dq is in k if b is
|
||
|
True, or r = p - Dq does not have an elementary integral over k(t) if b is
|
||
|
False.
|
||
|
"""
|
||
|
Zero = Poly(0, DE.t)
|
||
|
q = Poly(0, DE.t)
|
||
|
|
||
|
if not p.expr.has(DE.t):
|
||
|
return (Zero, p, True)
|
||
|
|
||
|
from .prde import limited_integrate
|
||
|
while True:
|
||
|
if not p.expr.has(DE.t):
|
||
|
return (q, p, True)
|
||
|
|
||
|
Dta, Dtb = frac_in(DE.d, DE.T[DE.level - 1])
|
||
|
|
||
|
with DecrementLevel(DE): # We had better be integrating the lowest extension (x)
|
||
|
# with ratint().
|
||
|
a = p.LC()
|
||
|
aa, ad = frac_in(a, DE.t)
|
||
|
|
||
|
try:
|
||
|
rv = limited_integrate(aa, ad, [(Dta, Dtb)], DE)
|
||
|
if rv is None:
|
||
|
raise NonElementaryIntegralException
|
||
|
(ba, bd), c = rv
|
||
|
except NonElementaryIntegralException:
|
||
|
return (q, p, False)
|
||
|
|
||
|
m = p.degree(DE.t)
|
||
|
q0 = c[0].as_poly(DE.t)*Poly(DE.t**(m + 1)/(m + 1), DE.t) + \
|
||
|
(ba.as_expr()/bd.as_expr()).as_poly(DE.t)*Poly(DE.t**m, DE.t)
|
||
|
|
||
|
p = p - derivation(q0, DE)
|
||
|
q = q + q0
|
||
|
|
||
|
|
||
|
def integrate_primitive(a, d, DE, z=None):
|
||
|
"""
|
||
|
Integration of primitive functions.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Given a primitive monomial t over k and f in k(t), return g elementary over
|
||
|
k(t), i in k(t), and b in {True, False} such that i = f - Dg is in k if b
|
||
|
is True or i = f - Dg does not have an elementary integral over k(t) if b
|
||
|
is False.
|
||
|
|
||
|
This function returns a Basic expression for the first argument. If b is
|
||
|
True, the second argument is Basic expression in k to recursively integrate.
|
||
|
If b is False, the second argument is an unevaluated Integral, which has
|
||
|
been proven to be nonelementary.
|
||
|
"""
|
||
|
# XXX: a and d must be canceled, or this might return incorrect results
|
||
|
z = z or Dummy("z")
|
||
|
s = list(zip(reversed(DE.T), reversed([f(DE.x) for f in DE.Tfuncs])))
|
||
|
|
||
|
g1, h, r = hermite_reduce(a, d, DE)
|
||
|
g2, b = residue_reduce(h[0], h[1], DE, z=z)
|
||
|
if not b:
|
||
|
i = cancel(a.as_expr()/d.as_expr() - (g1[1]*derivation(g1[0], DE) -
|
||
|
g1[0]*derivation(g1[1], DE)).as_expr()/(g1[1]**2).as_expr() -
|
||
|
residue_reduce_derivation(g2, DE, z))
|
||
|
i = NonElementaryIntegral(cancel(i).subs(s), DE.x)
|
||
|
return ((g1[0].as_expr()/g1[1].as_expr()).subs(s) +
|
||
|
residue_reduce_to_basic(g2, DE, z), i, b)
|
||
|
|
||
|
# h - Dg2 + r
|
||
|
p = cancel(h[0].as_expr()/h[1].as_expr() - residue_reduce_derivation(g2,
|
||
|
DE, z) + r[0].as_expr()/r[1].as_expr())
|
||
|
p = p.as_poly(DE.t)
|
||
|
|
||
|
q, i, b = integrate_primitive_polynomial(p, DE)
|
||
|
|
||
|
ret = ((g1[0].as_expr()/g1[1].as_expr() + q.as_expr()).subs(s) +
|
||
|
residue_reduce_to_basic(g2, DE, z))
|
||
|
if not b:
|
||
|
# TODO: This does not do the right thing when b is False
|
||
|
i = NonElementaryIntegral(cancel(i.as_expr()).subs(s), DE.x)
|
||
|
else:
|
||
|
i = cancel(i.as_expr())
|
||
|
|
||
|
return (ret, i, b)
|
||
|
|
||
|
|
||
|
def integrate_hyperexponential_polynomial(p, DE, z):
|
||
|
"""
|
||
|
Integration of hyperexponential polynomials.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Given a hyperexponential monomial t over k and ``p`` in k[t, 1/t], return q in
|
||
|
k[t, 1/t] and a bool b in {True, False} such that p - Dq in k if b is True,
|
||
|
or p - Dq does not have an elementary integral over k(t) if b is False.
|
||
|
"""
|
||
|
t1 = DE.t
|
||
|
dtt = DE.d.exquo(Poly(DE.t, DE.t))
|
||
|
qa = Poly(0, DE.t)
|
||
|
qd = Poly(1, DE.t)
|
||
|
b = True
|
||
|
|
||
|
if p.is_zero:
|
||
|
return(qa, qd, b)
|
||
|
|
||
|
from sympy.integrals.rde import rischDE
|
||
|
|
||
|
with DecrementLevel(DE):
|
||
|
for i in range(-p.degree(z), p.degree(t1) + 1):
|
||
|
if not i:
|
||
|
continue
|
||
|
elif i < 0:
|
||
|
# If you get AttributeError: 'NoneType' object has no attribute 'nth'
|
||
|
# then this should really not have expand=False
|
||
|
# But it shouldn't happen because p is already a Poly in t and z
|
||
|
a = p.as_poly(z, expand=False).nth(-i)
|
||
|
else:
|
||
|
# If you get AttributeError: 'NoneType' object has no attribute 'nth'
|
||
|
# then this should really not have expand=False
|
||
|
a = p.as_poly(t1, expand=False).nth(i)
|
||
|
|
||
|
aa, ad = frac_in(a, DE.t, field=True)
|
||
|
aa, ad = aa.cancel(ad, include=True)
|
||
|
iDt = Poly(i, t1)*dtt
|
||
|
iDta, iDtd = frac_in(iDt, DE.t, field=True)
|
||
|
try:
|
||
|
va, vd = rischDE(iDta, iDtd, Poly(aa, DE.t), Poly(ad, DE.t), DE)
|
||
|
va, vd = frac_in((va, vd), t1, cancel=True)
|
||
|
except NonElementaryIntegralException:
|
||
|
b = False
|
||
|
else:
|
||
|
qa = qa*vd + va*Poly(t1**i)*qd
|
||
|
qd *= vd
|
||
|
|
||
|
return (qa, qd, b)
|
||
|
|
||
|
|
||
|
def integrate_hyperexponential(a, d, DE, z=None, conds='piecewise'):
|
||
|
"""
|
||
|
Integration of hyperexponential functions.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Given a hyperexponential monomial t over k and f in k(t), return g
|
||
|
elementary over k(t), i in k(t), and a bool b in {True, False} such that
|
||
|
i = f - Dg is in k if b is True or i = f - Dg does not have an elementary
|
||
|
integral over k(t) if b is False.
|
||
|
|
||
|
This function returns a Basic expression for the first argument. If b is
|
||
|
True, the second argument is Basic expression in k to recursively integrate.
|
||
|
If b is False, the second argument is an unevaluated Integral, which has
|
||
|
been proven to be nonelementary.
|
||
|
"""
|
||
|
# XXX: a and d must be canceled, or this might return incorrect results
|
||
|
z = z or Dummy("z")
|
||
|
s = list(zip(reversed(DE.T), reversed([f(DE.x) for f in DE.Tfuncs])))
|
||
|
|
||
|
g1, h, r = hermite_reduce(a, d, DE)
|
||
|
g2, b = residue_reduce(h[0], h[1], DE, z=z)
|
||
|
if not b:
|
||
|
i = cancel(a.as_expr()/d.as_expr() - (g1[1]*derivation(g1[0], DE) -
|
||
|
g1[0]*derivation(g1[1], DE)).as_expr()/(g1[1]**2).as_expr() -
|
||
|
residue_reduce_derivation(g2, DE, z))
|
||
|
i = NonElementaryIntegral(cancel(i.subs(s)), DE.x)
|
||
|
return ((g1[0].as_expr()/g1[1].as_expr()).subs(s) +
|
||
|
residue_reduce_to_basic(g2, DE, z), i, b)
|
||
|
|
||
|
# p should be a polynomial in t and 1/t, because Sirr == k[t, 1/t]
|
||
|
# h - Dg2 + r
|
||
|
p = cancel(h[0].as_expr()/h[1].as_expr() - residue_reduce_derivation(g2,
|
||
|
DE, z) + r[0].as_expr()/r[1].as_expr())
|
||
|
pp = as_poly_1t(p, DE.t, z)
|
||
|
|
||
|
qa, qd, b = integrate_hyperexponential_polynomial(pp, DE, z)
|
||
|
|
||
|
i = pp.nth(0, 0)
|
||
|
|
||
|
ret = ((g1[0].as_expr()/g1[1].as_expr()).subs(s) \
|
||
|
+ residue_reduce_to_basic(g2, DE, z))
|
||
|
|
||
|
qas = qa.as_expr().subs(s)
|
||
|
qds = qd.as_expr().subs(s)
|
||
|
if conds == 'piecewise' and DE.x not in qds.free_symbols:
|
||
|
# We have to be careful if the exponent is S.Zero!
|
||
|
|
||
|
# XXX: Does qd = 0 always necessarily correspond to the exponential
|
||
|
# equaling 1?
|
||
|
ret += Piecewise(
|
||
|
(qas/qds, Ne(qds, 0)),
|
||
|
(integrate((p - i).subs(DE.t, 1).subs(s), DE.x), True)
|
||
|
)
|
||
|
else:
|
||
|
ret += qas/qds
|
||
|
|
||
|
if not b:
|
||
|
i = p - (qd*derivation(qa, DE) - qa*derivation(qd, DE)).as_expr()/\
|
||
|
(qd**2).as_expr()
|
||
|
i = NonElementaryIntegral(cancel(i).subs(s), DE.x)
|
||
|
return (ret, i, b)
|
||
|
|
||
|
|
||
|
def integrate_hypertangent_polynomial(p, DE):
|
||
|
"""
|
||
|
Integration of hypertangent polynomials.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Given a differential field k such that sqrt(-1) is not in k, a
|
||
|
hypertangent monomial t over k, and p in k[t], return q in k[t] and
|
||
|
c in k such that p - Dq - c*D(t**2 + 1)/(t**1 + 1) is in k and p -
|
||
|
Dq does not have an elementary integral over k(t) if Dc != 0.
|
||
|
"""
|
||
|
# XXX: Make sure that sqrt(-1) is not in k.
|
||
|
q, r = polynomial_reduce(p, DE)
|
||
|
a = DE.d.exquo(Poly(DE.t**2 + 1, DE.t))
|
||
|
c = Poly(r.nth(1)/(2*a.as_expr()), DE.t)
|
||
|
return (q, c)
|
||
|
|
||
|
|
||
|
def integrate_nonlinear_no_specials(a, d, DE, z=None):
|
||
|
"""
|
||
|
Integration of nonlinear monomials with no specials.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Given a nonlinear monomial t over k such that Sirr ({p in k[t] | p is
|
||
|
special, monic, and irreducible}) is empty, and f in k(t), returns g
|
||
|
elementary over k(t) and a Boolean b in {True, False} such that f - Dg is
|
||
|
in k if b == True, or f - Dg does not have an elementary integral over k(t)
|
||
|
if b == False.
|
||
|
|
||
|
This function is applicable to all nonlinear extensions, but in the case
|
||
|
where it returns b == False, it will only have proven that the integral of
|
||
|
f - Dg is nonelementary if Sirr is empty.
|
||
|
|
||
|
This function returns a Basic expression.
|
||
|
"""
|
||
|
# TODO: Integral from k?
|
||
|
# TODO: split out nonelementary integral
|
||
|
# XXX: a and d must be canceled, or this might not return correct results
|
||
|
z = z or Dummy("z")
|
||
|
s = list(zip(reversed(DE.T), reversed([f(DE.x) for f in DE.Tfuncs])))
|
||
|
|
||
|
g1, h, r = hermite_reduce(a, d, DE)
|
||
|
g2, b = residue_reduce(h[0], h[1], DE, z=z)
|
||
|
if not b:
|
||
|
return ((g1[0].as_expr()/g1[1].as_expr()).subs(s) +
|
||
|
residue_reduce_to_basic(g2, DE, z), b)
|
||
|
|
||
|
# Because f has no specials, this should be a polynomial in t, or else
|
||
|
# there is a bug.
|
||
|
p = cancel(h[0].as_expr()/h[1].as_expr() - residue_reduce_derivation(g2,
|
||
|
DE, z).as_expr() + r[0].as_expr()/r[1].as_expr()).as_poly(DE.t)
|
||
|
q1, q2 = polynomial_reduce(p, DE)
|
||
|
|
||
|
if q2.expr.has(DE.t):
|
||
|
b = False
|
||
|
else:
|
||
|
b = True
|
||
|
|
||
|
ret = (cancel(g1[0].as_expr()/g1[1].as_expr() + q1.as_expr()).subs(s) +
|
||
|
residue_reduce_to_basic(g2, DE, z))
|
||
|
return (ret, b)
|
||
|
|
||
|
|
||
|
class NonElementaryIntegral(Integral):
|
||
|
"""
|
||
|
Represents a nonelementary Integral.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
If the result of integrate() is an instance of this class, it is
|
||
|
guaranteed to be nonelementary. Note that integrate() by default will try
|
||
|
to find any closed-form solution, even in terms of special functions which
|
||
|
may themselves not be elementary. To make integrate() only give
|
||
|
elementary solutions, or, in the cases where it can prove the integral to
|
||
|
be nonelementary, instances of this class, use integrate(risch=True).
|
||
|
In this case, integrate() may raise NotImplementedError if it cannot make
|
||
|
such a determination.
|
||
|
|
||
|
integrate() uses the deterministic Risch algorithm to integrate elementary
|
||
|
functions or prove that they have no elementary integral. In some cases,
|
||
|
this algorithm can split an integral into an elementary and nonelementary
|
||
|
part, so that the result of integrate will be the sum of an elementary
|
||
|
expression and a NonElementaryIntegral.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy import integrate, exp, log, Integral
|
||
|
>>> from sympy.abc import x
|
||
|
|
||
|
>>> a = integrate(exp(-x**2), x, risch=True)
|
||
|
>>> print(a)
|
||
|
Integral(exp(-x**2), x)
|
||
|
>>> type(a)
|
||
|
<class 'sympy.integrals.risch.NonElementaryIntegral'>
|
||
|
|
||
|
>>> expr = (2*log(x)**2 - log(x) - x**2)/(log(x)**3 - x**2*log(x))
|
||
|
>>> b = integrate(expr, x, risch=True)
|
||
|
>>> print(b)
|
||
|
-log(-x + log(x))/2 + log(x + log(x))/2 + Integral(1/log(x), x)
|
||
|
>>> type(b.atoms(Integral).pop())
|
||
|
<class 'sympy.integrals.risch.NonElementaryIntegral'>
|
||
|
|
||
|
"""
|
||
|
# TODO: This is useful in and of itself, because isinstance(result,
|
||
|
# NonElementaryIntegral) will tell if the integral has been proven to be
|
||
|
# elementary. But should we do more? Perhaps a no-op .doit() if
|
||
|
# elementary=True? Or maybe some information on why the integral is
|
||
|
# nonelementary.
|
||
|
pass
|
||
|
|
||
|
|
||
|
def risch_integrate(f, x, extension=None, handle_first='log',
|
||
|
separate_integral=False, rewrite_complex=None,
|
||
|
conds='piecewise'):
|
||
|
r"""
|
||
|
The Risch Integration Algorithm.
|
||
|
|
||
|
Explanation
|
||
|
===========
|
||
|
|
||
|
Only transcendental functions are supported. Currently, only exponentials
|
||
|
and logarithms are supported, but support for trigonometric functions is
|
||
|
forthcoming.
|
||
|
|
||
|
If this function returns an unevaluated Integral in the result, it means
|
||
|
that it has proven that integral to be nonelementary. Any errors will
|
||
|
result in raising NotImplementedError. The unevaluated Integral will be
|
||
|
an instance of NonElementaryIntegral, a subclass of Integral.
|
||
|
|
||
|
handle_first may be either 'exp' or 'log'. This changes the order in
|
||
|
which the extension is built, and may result in a different (but
|
||
|
equivalent) solution (for an example of this, see issue 5109). It is also
|
||
|
possible that the integral may be computed with one but not the other,
|
||
|
because not all cases have been implemented yet. It defaults to 'log' so
|
||
|
that the outer extension is exponential when possible, because more of the
|
||
|
exponential case has been implemented.
|
||
|
|
||
|
If ``separate_integral`` is ``True``, the result is returned as a tuple (ans, i),
|
||
|
where the integral is ans + i, ans is elementary, and i is either a
|
||
|
NonElementaryIntegral or 0. This useful if you want to try further
|
||
|
integrating the NonElementaryIntegral part using other algorithms to
|
||
|
possibly get a solution in terms of special functions. It is False by
|
||
|
default.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.integrals.risch import risch_integrate
|
||
|
>>> from sympy import exp, log, pprint
|
||
|
>>> from sympy.abc import x
|
||
|
|
||
|
First, we try integrating exp(-x**2). Except for a constant factor of
|
||
|
2/sqrt(pi), this is the famous error function.
|
||
|
|
||
|
>>> pprint(risch_integrate(exp(-x**2), x))
|
||
|
/
|
||
|
|
|
||
|
| 2
|
||
|
| -x
|
||
|
| e dx
|
||
|
|
|
||
|
/
|
||
|
|
||
|
The unevaluated Integral in the result means that risch_integrate() has
|
||
|
proven that exp(-x**2) does not have an elementary anti-derivative.
|
||
|
|
||
|
In many cases, risch_integrate() can split out the elementary
|
||
|
anti-derivative part from the nonelementary anti-derivative part.
|
||
|
For example,
|
||
|
|
||
|
>>> pprint(risch_integrate((2*log(x)**2 - log(x) - x**2)/(log(x)**3 -
|
||
|
... x**2*log(x)), x))
|
||
|
/
|
||
|
|
|
||
|
log(-x + log(x)) log(x + log(x)) | 1
|
||
|
- ---------------- + --------------- + | ------ dx
|
||
|
2 2 | log(x)
|
||
|
|
|
||
|
/
|
||
|
|
||
|
This means that it has proven that the integral of 1/log(x) is
|
||
|
nonelementary. This function is also known as the logarithmic integral,
|
||
|
and is often denoted as Li(x).
|
||
|
|
||
|
risch_integrate() currently only accepts purely transcendental functions
|
||
|
with exponentials and logarithms, though note that this can include
|
||
|
nested exponentials and logarithms, as well as exponentials with bases
|
||
|
other than E.
|
||
|
|
||
|
>>> pprint(risch_integrate(exp(x)*exp(exp(x)), x))
|
||
|
/ x\
|
||
|
\e /
|
||
|
e
|
||
|
>>> pprint(risch_integrate(exp(exp(x)), x))
|
||
|
/
|
||
|
|
|
||
|
| / x\
|
||
|
| \e /
|
||
|
| e dx
|
||
|
|
|
||
|
/
|
||
|
|
||
|
>>> pprint(risch_integrate(x*x**x*log(x) + x**x + x*x**x, x))
|
||
|
x
|
||
|
x*x
|
||
|
>>> pprint(risch_integrate(x**x, x))
|
||
|
/
|
||
|
|
|
||
|
| x
|
||
|
| x dx
|
||
|
|
|
||
|
/
|
||
|
|
||
|
>>> pprint(risch_integrate(-1/(x*log(x)*log(log(x))**2), x))
|
||
|
1
|
||
|
-----------
|
||
|
log(log(x))
|
||
|
|
||
|
"""
|
||
|
f = S(f)
|
||
|
|
||
|
DE = extension or DifferentialExtension(f, x, handle_first=handle_first,
|
||
|
dummy=True, rewrite_complex=rewrite_complex)
|
||
|
fa, fd = DE.fa, DE.fd
|
||
|
|
||
|
result = S.Zero
|
||
|
for case in reversed(DE.cases):
|
||
|
if not fa.expr.has(DE.t) and not fd.expr.has(DE.t) and not case == 'base':
|
||
|
DE.decrement_level()
|
||
|
fa, fd = frac_in((fa, fd), DE.t)
|
||
|
continue
|
||
|
|
||
|
fa, fd = fa.cancel(fd, include=True)
|
||
|
if case == 'exp':
|
||
|
ans, i, b = integrate_hyperexponential(fa, fd, DE, conds=conds)
|
||
|
elif case == 'primitive':
|
||
|
ans, i, b = integrate_primitive(fa, fd, DE)
|
||
|
elif case == 'base':
|
||
|
# XXX: We can't call ratint() directly here because it doesn't
|
||
|
# handle polynomials correctly.
|
||
|
ans = integrate(fa.as_expr()/fd.as_expr(), DE.x, risch=False)
|
||
|
b = False
|
||
|
i = S.Zero
|
||
|
else:
|
||
|
raise NotImplementedError("Only exponential and logarithmic "
|
||
|
"extensions are currently supported.")
|
||
|
|
||
|
result += ans
|
||
|
if b:
|
||
|
DE.decrement_level()
|
||
|
fa, fd = frac_in(i, DE.t)
|
||
|
else:
|
||
|
result = result.subs(DE.backsubs)
|
||
|
if not i.is_zero:
|
||
|
i = NonElementaryIntegral(i.function.subs(DE.backsubs),i.limits)
|
||
|
if not separate_integral:
|
||
|
result += i
|
||
|
return result
|
||
|
else:
|
||
|
|
||
|
if isinstance(i, NonElementaryIntegral):
|
||
|
return (result, i)
|
||
|
else:
|
||
|
return (result, 0)
|