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 from sympy.core.intfunc import 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 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 v in d.values(): # 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 = min([new, factor(new)], key=count_ops) # 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])