def normal(expr, *syms): p, q = together(expr).as_numer_denom() if p.is_polynomial(*syms) and q.is_polynomial(*syms): from sympy.polynomials import gcd, quo G = gcd(p, q, syms) if not isinstance(G, Basic.One): p = quo(p, G, syms) q = quo(q, G, syms) return p / q
def _eval_product(self, term=None): k = self.index a = self.lower n = self.upper if term is None: term = self.term if not term.has(k): return term ** (n - a + 1) elif term.is_polynomial(k): poly = term.as_polynomial(k) A = B = Q = S.One C = poly.leading_coeff() all_roots = roots(poly) for r in all_roots: A *= Basic.RisingFactorial(a - r, n - a + 1) Q *= n - r if len(all_roots) < poly.degree(): B = Product(quo(poly, Q, k), (k, a, n)) return C ** (n - a + 1) * A * B elif isinstance(term, Basic.Add): p, q = term.as_numer_denom() p = self._eval_product(p) q = self._eval_product(q) return p / q elif isinstance(term, Basic.Mul): exclude, include = [], [] for t in term: p = self._eval_product(t) if p is not None: exclude.append(p) else: include.append(p) if not exclude: return None else: A, B = Mul(*exclude), Mul(*include) return A * Product(B, (k, a, n)) elif isinstance(term, Basic.Pow): if not term.base.has(k): s = sum(term.exp, (k, a, n)) if not isinstance(s, Sum): return term.base ** s elif not term.exp.has(k): p = self._eval_product(term.base) if p is not None: return p ** term.exp
def ext_gcd(p, q, x): U = (p, S.One, S.Zero) V = (q, S.Zero, S.One) while True: q = quo(U[0], V[0], x) U, V = V, [ (a - q*b).expand() for a, b in zip(U, V) ] if isinstance(V[0], Basic.Zero): return U
def factorization(poly, linear=False): """Returns a list with polynomial factors over rationals or, if 'linear' flag is set over Q(i). This simple handler should be merged with original factor() method. >>> from sympy import * >>> x, y = symbols('xy') >>> factorization(x**2 - y**2) set([1, x - y, x + y]) >>> factorization(x**2 + 1) set([1, 1 + x**2]) >>> factorization(x**2 + 1, linear=True) set([1, x - I, I + x]) """ factored = set([ q.as_basic() for q in factor_.factor(poly) ]) if not linear: return factored else: factors = [] for factor in factored: symbols = factor.atoms(Symbol) if len(symbols) == 1: x = symbols.pop() else: factors += [ factor ] continue linearities = [ x - r for r in roots(factor, x) ] if not linearities: factors += [ factor ] else: unfactorable = quo(factor, Basic.Mul(*linearities)) if not isinstance(unfactorable, Basic.Number): factors += [ unfactorable ] factors += linearities return set(factors)
def splitter(p): for y in p.atoms(Basic.Symbol): if not isinstance(derivation(y), Basic.Zero): c, q = p.as_polynomial(y).as_primitive() q = q.as_basic() h = gcd(q, derivation(q), y) s = quo(h, gcd(q, q.diff(y), y), y) c_split = splitter(c) if s.as_polynomial(y).degree() == 0: return (c_split[0], q * c_split[1]) q_split = splitter(normal(q / s, *V)) return (c_split[0]*q_split[0]*s, c_split[1]*q_split[1]) else: return (S.One, p)
def normal(f, g, n): """Given relatively prime univariate polynomials 'f' and 'g', rewrite their quotient to a normal form defined as follows: f(n) A(n) C(n+1) ---- = Z ----------- g(n) B(n) C(n) where Z is arbitrary constant and A, B, C are monic polynomials in 'n' with follwing properties: (1) gcd(A(n), B(n+h)) = 1 for all 'h' in N (2) gcd(B(n), C(n+1)) = 1 (3) gcd(A(n), C(n)) = 1 This normal form, or rational factorization in other words, is crucial step in Gosper's algorithm and in difference equations solving. It can be also used to decide if two hypergeometric are similar or not. This procedure will return return triple containig elements of this factorization in the form (Z*A, B, C). For example: >>> from sympy import Symbol >>> n = Symbol('n', integer=True) >>> normal(4*n+5, 2*(4*n+1)*(2*n+3), n) (1/4, 3/2 + n, 1/4 + n) """ f, g = map(Basic.sympify, (f, g)) if f.is_polynomial: p = f.as_polynomial(n) else: raise ValueError("'f' must be a polynomial") if g.is_polynomial: q = g.as_polynomial(n) else: raise ValueError("'g' must be a polynomial") a, p = p.as_monic() b, q = q.as_monic() A = p.sympy_expr B = q.sympy_expr C, Z = S.One, a / b h = Symbol('h', dummy=True) res = resultant(A, B.subs(n, n+h), n) if not res.is_polynomial(h): res = quo(*res.as_numer_denom()) _nni_roots = nni_roots(res, h) if _nni_roots == []: return (f, g, S.One) else: _nni_roots.sort() for i in _nni_roots: d = gcd(A, B.subs(n, n+i), n) A = quo(A, d, n) B = quo(B, d.subs(n, n-i), n) C *= Mul(*[ d.subs(n, n-j) for j in xrange(1, i+1) ]) return (Z*A, B, C)
def hypersimp(term, n, consecutive=True, simplify=True): """Given combinatorial term a(n) simplify its consecutive term ratio ie. a(n+1)/a(n). The term can be composed of functions and integer sequences which have equivalent represenation in terms of gamma special function. Currently ths includes factorials (falling, rising), binomials and gamma it self. The algorithm performs three basic steps: (1) Rewrite all functions in terms of gamma, if possible. (2) Rewrite all occurences of gamma in terms of produtcs of gamma and rising factorial with integer, absolute constant exponent. (3) Perform simplification of nested fractions, powers and if the resulting expression is a quotient of polynomials, reduce their total degree. If the term given is hypergeometric then the result of this procudure is a quotient of polynomials of minimal degree. Sequence is hypergeometric if it is anihilated by linear, homogeneous recurrence operator of first order, so in other words when a(n+1)/a(n) is a rational function. When the status of being hypergeometric or not, is required then you can avoid additional simplification by unsetting 'simplify' flag. This algorithm, due to Wolfram Koepf, is very simple but powerful, however its full potential will be visible when simplification in general will improve. For more information on the implemented algorithm refer to: [1] W. Koepf, Algorithms for m-fold Hypergeometric Summation, Journal of Symbolic Computation (1995) 20, 399-417 """ term = Basic.sympify(term) if consecutive == True: term = term.subs(n, n+1)/term expr = term.rewrite(gamma).expand(func=True, basic=False) p, q = together(expr).as_numer_denom() if p.is_polynomial(n) and q.is_polynomial(n): if simplify == True: from sympy.polynomials import gcd, quo G = gcd(p, q, n) if not isinstance(G, Basic.One): p = quo(p, G, n) q = quo(q, G, n) p = p.as_polynomial(n) q = q.as_polynomial(n) a, p = p.as_integer() b, q = q.as_integer() p = p.as_basic() q = q.as_basic() return (b/a) * (p/q) return p/q else: return None
def rsolve_hyper(coeffs, f, n, **hints): """Given linear recurrence operator L of order 'k' with polynomial coefficients and inhomogeneous equation Ly = f we seek for all hypergeometric solutions over field K of characteristic zero. The inhomogeneous part can be either hypergeometric or a sum of a fixed number of pairwise dissimilar hypergeometric terms. The algorithm performs three basic steps: (1) Group together similar hypergeometric terms in the inhomogeneous part of Ly = f, and find particular solution using Abramov's algorithm. (2) Compute generating set of L and find basis in it, so that all solutions are lineary independent. (3) Form final solution with the number of arbitrary constants equal to dimension of basis of L. Term a(n) is hypergeometric if it is anihilated by first order linear difference equations with polynomial coefficients or, in simpler words, if consecutive term ratio is a rational function. The output of this procedure is a linear combination of fixed number of hypergeometric terms. However the underlying method can generate larger class of solutions - D'Alembertian terms. Note also that this method not only computes the kernel of the inhomogeneous equation, but also reduces in to a basis so that solutions generated by this procedure are lineary independent For more information on the implemented algorithm refer to: [1] M. Petkovsek, Hypergeometric solutions of linear recurrences with polynomial coefficients, J. Symbolic Computation, 14 (1992), 243-264. [2] M. Petkovsek, H. S. Wilf, D. Zeilberger, A = B, 1996. """ coeffs = map(Basic.sympify, coeffs) f = Basic.sympify(f) r, kernel = len(coeffs)-1, [] if not isinstance(f, Basic.Zero): if isinstance(f, Basic.Add): similar = {} for g in f.expand(): if not g.is_hypergeometric(n): return None for h in similar.iterkeys(): if hypersimilar(g, h, n): similar[h] += g break else: similar[g] = S.Zero inhomogeneous = [] for g, h in similar.iteritems(): inhomogeneous.append(g+h) elif f.is_hypergeometric(n): inhomogeneous = [f] else: return None for i, g in enumerate(inhomogeneous): coeff, polys = S.One, coeffs[:] denoms = [ S.One ] * (r+1) s = hypersimp(g, n) for j in xrange(1, r+1): coeff *= s.subs(n, n+j-1) p, q = coeff.as_numer_denom() polys[j] *= p denoms[j] = q for j in xrange(0, r+1): polys[j] *= Mul(*(denoms[:j] + denoms[j+1:])) R = rsolve_poly(polys, Mul(*denoms), n) if not (R is None or isinstance(R, Basic.Zero)): inhomogeneous[i] *= R else: return None result = Add(*inhomogeneous) else: result = S.Zero Z = Symbol('Z', dummy=True) p, q = coeffs[0], coeffs[r].subs(n, n-r+1) p_factors = [ z for z in set(roots(p, n)) ] q_factors = [ z for z in set(roots(q, n)) ] factors = [ (S.One, S.One) ] for p in p_factors: for q in q_factors: if p.is_integer and q.is_integer and p <= q: continue else: factors += [(n-p, n-q)] p = [ (n-p, S.One) for p in p_factors ] q = [ (S.One, n-q) for q in q_factors ] factors = p + factors + q for A, B in factors: polys, degrees = [], [] D = A*B.subs(n, n+r-1) for i in xrange(0, r+1): a = Mul(*[ A.subs(n, n+j) for j in xrange(0, i) ]) b = Mul(*[ B.subs(n, n+j) for j in xrange(i, r) ]) poly = (quo(coeffs[i]*a*b, D, n)) polys.append(poly.as_polynomial(n)) if not isinstance(poly, Basic.Zero): degrees.append(polys[i].degree()) d, poly = max(degrees), S.Zero for i in xrange(0, r+1): coeff = polys[i].nth_coeff(d) if not isinstance(coeff, Basic.Zero): poly += coeff * Z**i for z in set(roots(poly, Z)): if not z.is_real or z.is_zero: continue C = rsolve_poly([ polys[i]*z**i for i in xrange(r+1) ], 0, n) if C is not None and not isinstance(C, Basic.Zero): ratio = z * A * C.subs(n, n + 1) / B / C K = product(simplify(ratio), (n, 0, n-1)) if casoratian(kernel+[K], n) != 0: kernel.append(K) symbols = [ Symbol('C'+str(i)) for i in xrange(len(kernel)) ] for C, ker in zip(symbols, kernel): result += C * ker if hints.get('symbols', False): return (result, symbols) else: return result
def rsolve_ratio(coeffs, f, n, **hints): """Given linear recurrence operator L of order 'k' with polynomial coefficients and inhomogeneous equation Ly = f, where 'f' is a polynomial, we seek for all rational solutions over field K of characteristic zero. This procedure accepts only polynomials, however if you are interested in solving recurrence with ratinal coefficients then use rsolve() with will preprocess equation given and run this procedure with polynomial arguments. The algorithm performs two basic steps: (1) Compute polynomial v(n) which can be used as universal denominator of any rational solution of equation Ly = f. (2) Construct new linear difference equation by substitution y(n) = u(n)/v(n) and solve it for u(n) finding all its polynomial solutions. Return None if none were found. Algorithm implemented here is a revised version of the original Abramov's algorithm, developed in 1989. The new approach is much simpler to implement and has better overall efficiency. This method can be easily adapted to q-difference equations case. Besides finding rational solutions alone, this functions is an important part of Hyper algorithm were it is used to find particular solution of ingomogeneous part of a recurrence. For more information on the implemented algorithm refer to: [1] S. A. Abramov, Rational solutions of linear difference and q-difference equations with polynomial coefficients, in: T. Levelt, ed., Proc. ISSAC '95, ACM Press, New York, 1995, 285-289 """ f = Basic.sympify(f) if not f.is_polynomial(n): return None coeffs = map(Basic.sympify, coeffs) r = len(coeffs)-1 A, B = coeffs[r], coeffs[0] A = A.subs(n, n-r).expand() h = Symbol('h', dummy=True) res = resultant(A, B.subs(n, n+h), n) if not res.is_polynomial(h): p, q = res.as_numer_denom() res = quo(p, q, h) _nni_roots = nni_roots(res, h) if _nni_roots == []: return rsolve_poly(coeffs, f, n, **hints) else: C, numers = S.One, [S.Zero]*(r+1) for i in xrange(int(max(_nni_roots)), -1, -1): d = gcd(A, B.subs(n, n+i), n) A = quo(A, d, n) B = quo(B, d.subs(n, n-i), n) C *= Mul(*[ d.subs(n, n-j) for j in xrange(0, i+1) ]) denoms = [ C.subs(n, n+i) for i in range(0, r+1) ] for i in range(0, r+1): g = gcd(coeffs[i], denoms[i], n) numers[i] = quo(coeffs[i], g, n) denoms[i] = quo(denoms[i], g, n) for i in xrange(0, r+1): numers[i] *= Mul(*(denoms[:i] + denoms[i+1:])) result = rsolve_poly(numers, f * Mul(*denoms), n, **hints) if result is not None: if hints.get('symbols', False): return (simplify(result[0] / C), result[1]) else: return simplify(result / C) else: return None
def apart(f, z, domain=None, index=None): """Computes full partial fraction decomposition of a univariate rational function over the algebraic closure of its field of definition. Although only gcd operations over the initial field are required, the expansion is returned in a formal form with linear denominators. However it is possible to force expansion of the resulting formal summations, and so factorization over a specified domain is performed. To specify the desired behavior of the algorithm use the 'domain' keyword. Setting it to None, which is done be default, will result in no factorization at all. Otherwise it can be assigned with one of Z, Q, R, C domain specifiers and the formal partial fraction expansion will be rewritten using all possible roots over this domain. If the resulting expansion contains formal summations, then for all those a single dummy index variable named 'a' will be generated. To change this default behavior issue new name via 'index' keyword. For more information on the implemented algorithm refer to: [1] M. Bronstein, B. Salvy, Full partial fraction decomposition of rational functions, in: M. Bronstein, ed., Proceedings ISSAC '93, ACM Press, Kiev, Ukraine, 1993, pp. 157-160. """ f = Basic.sympify(f) if isinstance(f, Basic.Add): return Add(*[ apart(g) for g in f ]) else: if f.is_fraction(z): f = normal(f, z) else: return f P, Q = f.as_numer_denom() if not Q.has(z): return f u = Function('u')(z) if index is None: A = Symbol('a', dummy=True) else: A = Symbol(index) partial, r = div(P, Q, z) f, q, U = r / Q, Q, [] for k, d in enumerate(sqf(q, z)): n, d = k + 1, d.as_basic() U += [ u.diff(z, k) ] h = normal(f * d**n, z) / u**n H, subs = [h], [] for j in range(1, n): H += [ H[-1].diff(z) / j ] for j in range(1, n+1): subs += [ (U[j-1], d.diff(z, j) / j) ] for j in range(0, n): P, Q = together(H[j]).as_numer_denom() for i in range(0, j+1): P = P.subs(*subs[j-i]) Q = Q.subs(*subs[0]) G = gcd(P, d, z) D = quo(d, G, z) g, B, _ = ext_gcd(Q, D, z) b = rem(P * B / g, D, z) term = b.subs(z, A) / (z - A)**(n-j) if domain is None: a = D.diff(z) if not a.has(z): partial += term.subs(A, -D.subs(z, 0) / a) else: partial += Basic.Sum(term, (A, Basic.RootOf(D, z))) else: raise NotImplementedError return partial