123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503 |
- from sympy.core import Function, S, Mul, Pow, Add
- from sympy.core.sorting import ordered, default_sort_key
- from sympy.core.function import expand_func
- from sympy.core.symbol import Dummy
- from sympy.functions import gamma, sqrt, sin
- from sympy.polys import factor, cancel
- from sympy.utilities.iterables import sift, uniq
- def gammasimp(expr):
- r"""
- Simplify expressions with gamma functions.
- Explanation
- ===========
- This function takes as input an expression containing gamma
- functions or functions that can be rewritten in terms of gamma
- functions and tries to minimize the number of those functions and
- reduce the size of their arguments.
- The algorithm works by rewriting all gamma functions as expressions
- involving rising factorials (Pochhammer symbols) and applies
- recurrence relations and other transformations applicable to rising
- factorials, to reduce their arguments, possibly letting the resulting
- rising factorial to cancel. Rising factorials with the second argument
- being an integer are expanded into polynomial forms and finally all
- other rising factorial are rewritten in terms of gamma functions.
- Then the following two steps are performed.
- 1. Reduce the number of gammas by applying the reflection theorem
- gamma(x)*gamma(1-x) == pi/sin(pi*x).
- 2. Reduce the number of gammas by applying the multiplication theorem
- gamma(x)*gamma(x+1/n)*...*gamma(x+(n-1)/n) == C*gamma(n*x).
- It then reduces the number of prefactors by absorbing them into gammas
- where possible and expands gammas with rational argument.
- All transformation rules can be found (or were derived from) here:
- .. [1] http://functions.wolfram.com/GammaBetaErf/Pochhammer/17/01/02/
- .. [2] http://functions.wolfram.com/GammaBetaErf/Pochhammer/27/01/0005/
- Examples
- ========
- >>> from sympy.simplify import gammasimp
- >>> from sympy import gamma, Symbol
- >>> from sympy.abc import x
- >>> n = Symbol('n', integer = True)
- >>> gammasimp(gamma(x)/gamma(x - 3))
- (x - 3)*(x - 2)*(x - 1)
- >>> gammasimp(gamma(n + 3))
- gamma(n + 3)
- """
- expr = expr.rewrite(gamma)
- # compute_ST will be looking for Functions and we don't want
- # it looking for non-gamma functions: issue 22606
- # so we mask free, non-gamma functions
- f = expr.atoms(Function)
- # take out gammas
- gammas = {i for i in f if isinstance(i, gamma)}
- if not gammas:
- return expr # avoid side effects like factoring
- f -= gammas
- # keep only those without bound symbols
- f = f & expr.as_dummy().atoms(Function)
- if f:
- dum, fun, simp = zip(*[
- (Dummy(), fi, fi.func(*[
- _gammasimp(a, as_comb=False) for a in fi.args]))
- for fi in ordered(f)])
- d = expr.xreplace(dict(zip(fun, dum)))
- return _gammasimp(d, as_comb=False).xreplace(dict(zip(dum, simp)))
- return _gammasimp(expr, as_comb=False)
- def _gammasimp(expr, as_comb):
- """
- Helper function for gammasimp and combsimp.
- Explanation
- ===========
- Simplifies expressions written in terms of gamma function. If
- as_comb is True, it tries to preserve integer arguments. See
- docstring of gammasimp for more information. This was part of
- combsimp() in combsimp.py.
- """
- expr = expr.replace(gamma,
- lambda n: _rf(1, (n - 1).expand()))
- if as_comb:
- expr = expr.replace(_rf,
- lambda a, b: gamma(b + 1))
- else:
- expr = expr.replace(_rf,
- lambda a, b: gamma(a + b)/gamma(a))
- def rule_gamma(expr, level=0):
- """ Simplify products of gamma functions further. """
- if expr.is_Atom:
- return expr
- def gamma_rat(x):
- # helper to simplify ratios of gammas
- was = x.count(gamma)
- xx = x.replace(gamma, lambda n: _rf(1, (n - 1).expand()
- ).replace(_rf, lambda a, b: gamma(a + b)/gamma(a)))
- if xx.count(gamma) < was:
- x = xx
- return x
- def gamma_factor(x):
- # return True if there is a gamma factor in shallow args
- if isinstance(x, gamma):
- return True
- if x.is_Add or x.is_Mul:
- return any(gamma_factor(xi) for xi in x.args)
- if x.is_Pow and (x.exp.is_integer or x.base.is_positive):
- return gamma_factor(x.base)
- return False
- # recursion step
- if level == 0:
- expr = expr.func(*[rule_gamma(x, level + 1) for x in expr.args])
- level += 1
- if not expr.is_Mul:
- return expr
- # non-commutative step
- if level == 1:
- args, nc = expr.args_cnc()
- if not args:
- return expr
- if nc:
- return rule_gamma(Mul._from_args(args), level + 1)*Mul._from_args(nc)
- level += 1
- # pure gamma handling, not factor absorption
- if level == 2:
- T, F = sift(expr.args, gamma_factor, binary=True)
- gamma_ind = Mul(*F)
- d = Mul(*T)
- nd, dd = d.as_numer_denom()
- for ipass in range(2):
- args = list(ordered(Mul.make_args(nd)))
- for i, ni in enumerate(args):
- if ni.is_Add:
- ni, dd = Add(*[
- rule_gamma(gamma_rat(a/dd), level + 1) for a in ni.args]
- ).as_numer_denom()
- args[i] = ni
- if not dd.has(gamma):
- break
- nd = Mul(*args)
- if ipass == 0 and not gamma_factor(nd):
- break
- nd, dd = dd, nd # now process in reversed order
- expr = gamma_ind*nd/dd
- if not (expr.is_Mul and (gamma_factor(dd) or gamma_factor(nd))):
- return expr
- level += 1
- # iteration until constant
- if level == 3:
- while True:
- was = expr
- expr = rule_gamma(expr, 4)
- if expr == was:
- return expr
- numer_gammas = []
- denom_gammas = []
- numer_others = []
- denom_others = []
- def explicate(p):
- if p is S.One:
- return None, []
- b, e = p.as_base_exp()
- if e.is_Integer:
- if isinstance(b, gamma):
- return True, [b.args[0]]*e
- else:
- return False, [b]*e
- else:
- return False, [p]
- newargs = list(ordered(expr.args))
- while newargs:
- n, d = newargs.pop().as_numer_denom()
- isg, l = explicate(n)
- if isg:
- numer_gammas.extend(l)
- elif isg is False:
- numer_others.extend(l)
- isg, l = explicate(d)
- if isg:
- denom_gammas.extend(l)
- elif isg is False:
- denom_others.extend(l)
- # =========== level 2 work: pure gamma manipulation =========
- if not as_comb:
- # Try to reduce the number of gamma factors by applying the
- # reflection formula gamma(x)*gamma(1-x) = pi/sin(pi*x)
- for gammas, numer, denom in [(
- numer_gammas, numer_others, denom_others),
- (denom_gammas, denom_others, numer_others)]:
- new = []
- while gammas:
- g1 = gammas.pop()
- if g1.is_integer:
- new.append(g1)
- continue
- for i, g2 in enumerate(gammas):
- n = g1 + g2 - 1
- if not n.is_Integer:
- continue
- numer.append(S.Pi)
- denom.append(sin(S.Pi*g1))
- gammas.pop(i)
- if n > 0:
- for k in range(n):
- numer.append(1 - g1 + k)
- elif n < 0:
- for k in range(-n):
- denom.append(-g1 - k)
- break
- else:
- new.append(g1)
- # /!\ updating IN PLACE
- gammas[:] = new
- # Try to reduce the number of gammas by using the duplication
- # theorem to cancel an upper and lower: gamma(2*s)/gamma(s) =
- # 2**(2*s + 1)/(4*sqrt(pi))*gamma(s + 1/2). Although this could
- # be done with higher argument ratios like gamma(3*x)/gamma(x),
- # this would not reduce the number of gammas as in this case.
- for ng, dg, no, do in [(numer_gammas, denom_gammas, numer_others,
- denom_others),
- (denom_gammas, numer_gammas, denom_others,
- numer_others)]:
- while True:
- for x in ng:
- for y in dg:
- n = x - 2*y
- if n.is_Integer:
- break
- else:
- continue
- break
- else:
- break
- ng.remove(x)
- dg.remove(y)
- if n > 0:
- for k in range(n):
- no.append(2*y + k)
- elif n < 0:
- for k in range(-n):
- do.append(2*y - 1 - k)
- ng.append(y + S.Half)
- no.append(2**(2*y - 1))
- do.append(sqrt(S.Pi))
- # Try to reduce the number of gamma factors by applying the
- # multiplication theorem (used when n gammas with args differing
- # by 1/n mod 1 are encountered).
- #
- # run of 2 with args differing by 1/2
- #
- # >>> gammasimp(gamma(x)*gamma(x+S.Half))
- # 2*sqrt(2)*2**(-2*x - 1/2)*sqrt(pi)*gamma(2*x)
- #
- # run of 3 args differing by 1/3 (mod 1)
- #
- # >>> gammasimp(gamma(x)*gamma(x+S(1)/3)*gamma(x+S(2)/3))
- # 6*3**(-3*x - 1/2)*pi*gamma(3*x)
- # >>> gammasimp(gamma(x)*gamma(x+S(1)/3)*gamma(x+S(5)/3))
- # 2*3**(-3*x - 1/2)*pi*(3*x + 2)*gamma(3*x)
- #
- def _run(coeffs):
- # find runs in coeffs such that the difference in terms (mod 1)
- # of t1, t2, ..., tn is 1/n
- u = list(uniq(coeffs))
- for i in range(len(u)):
- dj = ([((u[j] - u[i]) % 1, j) for j in range(i + 1, len(u))])
- for one, j in dj:
- if one.p == 1 and one.q != 1:
- n = one.q
- got = [i]
- get = list(range(1, n))
- for d, j in dj:
- m = n*d
- if m.is_Integer and m in get:
- get.remove(m)
- got.append(j)
- if not get:
- break
- else:
- continue
- for i, j in enumerate(got):
- c = u[j]
- coeffs.remove(c)
- got[i] = c
- return one.q, got[0], got[1:]
- def _mult_thm(gammas, numer, denom):
- # pull off and analyze the leading coefficient from each gamma arg
- # looking for runs in those Rationals
- # expr -> coeff + resid -> rats[resid] = coeff
- rats = {}
- for g in gammas:
- c, resid = g.as_coeff_Add()
- rats.setdefault(resid, []).append(c)
- # look for runs in Rationals for each resid
- keys = sorted(rats, key=default_sort_key)
- for resid in keys:
- coeffs = list(sorted(rats[resid]))
- new = []
- while True:
- run = _run(coeffs)
- if run is None:
- break
- # process the sequence that was found:
- # 1) convert all the gamma functions to have the right
- # argument (could be off by an integer)
- # 2) append the factors corresponding to the theorem
- # 3) append the new gamma function
- n, ui, other = run
- # (1)
- for u in other:
- con = resid + u - 1
- for k in range(int(u - ui)):
- numer.append(con - k)
- con = n*(resid + ui) # for (2) and (3)
- # (2)
- numer.append((2*S.Pi)**(S(n - 1)/2)*
- n**(S.Half - con))
- # (3)
- new.append(con)
- # restore resid to coeffs
- rats[resid] = [resid + c for c in coeffs] + new
- # rebuild the gamma arguments
- g = []
- for resid in keys:
- g += rats[resid]
- # /!\ updating IN PLACE
- gammas[:] = g
- for l, numer, denom in [(numer_gammas, numer_others, denom_others),
- (denom_gammas, denom_others, numer_others)]:
- _mult_thm(l, numer, denom)
- # =========== level >= 2 work: factor absorption =========
- if level >= 2:
- # Try to absorb factors into the gammas: x*gamma(x) -> gamma(x + 1)
- # and gamma(x)/(x - 1) -> gamma(x - 1)
- # This code (in particular repeated calls to find_fuzzy) can be very
- # slow.
- def find_fuzzy(l, x):
- if not l:
- return
- S1, T1 = compute_ST(x)
- for y in l:
- S2, T2 = inv[y]
- if T1 != T2 or (not S1.intersection(S2) and
- (S1 != set() or S2 != set())):
- continue
- # XXX we want some simplification (e.g. cancel or
- # simplify) but no matter what it's slow.
- a = len(cancel(x/y).free_symbols)
- b = len(x.free_symbols)
- c = len(y.free_symbols)
- # TODO is there a better heuristic?
- if a == 0 and (b > 0 or c > 0):
- return y
- # We thus try to avoid expensive calls by building the following
- # "invariants": For every factor or gamma function argument
- # - the set of free symbols S
- # - the set of functional components T
- # We will only try to absorb if T1==T2 and (S1 intersect S2 != emptyset
- # or S1 == S2 == emptyset)
- inv = {}
- def compute_ST(expr):
- if expr in inv:
- return inv[expr]
- return (expr.free_symbols, expr.atoms(Function).union(
- {e.exp for e in expr.atoms(Pow)}))
- def update_ST(expr):
- inv[expr] = compute_ST(expr)
- for expr in numer_gammas + denom_gammas + numer_others + denom_others:
- update_ST(expr)
- for gammas, numer, denom in [(
- numer_gammas, numer_others, denom_others),
- (denom_gammas, denom_others, numer_others)]:
- new = []
- while gammas:
- g = gammas.pop()
- cont = True
- while cont:
- cont = False
- y = find_fuzzy(numer, g)
- if y is not None:
- numer.remove(y)
- if y != g:
- numer.append(y/g)
- update_ST(y/g)
- g += 1
- cont = True
- y = find_fuzzy(denom, g - 1)
- if y is not None:
- denom.remove(y)
- if y != g - 1:
- numer.append((g - 1)/y)
- update_ST((g - 1)/y)
- g -= 1
- cont = True
- new.append(g)
- # /!\ updating IN PLACE
- gammas[:] = new
- # =========== rebuild expr ==================================
- return Mul(*[gamma(g) for g in numer_gammas]) \
- / Mul(*[gamma(g) for g in denom_gammas]) \
- * Mul(*numer_others) / Mul(*denom_others)
- was = factor(expr)
- # (for some reason we cannot use Basic.replace in this case)
- expr = rule_gamma(was)
- if expr != was:
- expr = factor(expr)
- expr = expr.replace(gamma,
- lambda n: expand_func(gamma(n)) if n.is_Rational else gamma(n))
- return expr
- class _rf(Function):
- @classmethod
- def eval(cls, a, b):
- if b.is_Integer:
- if not b:
- return S.One
- n, result = int(b), S.One
- if n > 0:
- for i in range(n):
- result *= a + i
- return result
- elif n < 0:
- for i in range(1, -n + 1):
- result *= a - i
- return 1/result
- else:
- if b.is_Add:
- c, _b = b.as_coeff_Add()
- if c.is_Integer:
- if c > 0:
- return _rf(a, _b)*_rf(a + _b, c)
- elif c < 0:
- return _rf(a, _b)/_rf(a + _b + c, -c)
- if a.is_Add:
- c, _a = a.as_coeff_Add()
- if c.is_Integer:
- if c > 0:
- return _rf(_a, b)*_rf(_a + b, c)/_rf(_a, c)
- elif c < 0:
- return _rf(_a, b)*_rf(_a + c, -c)/_rf(_a + b + c, -c)
|