Traktor/myenv/Lib/site-packages/sympy/simplify/trigsimp.py

1253 lines
46 KiB
Python
Raw Permalink Normal View History

2024-05-26 05:12:46 +02:00
from collections import defaultdict
from functools import reduce
from sympy.core import (sympify, Basic, S, Expr, factor_terms,
Mul, Add, bottom_up)
from sympy.core.cache import cacheit
from sympy.core.function import (count_ops, _mexpand, FunctionClass, expand,
expand_mul, _coeff_isneg, Derivative)
from sympy.core.numbers import I, Integer, igcd
from sympy.core.sorting import _nodes
from sympy.core.symbol import Dummy, symbols, Wild
from sympy.external.gmpy import SYMPY_INTS
from sympy.functions import sin, cos, exp, cosh, tanh, sinh, tan, cot, coth
from sympy.functions import atan2
from sympy.functions.elementary.hyperbolic import HyperbolicFunction
from sympy.functions.elementary.trigonometric import TrigonometricFunction
from sympy.polys import Poly, factor, cancel, parallel_poly_from_expr
from sympy.polys.domains import ZZ
from sympy.polys.polyerrors import PolificationFailed
from sympy.polys.polytools import groebner
from sympy.simplify.cse_main import cse
from sympy.strategies.core import identity
from sympy.strategies.tree import greedy
from sympy.utilities.iterables import iterable
from sympy.utilities.misc import debug
def trigsimp_groebner(expr, hints=[], quick=False, order="grlex",
polynomial=False):
"""
Simplify trigonometric expressions using a groebner basis algorithm.
Explanation
===========
This routine takes a fraction involving trigonometric or hyperbolic
expressions, and tries to simplify it. The primary metric is the
total degree. Some attempts are made to choose the simplest possible
expression of the minimal degree, but this is non-rigorous, and also
very slow (see the ``quick=True`` option).
If ``polynomial`` is set to True, instead of simplifying numerator and
denominator together, this function just brings numerator and denominator
into a canonical form. This is much faster, but has potentially worse
results. However, if the input is a polynomial, then the result is
guaranteed to be an equivalent polynomial of minimal degree.
The most important option is hints. Its entries can be any of the
following:
- a natural number
- a function
- an iterable of the form (func, var1, var2, ...)
- anything else, interpreted as a generator
A number is used to indicate that the search space should be increased.
A function is used to indicate that said function is likely to occur in a
simplified expression.
An iterable is used indicate that func(var1 + var2 + ...) is likely to
occur in a simplified .
An additional generator also indicates that it is likely to occur.
(See examples below).
This routine carries out various computationally intensive algorithms.
The option ``quick=True`` can be used to suppress one particularly slow
step (at the expense of potentially more complicated results, but never at
the expense of increased total degree).
Examples
========
>>> from sympy.abc import x, y
>>> from sympy import sin, tan, cos, sinh, cosh, tanh
>>> from sympy.simplify.trigsimp import trigsimp_groebner
Suppose you want to simplify ``sin(x)*cos(x)``. Naively, nothing happens:
>>> ex = sin(x)*cos(x)
>>> trigsimp_groebner(ex)
sin(x)*cos(x)
This is because ``trigsimp_groebner`` only looks for a simplification
involving just ``sin(x)`` and ``cos(x)``. You can tell it to also try
``2*x`` by passing ``hints=[2]``:
>>> trigsimp_groebner(ex, hints=[2])
sin(2*x)/2
>>> trigsimp_groebner(sin(x)**2 - cos(x)**2, hints=[2])
-cos(2*x)
Increasing the search space this way can quickly become expensive. A much
faster way is to give a specific expression that is likely to occur:
>>> trigsimp_groebner(ex, hints=[sin(2*x)])
sin(2*x)/2
Hyperbolic expressions are similarly supported:
>>> trigsimp_groebner(sinh(2*x)/sinh(x))
2*cosh(x)
Note how no hints had to be passed, since the expression already involved
``2*x``.
The tangent function is also supported. You can either pass ``tan`` in the
hints, to indicate that tan should be tried whenever cosine or sine are,
or you can pass a specific generator:
>>> trigsimp_groebner(sin(x)/cos(x), hints=[tan])
tan(x)
>>> trigsimp_groebner(sinh(x)/cosh(x), hints=[tanh(x)])
tanh(x)
Finally, you can use the iterable form to suggest that angle sum formulae
should be tried:
>>> ex = (tan(x) + tan(y))/(1 - tan(x)*tan(y))
>>> trigsimp_groebner(ex, hints=[(tan, x, y)])
tan(x + y)
"""
# TODO
# - preprocess by replacing everything by funcs we can handle
# - optionally use cot instead of tan
# - more intelligent hinting.
# For example, if the ideal is small, and we have sin(x), sin(y),
# add sin(x + y) automatically... ?
# - algebraic numbers ...
# - expressions of lowest degree are not distinguished properly
# e.g. 1 - sin(x)**2
# - we could try to order the generators intelligently, so as to influence
# which monomials appear in the quotient basis
# THEORY
# ------
# Ratsimpmodprime above can be used to "simplify" a rational function
# modulo a prime ideal. "Simplify" mainly means finding an equivalent
# expression of lower total degree.
#
# We intend to use this to simplify trigonometric functions. To do that,
# we need to decide (a) which ring to use, and (b) modulo which ideal to
# simplify. In practice, (a) means settling on a list of "generators"
# a, b, c, ..., such that the fraction we want to simplify is a rational
# function in a, b, c, ..., with coefficients in ZZ (integers).
# (2) means that we have to decide what relations to impose on the
# generators. There are two practical problems:
# (1) The ideal has to be *prime* (a technical term).
# (2) The relations have to be polynomials in the generators.
#
# We typically have two kinds of generators:
# - trigonometric expressions, like sin(x), cos(5*x), etc
# - "everything else", like gamma(x), pi, etc.
#
# Since this function is trigsimp, we will concentrate on what to do with
# trigonometric expressions. We can also simplify hyperbolic expressions,
# but the extensions should be clear.
#
# One crucial point is that all *other* generators really should behave
# like indeterminates. In particular if (say) "I" is one of them, then
# in fact I**2 + 1 = 0 and we may and will compute non-sensical
# expressions. However, we can work with a dummy and add the relation
# I**2 + 1 = 0 to our ideal, then substitute back in the end.
#
# Now regarding trigonometric generators. We split them into groups,
# according to the argument of the trigonometric functions. We want to
# organise this in such a way that most trigonometric identities apply in
# the same group. For example, given sin(x), cos(2*x) and cos(y), we would
# group as [sin(x), cos(2*x)] and [cos(y)].
#
# Our prime ideal will be built in three steps:
# (1) For each group, compute a "geometrically prime" ideal of relations.
# Geometrically prime means that it generates a prime ideal in
# CC[gens], not just ZZ[gens].
# (2) Take the union of all the generators of the ideals for all groups.
# By the geometric primality condition, this is still prime.
# (3) Add further inter-group relations which preserve primality.
#
# Step (1) works as follows. We will isolate common factors in the
# argument, so that all our generators are of the form sin(n*x), cos(n*x)
# or tan(n*x), with n an integer. Suppose first there are no tan terms.
# The ideal [sin(x)**2 + cos(x)**2 - 1] is geometrically prime, since
# X**2 + Y**2 - 1 is irreducible over CC.
# Now, if we have a generator sin(n*x), than we can, using trig identities,
# express sin(n*x) as a polynomial in sin(x) and cos(x). We can add this
# relation to the ideal, preserving geometric primality, since the quotient
# ring is unchanged.
# Thus we have treated all sin and cos terms.
# For tan(n*x), we add a relation tan(n*x)*cos(n*x) - sin(n*x) = 0.
# (This requires of course that we already have relations for cos(n*x) and
# sin(n*x).) It is not obvious, but it seems that this preserves geometric
# primality.
# XXX A real proof would be nice. HELP!
# Sketch that <S**2 + C**2 - 1, C*T - S> is a prime ideal of
# CC[S, C, T]:
# - it suffices to show that the projective closure in CP**3 is
# irreducible
# - using the half-angle substitutions, we can express sin(x), tan(x),
# cos(x) as rational functions in tan(x/2)
# - from this, we get a rational map from CP**1 to our curve
# - this is a morphism, hence the curve is prime
#
# Step (2) is trivial.
#
# Step (3) works by adding selected relations of the form
# sin(x + y) - sin(x)*cos(y) - sin(y)*cos(x), etc. Geometric primality is
# preserved by the same argument as before.
def parse_hints(hints):
"""Split hints into (n, funcs, iterables, gens)."""
n = 1
funcs, iterables, gens = [], [], []
for e in hints:
if isinstance(e, (SYMPY_INTS, Integer)):
n = e
elif isinstance(e, FunctionClass):
funcs.append(e)
elif iterable(e):
iterables.append((e[0], e[1:]))
# XXX sin(x+2y)?
# Note: we go through polys so e.g.
# sin(-x) -> -sin(x) -> sin(x)
gens.extend(parallel_poly_from_expr(
[e[0](x) for x in e[1:]] + [e[0](Add(*e[1:]))])[1].gens)
else:
gens.append(e)
return n, funcs, iterables, gens
def build_ideal(x, terms):
"""
Build generators for our ideal. ``Terms`` is an iterable with elements of
the form (fn, coeff), indicating that we have a generator fn(coeff*x).
If any of the terms is trigonometric, sin(x) and cos(x) are guaranteed
to appear in terms. Similarly for hyperbolic functions. For tan(n*x),
sin(n*x) and cos(n*x) are guaranteed.
"""
I = []
y = Dummy('y')
for fn, coeff in terms:
for c, s, t, rel in (
[cos, sin, tan, cos(x)**2 + sin(x)**2 - 1],
[cosh, sinh, tanh, cosh(x)**2 - sinh(x)**2 - 1]):
if coeff == 1 and fn in [c, s]:
I.append(rel)
elif fn == t:
I.append(t(coeff*x)*c(coeff*x) - s(coeff*x))
elif fn in [c, s]:
cn = fn(coeff*y).expand(trig=True).subs(y, x)
I.append(fn(coeff*x) - cn)
return list(set(I))
def analyse_gens(gens, hints):
"""
Analyse the generators ``gens``, using the hints ``hints``.
The meaning of ``hints`` is described in the main docstring.
Return a new list of generators, and also the ideal we should
work with.
"""
# First parse the hints
n, funcs, iterables, extragens = parse_hints(hints)
debug('n=%s funcs: %s iterables: %s extragens: %s',
(funcs, iterables, extragens))
# We just add the extragens to gens and analyse them as before
gens = list(gens)
gens.extend(extragens)
# remove duplicates
funcs = list(set(funcs))
iterables = list(set(iterables))
gens = list(set(gens))
# all the functions we can do anything with
allfuncs = {sin, cos, tan, sinh, cosh, tanh}
# sin(3*x) -> ((3, x), sin)
trigterms = [(g.args[0].as_coeff_mul(), g.func) for g in gens
if g.func in allfuncs]
# Our list of new generators - start with anything that we cannot
# work with (i.e. is not a trigonometric term)
freegens = [g for g in gens if g.func not in allfuncs]
newgens = []
trigdict = {}
for (coeff, var), fn in trigterms:
trigdict.setdefault(var, []).append((coeff, fn))
res = [] # the ideal
for key, val in trigdict.items():
# We have now assembeled a dictionary. Its keys are common
# arguments in trigonometric expressions, and values are lists of
# pairs (fn, coeff). x0, (fn, coeff) in trigdict means that we
# need to deal with fn(coeff*x0). We take the rational gcd of the
# coeffs, call it ``gcd``. We then use x = x0/gcd as "base symbol",
# all other arguments are integral multiples thereof.
# We will build an ideal which works with sin(x), cos(x).
# If hint tan is provided, also work with tan(x). Moreover, if
# n > 1, also work with sin(k*x) for k <= n, and similarly for cos
# (and tan if the hint is provided). Finally, any generators which
# the ideal does not work with but we need to accommodate (either
# because it was in expr or because it was provided as a hint)
# we also build into the ideal.
# This selection process is expressed in the list ``terms``.
# build_ideal then generates the actual relations in our ideal,
# from this list.
fns = [x[1] for x in val]
val = [x[0] for x in val]
gcd = reduce(igcd, val)
terms = [(fn, v/gcd) for (fn, v) in zip(fns, val)]
fs = set(funcs + fns)
for c, s, t in ([cos, sin, tan], [cosh, sinh, tanh]):
if any(x in fs for x in (c, s, t)):
fs.add(c)
fs.add(s)
for fn in fs:
for k in range(1, n + 1):
terms.append((fn, k))
extra = []
for fn, v in terms:
if fn == tan:
extra.append((sin, v))
extra.append((cos, v))
if fn in [sin, cos] and tan in fs:
extra.append((tan, v))
if fn == tanh:
extra.append((sinh, v))
extra.append((cosh, v))
if fn in [sinh, cosh] and tanh in fs:
extra.append((tanh, v))
terms.extend(extra)
x = gcd*Mul(*key)
r = build_ideal(x, terms)
res.extend(r)
newgens.extend({fn(v*x) for fn, v in terms})
# Add generators for compound expressions from iterables
for fn, args in iterables:
if fn == tan:
# Tan expressions are recovered from sin and cos.
iterables.extend([(sin, args), (cos, args)])
elif fn == tanh:
# Tanh expressions are recovered from sihn and cosh.
iterables.extend([(sinh, args), (cosh, args)])
else:
dummys = symbols('d:%i' % len(args), cls=Dummy)
expr = fn( Add(*dummys)).expand(trig=True).subs(list(zip(dummys, args)))
res.append(fn(Add(*args)) - expr)
if myI in gens:
res.append(myI**2 + 1)
freegens.remove(myI)
newgens.append(myI)
return res, freegens, newgens
myI = Dummy('I')
expr = expr.subs(S.ImaginaryUnit, myI)
subs = [(myI, S.ImaginaryUnit)]
num, denom = cancel(expr).as_numer_denom()
try:
(pnum, pdenom), opt = parallel_poly_from_expr([num, denom])
except PolificationFailed:
return expr
debug('initial gens:', opt.gens)
ideal, freegens, gens = analyse_gens(opt.gens, hints)
debug('ideal:', ideal)
debug('new gens:', gens, " -- len", len(gens))
debug('free gens:', freegens, " -- len", len(gens))
# NOTE we force the domain to be ZZ to stop polys from injecting generators
# (which is usually a sign of a bug in the way we build the ideal)
if not gens:
return expr
G = groebner(ideal, order=order, gens=gens, domain=ZZ)
debug('groebner basis:', list(G), " -- len", len(G))
# If our fraction is a polynomial in the free generators, simplify all
# coefficients separately:
from sympy.simplify.ratsimp import ratsimpmodprime
if freegens and pdenom.has_only_gens(*set(gens).intersection(pdenom.gens)):
num = Poly(num, gens=gens+freegens).eject(*gens)
res = []
for monom, coeff in num.terms():
ourgens = set(parallel_poly_from_expr([coeff, denom])[1].gens)
# We compute the transitive closure of all generators that can
# be reached from our generators through relations in the ideal.
changed = True
while changed:
changed = False
for p in ideal:
p = Poly(p)
if not ourgens.issuperset(p.gens) and \
not p.has_only_gens(*set(p.gens).difference(ourgens)):
changed = True
ourgens.update(p.exclude().gens)
# NOTE preserve order!
realgens = [x for x in gens if x in ourgens]
# The generators of the ideal have now been (implicitly) split
# into two groups: those involving ourgens and those that don't.
# Since we took the transitive closure above, these two groups
# live in subgrings generated by a *disjoint* set of variables.
# Any sensible groebner basis algorithm will preserve this disjoint
# structure (i.e. the elements of the groebner basis can be split
# similarly), and and the two subsets of the groebner basis then
# form groebner bases by themselves. (For the smaller generating
# sets, of course.)
ourG = [g.as_expr() for g in G.polys if
g.has_only_gens(*ourgens.intersection(g.gens))]
res.append(Mul(*[a**b for a, b in zip(freegens, monom)]) * \
ratsimpmodprime(coeff/denom, ourG, order=order,
gens=realgens, quick=quick, domain=ZZ,
polynomial=polynomial).subs(subs))
return Add(*res)
# NOTE The following is simpler and has less assumptions on the
# groebner basis algorithm. If the above turns out to be broken,
# use this.
return Add(*[Mul(*[a**b for a, b in zip(freegens, monom)]) * \
ratsimpmodprime(coeff/denom, list(G), order=order,
gens=gens, quick=quick, domain=ZZ)
for monom, coeff in num.terms()])
else:
return ratsimpmodprime(
expr, list(G), order=order, gens=freegens+gens,
quick=quick, domain=ZZ, polynomial=polynomial).subs(subs)
_trigs = (TrigonometricFunction, HyperbolicFunction)
def _trigsimp_inverse(rv):
def check_args(x, y):
try:
return x.args[0] == y.args[0]
except IndexError:
return False
def f(rv):
# for simple functions
g = getattr(rv, 'inverse', None)
if (g is not None and isinstance(rv.args[0], g()) and
isinstance(g()(1), TrigonometricFunction)):
return rv.args[0].args[0]
# for atan2 simplifications, harder because atan2 has 2 args
if isinstance(rv, atan2):
y, x = rv.args
if _coeff_isneg(y):
return -f(atan2(-y, x))
elif _coeff_isneg(x):
return S.Pi - f(atan2(y, -x))
if check_args(x, y):
if isinstance(y, sin) and isinstance(x, cos):
return x.args[0]
if isinstance(y, cos) and isinstance(x, sin):
return S.Pi / 2 - x.args[0]
return rv
return bottom_up(rv, f)
def trigsimp(expr, inverse=False, **opts):
"""Returns a reduced expression by using known trig identities.
Parameters
==========
inverse : bool, optional
If ``inverse=True``, it will be assumed that a composition of inverse
functions, such as sin and asin, can be cancelled in any order.
For example, ``asin(sin(x))`` will yield ``x`` without checking whether
x belongs to the set where this relation is true. The default is False.
Default : True
method : string, optional
Specifies the method to use. Valid choices are:
- ``'matching'``, default
- ``'groebner'``
- ``'combined'``
- ``'fu'``
- ``'old'``
If ``'matching'``, simplify the expression recursively by targeting
common patterns. If ``'groebner'``, apply an experimental groebner
basis algorithm. In this case further options are forwarded to
``trigsimp_groebner``, please refer to
its docstring. If ``'combined'``, it first runs the groebner basis
algorithm with small default parameters, then runs the ``'matching'``
algorithm. If ``'fu'``, run the collection of trigonometric
transformations described by Fu, et al. (see the
:py:func:`~sympy.simplify.fu.fu` docstring). If ``'old'``, the original
SymPy trig simplification function is run.
opts :
Optional keyword arguments passed to the method. See each method's
function docstring for details.
Examples
========
>>> from sympy import trigsimp, sin, cos, log
>>> from sympy.abc import x
>>> e = 2*sin(x)**2 + 2*cos(x)**2
>>> trigsimp(e)
2
Simplification occurs wherever trigonometric functions are located.
>>> trigsimp(log(e))
log(2)
Using ``method='groebner'`` (or ``method='combined'``) might lead to
greater simplification.
The old trigsimp routine can be accessed as with method ``method='old'``.
>>> from sympy import coth, tanh
>>> t = 3*tanh(x)**7 - 2/coth(x)**7
>>> trigsimp(t, method='old') == t
True
>>> trigsimp(t)
tanh(x)**7
"""
from sympy.simplify.fu import fu
expr = sympify(expr)
_eval_trigsimp = getattr(expr, '_eval_trigsimp', None)
if _eval_trigsimp is not None:
return _eval_trigsimp(**opts)
old = opts.pop('old', False)
if not old:
opts.pop('deep', None)
opts.pop('recursive', None)
method = opts.pop('method', 'matching')
else:
method = 'old'
def groebnersimp(ex, **opts):
def traverse(e):
if e.is_Atom:
return e
args = [traverse(x) for x in e.args]
if e.is_Function or e.is_Pow:
args = [trigsimp_groebner(x, **opts) for x in args]
return e.func(*args)
new = traverse(ex)
if not isinstance(new, Expr):
return new
return trigsimp_groebner(new, **opts)
trigsimpfunc = {
'fu': (lambda x: fu(x, **opts)),
'matching': (lambda x: futrig(x)),
'groebner': (lambda x: groebnersimp(x, **opts)),
'combined': (lambda x: futrig(groebnersimp(x,
polynomial=True, hints=[2, tan]))),
'old': lambda x: trigsimp_old(x, **opts),
}[method]
expr_simplified = trigsimpfunc(expr)
if inverse:
expr_simplified = _trigsimp_inverse(expr_simplified)
return expr_simplified
def exptrigsimp(expr):
"""
Simplifies exponential / trigonometric / hyperbolic functions.
Examples
========
>>> from sympy import exptrigsimp, exp, cosh, sinh
>>> from sympy.abc import z
>>> exptrigsimp(exp(z) + exp(-z))
2*cosh(z)
>>> exptrigsimp(cosh(z) - sinh(z))
exp(-z)
"""
from sympy.simplify.fu import hyper_as_trig, TR2i
def exp_trig(e):
# select the better of e, and e rewritten in terms of exp or trig
# functions
choices = [e]
if e.has(*_trigs):
choices.append(e.rewrite(exp))
choices.append(e.rewrite(cos))
return min(*choices, key=count_ops)
newexpr = bottom_up(expr, exp_trig)
def f(rv):
if not rv.is_Mul:
return rv
commutative_part, noncommutative_part = rv.args_cnc()
# Since as_powers_dict loses order information,
# if there is more than one noncommutative factor,
# it should only be used to simplify the commutative part.
if (len(noncommutative_part) > 1):
return f(Mul(*commutative_part))*Mul(*noncommutative_part)
rvd = rv.as_powers_dict()
newd = rvd.copy()
def signlog(expr, sign=S.One):
if expr is S.Exp1:
return sign, S.One
elif isinstance(expr, exp) or (expr.is_Pow and expr.base == S.Exp1):
return sign, expr.exp
elif sign is S.One:
return signlog(-expr, sign=-S.One)
else:
return None, None
ee = rvd[S.Exp1]
for k in rvd:
if k.is_Add and len(k.args) == 2:
# k == c*(1 + sign*E**x)
c = k.args[0]
sign, x = signlog(k.args[1]/c)
if not x:
continue
m = rvd[k]
newd[k] -= m
if ee == -x*m/2:
# sinh and cosh
newd[S.Exp1] -= ee
ee = 0
if sign == 1:
newd[2*c*cosh(x/2)] += m
else:
newd[-2*c*sinh(x/2)] += m
elif newd[1 - sign*S.Exp1**x] == -m:
# tanh
del newd[1 - sign*S.Exp1**x]
if sign == 1:
newd[-c/tanh(x/2)] += m
else:
newd[-c*tanh(x/2)] += m
else:
newd[1 + sign*S.Exp1**x] += m
newd[c] += m
return Mul(*[k**newd[k] for k in newd])
newexpr = bottom_up(newexpr, f)
# sin/cos and sinh/cosh ratios to tan and tanh, respectively
if newexpr.has(HyperbolicFunction):
e, f = hyper_as_trig(newexpr)
newexpr = f(TR2i(e))
if newexpr.has(TrigonometricFunction):
newexpr = TR2i(newexpr)
# can we ever generate an I where there was none previously?
if not (newexpr.has(I) and not expr.has(I)):
expr = newexpr
return expr
#-------------------- the old trigsimp routines ---------------------
def trigsimp_old(expr, *, first=True, **opts):
"""
Reduces expression by using known trig identities.
Notes
=====
deep:
- Apply trigsimp inside all objects with arguments
recursive:
- Use common subexpression elimination (cse()) and apply
trigsimp recursively (this is quite expensive if the
expression is large)
method:
- Determine the method to use. Valid choices are 'matching' (default),
'groebner', 'combined', 'fu' and 'futrig'. If 'matching', simplify the
expression recursively by pattern matching. If 'groebner', apply an
experimental groebner basis algorithm. In this case further options
are forwarded to ``trigsimp_groebner``, please refer to its docstring.
If 'combined', first run the groebner basis algorithm with small
default parameters, then run the 'matching' algorithm. 'fu' runs the
collection of trigonometric transformations described by Fu, et al.
(see the `fu` docstring) while `futrig` runs a subset of Fu-transforms
that mimic the behavior of `trigsimp`.
compare:
- show input and output from `trigsimp` and `futrig` when different,
but returns the `trigsimp` value.
Examples
========
>>> from sympy import trigsimp, sin, cos, log, cot
>>> from sympy.abc import x
>>> e = 2*sin(x)**2 + 2*cos(x)**2
>>> trigsimp(e, old=True)
2
>>> trigsimp(log(e), old=True)
log(2*sin(x)**2 + 2*cos(x)**2)
>>> trigsimp(log(e), deep=True, old=True)
log(2)
Using `method="groebner"` (or `"combined"`) can sometimes lead to a lot
more simplification:
>>> e = (-sin(x) + 1)/cos(x) + cos(x)/(-sin(x) + 1)
>>> trigsimp(e, old=True)
(1 - sin(x))/cos(x) + cos(x)/(1 - sin(x))
>>> trigsimp(e, method="groebner", old=True)
2/cos(x)
>>> trigsimp(1/cot(x)**2, compare=True, old=True)
futrig: tan(x)**2
cot(x)**(-2)
"""
old = expr
if first:
if not expr.has(*_trigs):
return expr
trigsyms = set().union(*[t.free_symbols for t in expr.atoms(*_trigs)])
if len(trigsyms) > 1:
from sympy.simplify.simplify import separatevars
d = separatevars(expr)
if d.is_Mul:
d = separatevars(d, dict=True) or d
if isinstance(d, dict):
expr = 1
for k, v in d.items():
# remove hollow factoring
was = v
v = expand_mul(v)
opts['first'] = False
vnew = trigsimp(v, **opts)
if vnew == v:
vnew = was
expr *= vnew
old = expr
else:
if d.is_Add:
for s in trigsyms:
r, e = expr.as_independent(s)
if r:
opts['first'] = False
expr = r + trigsimp(e, **opts)
if not expr.is_Add:
break
old = expr
recursive = opts.pop('recursive', False)
deep = opts.pop('deep', False)
method = opts.pop('method', 'matching')
def groebnersimp(ex, deep, **opts):
def traverse(e):
if e.is_Atom:
return e
args = [traverse(x) for x in e.args]
if e.is_Function or e.is_Pow:
args = [trigsimp_groebner(x, **opts) for x in args]
return e.func(*args)
if deep:
ex = traverse(ex)
return trigsimp_groebner(ex, **opts)
trigsimpfunc = {
'matching': (lambda x, d: _trigsimp(x, d)),
'groebner': (lambda x, d: groebnersimp(x, d, **opts)),
'combined': (lambda x, d: _trigsimp(groebnersimp(x,
d, polynomial=True, hints=[2, tan]),
d))
}[method]
if recursive:
w, g = cse(expr)
g = trigsimpfunc(g[0], deep)
for sub in reversed(w):
g = g.subs(sub[0], sub[1])
g = trigsimpfunc(g, deep)
result = g
else:
result = trigsimpfunc(expr, deep)
if opts.get('compare', False):
f = futrig(old)
if f != result:
print('\tfutrig:', f)
return result
def _dotrig(a, b):
"""Helper to tell whether ``a`` and ``b`` have the same sorts
of symbols in them -- no need to test hyperbolic patterns against
expressions that have no hyperbolics in them."""
return a.func == b.func and (
a.has(TrigonometricFunction) and b.has(TrigonometricFunction) or
a.has(HyperbolicFunction) and b.has(HyperbolicFunction))
_trigpat = None
def _trigpats():
global _trigpat
a, b, c = symbols('a b c', cls=Wild)
d = Wild('d', commutative=False)
# for the simplifications like sinh/cosh -> tanh:
# DO NOT REORDER THE FIRST 14 since these are assumed to be in this
# order in _match_div_rewrite.
matchers_division = (
(a*sin(b)**c/cos(b)**c, a*tan(b)**c, sin(b), cos(b)),
(a*tan(b)**c*cos(b)**c, a*sin(b)**c, sin(b), cos(b)),
(a*cot(b)**c*sin(b)**c, a*cos(b)**c, sin(b), cos(b)),
(a*tan(b)**c/sin(b)**c, a/cos(b)**c, sin(b), cos(b)),
(a*cot(b)**c/cos(b)**c, a/sin(b)**c, sin(b), cos(b)),
(a*cot(b)**c*tan(b)**c, a, sin(b), cos(b)),
(a*(cos(b) + 1)**c*(cos(b) - 1)**c,
a*(-sin(b)**2)**c, cos(b) + 1, cos(b) - 1),
(a*(sin(b) + 1)**c*(sin(b) - 1)**c,
a*(-cos(b)**2)**c, sin(b) + 1, sin(b) - 1),
(a*sinh(b)**c/cosh(b)**c, a*tanh(b)**c, S.One, S.One),
(a*tanh(b)**c*cosh(b)**c, a*sinh(b)**c, S.One, S.One),
(a*coth(b)**c*sinh(b)**c, a*cosh(b)**c, S.One, S.One),
(a*tanh(b)**c/sinh(b)**c, a/cosh(b)**c, S.One, S.One),
(a*coth(b)**c/cosh(b)**c, a/sinh(b)**c, S.One, S.One),
(a*coth(b)**c*tanh(b)**c, a, S.One, S.One),
(c*(tanh(a) + tanh(b))/(1 + tanh(a)*tanh(b)),
tanh(a + b)*c, S.One, S.One),
)
matchers_add = (
(c*sin(a)*cos(b) + c*cos(a)*sin(b) + d, sin(a + b)*c + d),
(c*cos(a)*cos(b) - c*sin(a)*sin(b) + d, cos(a + b)*c + d),
(c*sin(a)*cos(b) - c*cos(a)*sin(b) + d, sin(a - b)*c + d),
(c*cos(a)*cos(b) + c*sin(a)*sin(b) + d, cos(a - b)*c + d),
(c*sinh(a)*cosh(b) + c*sinh(b)*cosh(a) + d, sinh(a + b)*c + d),
(c*cosh(a)*cosh(b) + c*sinh(a)*sinh(b) + d, cosh(a + b)*c + d),
)
# for cos(x)**2 + sin(x)**2 -> 1
matchers_identity = (
(a*sin(b)**2, a - a*cos(b)**2),
(a*tan(b)**2, a*(1/cos(b))**2 - a),
(a*cot(b)**2, a*(1/sin(b))**2 - a),
(a*sin(b + c), a*(sin(b)*cos(c) + sin(c)*cos(b))),
(a*cos(b + c), a*(cos(b)*cos(c) - sin(b)*sin(c))),
(a*tan(b + c), a*((tan(b) + tan(c))/(1 - tan(b)*tan(c)))),
(a*sinh(b)**2, a*cosh(b)**2 - a),
(a*tanh(b)**2, a - a*(1/cosh(b))**2),
(a*coth(b)**2, a + a*(1/sinh(b))**2),
(a*sinh(b + c), a*(sinh(b)*cosh(c) + sinh(c)*cosh(b))),
(a*cosh(b + c), a*(cosh(b)*cosh(c) + sinh(b)*sinh(c))),
(a*tanh(b + c), a*((tanh(b) + tanh(c))/(1 + tanh(b)*tanh(c)))),
)
# Reduce any lingering artifacts, such as sin(x)**2 changing
# to 1-cos(x)**2 when sin(x)**2 was "simpler"
artifacts = (
(a - a*cos(b)**2 + c, a*sin(b)**2 + c, cos),
(a - a*(1/cos(b))**2 + c, -a*tan(b)**2 + c, cos),
(a - a*(1/sin(b))**2 + c, -a*cot(b)**2 + c, sin),
(a - a*cosh(b)**2 + c, -a*sinh(b)**2 + c, cosh),
(a - a*(1/cosh(b))**2 + c, a*tanh(b)**2 + c, cosh),
(a + a*(1/sinh(b))**2 + c, a*coth(b)**2 + c, sinh),
# same as above but with noncommutative prefactor
(a*d - a*d*cos(b)**2 + c, a*d*sin(b)**2 + c, cos),
(a*d - a*d*(1/cos(b))**2 + c, -a*d*tan(b)**2 + c, cos),
(a*d - a*d*(1/sin(b))**2 + c, -a*d*cot(b)**2 + c, sin),
(a*d - a*d*cosh(b)**2 + c, -a*d*sinh(b)**2 + c, cosh),
(a*d - a*d*(1/cosh(b))**2 + c, a*d*tanh(b)**2 + c, cosh),
(a*d + a*d*(1/sinh(b))**2 + c, a*d*coth(b)**2 + c, sinh),
)
_trigpat = (a, b, c, d, matchers_division, matchers_add,
matchers_identity, artifacts)
return _trigpat
def _replace_mul_fpowxgpow(expr, f, g, rexp, h, rexph):
"""Helper for _match_div_rewrite.
Replace f(b_)**c_*g(b_)**(rexp(c_)) with h(b)**rexph(c) if f(b_)
and g(b_) are both positive or if c_ is an integer.
"""
# assert expr.is_Mul and expr.is_commutative and f != g
fargs = defaultdict(int)
gargs = defaultdict(int)
args = []
for x in expr.args:
if x.is_Pow or x.func in (f, g):
b, e = x.as_base_exp()
if b.is_positive or e.is_integer:
if b.func == f:
fargs[b.args[0]] += e
continue
elif b.func == g:
gargs[b.args[0]] += e
continue
args.append(x)
common = set(fargs) & set(gargs)
hit = False
while common:
key = common.pop()
fe = fargs.pop(key)
ge = gargs.pop(key)
if fe == rexp(ge):
args.append(h(key)**rexph(fe))
hit = True
else:
fargs[key] = fe
gargs[key] = ge
if not hit:
return expr
while fargs:
key, e = fargs.popitem()
args.append(f(key)**e)
while gargs:
key, e = gargs.popitem()
args.append(g(key)**e)
return Mul(*args)
_idn = lambda x: x
_midn = lambda x: -x
_one = lambda x: S.One
def _match_div_rewrite(expr, i):
"""helper for __trigsimp"""
if i == 0:
expr = _replace_mul_fpowxgpow(expr, sin, cos,
_midn, tan, _idn)
elif i == 1:
expr = _replace_mul_fpowxgpow(expr, tan, cos,
_idn, sin, _idn)
elif i == 2:
expr = _replace_mul_fpowxgpow(expr, cot, sin,
_idn, cos, _idn)
elif i == 3:
expr = _replace_mul_fpowxgpow(expr, tan, sin,
_midn, cos, _midn)
elif i == 4:
expr = _replace_mul_fpowxgpow(expr, cot, cos,
_midn, sin, _midn)
elif i == 5:
expr = _replace_mul_fpowxgpow(expr, cot, tan,
_idn, _one, _idn)
# i in (6, 7) is skipped
elif i == 8:
expr = _replace_mul_fpowxgpow(expr, sinh, cosh,
_midn, tanh, _idn)
elif i == 9:
expr = _replace_mul_fpowxgpow(expr, tanh, cosh,
_idn, sinh, _idn)
elif i == 10:
expr = _replace_mul_fpowxgpow(expr, coth, sinh,
_idn, cosh, _idn)
elif i == 11:
expr = _replace_mul_fpowxgpow(expr, tanh, sinh,
_midn, cosh, _midn)
elif i == 12:
expr = _replace_mul_fpowxgpow(expr, coth, cosh,
_midn, sinh, _midn)
elif i == 13:
expr = _replace_mul_fpowxgpow(expr, coth, tanh,
_idn, _one, _idn)
else:
return None
return expr
def _trigsimp(expr, deep=False):
# protect the cache from non-trig patterns; we only allow
# trig patterns to enter the cache
if expr.has(*_trigs):
return __trigsimp(expr, deep)
return expr
@cacheit
def __trigsimp(expr, deep=False):
"""recursive helper for trigsimp"""
from sympy.simplify.fu import TR10i
if _trigpat is None:
_trigpats()
a, b, c, d, matchers_division, matchers_add, \
matchers_identity, artifacts = _trigpat
if expr.is_Mul:
# do some simplifications like sin/cos -> tan:
if not expr.is_commutative:
com, nc = expr.args_cnc()
expr = _trigsimp(Mul._from_args(com), deep)*Mul._from_args(nc)
else:
for i, (pattern, simp, ok1, ok2) in enumerate(matchers_division):
if not _dotrig(expr, pattern):
continue
newexpr = _match_div_rewrite(expr, i)
if newexpr is not None:
if newexpr != expr:
expr = newexpr
break
else:
continue
# use SymPy matching instead
res = expr.match(pattern)
if res and res.get(c, 0):
if not res[c].is_integer:
ok = ok1.subs(res)
if not ok.is_positive:
continue
ok = ok2.subs(res)
if not ok.is_positive:
continue
# if "a" contains any of trig or hyperbolic funcs with
# argument "b" then skip the simplification
if any(w.args[0] == res[b] for w in res[a].atoms(
TrigonometricFunction, HyperbolicFunction)):
continue
# simplify and finish:
expr = simp.subs(res)
break # process below
if expr.is_Add:
args = []
for term in expr.args:
if not term.is_commutative:
com, nc = term.args_cnc()
nc = Mul._from_args(nc)
term = Mul._from_args(com)
else:
nc = S.One
term = _trigsimp(term, deep)
for pattern, result in matchers_identity:
res = term.match(pattern)
if res is not None:
term = result.subs(res)
break
args.append(term*nc)
if args != expr.args:
expr = Add(*args)
expr = min(expr, expand(expr), key=count_ops)
if expr.is_Add:
for pattern, result in matchers_add:
if not _dotrig(expr, pattern):
continue
expr = TR10i(expr)
if expr.has(HyperbolicFunction):
res = expr.match(pattern)
# if "d" contains any trig or hyperbolic funcs with
# argument "a" or "b" then skip the simplification;
# this isn't perfect -- see tests
if res is None or not (a in res and b in res) or any(
w.args[0] in (res[a], res[b]) for w in res[d].atoms(
TrigonometricFunction, HyperbolicFunction)):
continue
expr = result.subs(res)
break
# Reduce any lingering artifacts, such as sin(x)**2 changing
# to 1 - cos(x)**2 when sin(x)**2 was "simpler"
for pattern, result, ex in artifacts:
if not _dotrig(expr, pattern):
continue
# Substitute a new wild that excludes some function(s)
# to help influence a better match. This is because
# sometimes, for example, 'a' would match sec(x)**2
a_t = Wild('a', exclude=[ex])
pattern = pattern.subs(a, a_t)
result = result.subs(a, a_t)
m = expr.match(pattern)
was = None
while m and was != expr:
was = expr
if m[a_t] == 0 or \
-m[a_t] in m[c].args or m[a_t] + m[c] == 0:
break
if d in m and m[a_t]*m[d] + m[c] == 0:
break
expr = result.subs(m)
m = expr.match(pattern)
m.setdefault(c, S.Zero)
elif expr.is_Mul or expr.is_Pow or deep and expr.args:
expr = expr.func(*[_trigsimp(a, deep) for a in expr.args])
try:
if not expr.has(*_trigs):
raise TypeError
e = expr.atoms(exp)
new = expr.rewrite(exp, deep=deep)
if new == e:
raise TypeError
fnew = factor(new)
if fnew != new:
new = sorted([new, factor(new)], key=count_ops)[0]
# if all exp that were introduced disappeared then accept it
if not (new.atoms(exp) - e):
expr = new
except TypeError:
pass
return expr
#------------------- end of old trigsimp routines --------------------
def futrig(e, *, hyper=True, **kwargs):
"""Return simplified ``e`` using Fu-like transformations.
This is not the "Fu" algorithm. This is called by default
from ``trigsimp``. By default, hyperbolics subexpressions
will be simplified, but this can be disabled by setting
``hyper=False``.
Examples
========
>>> from sympy import trigsimp, tan, sinh, tanh
>>> from sympy.simplify.trigsimp import futrig
>>> from sympy.abc import x
>>> trigsimp(1/tan(x)**2)
tan(x)**(-2)
>>> futrig(sinh(x)/tanh(x))
cosh(x)
"""
from sympy.simplify.fu import hyper_as_trig
e = sympify(e)
if not isinstance(e, Basic):
return e
if not e.args:
return e
old = e
e = bottom_up(e, _futrig)
if hyper and e.has(HyperbolicFunction):
e, f = hyper_as_trig(e)
e = f(bottom_up(e, _futrig))
if e != old and e.is_Mul and e.args[0].is_Rational:
# redistribute leading coeff on 2-arg Add
e = Mul(*e.as_coeff_Mul())
return e
def _futrig(e):
"""Helper for futrig."""
from sympy.simplify.fu import (
TR1, TR2, TR3, TR2i, TR10, L, TR10i,
TR8, TR6, TR15, TR16, TR111, TR5, TRmorrie, TR11, _TR11, TR14, TR22,
TR12)
if not e.has(TrigonometricFunction):
return e
if e.is_Mul:
coeff, e = e.as_independent(TrigonometricFunction)
else:
coeff = None
Lops = lambda x: (L(x), x.count_ops(), _nodes(x), len(x.args), x.is_Add)
trigs = lambda x: x.has(TrigonometricFunction)
tree = [identity,
(
TR3, # canonical angles
TR1, # sec-csc -> cos-sin
TR12, # expand tan of sum
lambda x: _eapply(factor, x, trigs),
TR2, # tan-cot -> sin-cos
[identity, lambda x: _eapply(_mexpand, x, trigs)],
TR2i, # sin-cos ratio -> tan
lambda x: _eapply(lambda i: factor(i.normal()), x, trigs),
TR14, # factored identities
TR5, # sin-pow -> cos_pow
TR10, # sin-cos of sums -> sin-cos prod
TR11, _TR11, TR6, # reduce double angles and rewrite cos pows
lambda x: _eapply(factor, x, trigs),
TR14, # factored powers of identities
[identity, lambda x: _eapply(_mexpand, x, trigs)],
TR10i, # sin-cos products > sin-cos of sums
TRmorrie,
[identity, TR8], # sin-cos products -> sin-cos of sums
[identity, lambda x: TR2i(TR2(x))], # tan -> sin-cos -> tan
[
lambda x: _eapply(expand_mul, TR5(x), trigs),
lambda x: _eapply(
expand_mul, TR15(x), trigs)], # pos/neg powers of sin
[
lambda x: _eapply(expand_mul, TR6(x), trigs),
lambda x: _eapply(
expand_mul, TR16(x), trigs)], # pos/neg powers of cos
TR111, # tan, sin, cos to neg power -> cot, csc, sec
[identity, TR2i], # sin-cos ratio to tan
[identity, lambda x: _eapply(
expand_mul, TR22(x), trigs)], # tan-cot to sec-csc
TR1, TR2, TR2i,
[identity, lambda x: _eapply(
factor_terms, TR12(x), trigs)], # expand tan of sum
)]
e = greedy(tree, objective=Lops)(e)
if coeff is not None:
e = coeff * e
return e
def _is_Expr(e):
"""_eapply helper to tell whether ``e`` and all its args
are Exprs."""
if isinstance(e, Derivative):
return _is_Expr(e.expr)
if not isinstance(e, Expr):
return False
return all(_is_Expr(i) for i in e.args)
def _eapply(func, e, cond=None):
"""Apply ``func`` to ``e`` if all args are Exprs else only
apply it to those args that *are* Exprs."""
if not isinstance(e, Expr):
return e
if _is_Expr(e) or not e.args:
return func(e)
return e.func(*[
_eapply(func, ei) if (cond is None or cond(ei)) else ei
for ei in e.args])