You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2198 lines
63 KiB

5 months ago
"""Real and complex root isolation and refinement algorithms. """
from sympy.polys.densearith import (
dup_neg, dup_rshift, dup_rem,
dup_l2_norm_squared)
from sympy.polys.densebasic import (
dup_LC, dup_TC, dup_degree,
dup_strip, dup_reverse,
dup_convert,
dup_terms_gcd)
from sympy.polys.densetools import (
dup_clear_denoms,
dup_mirror, dup_scale, dup_shift,
dup_transform,
dup_diff,
dup_eval, dmp_eval_in,
dup_sign_variations,
dup_real_imag)
from sympy.polys.euclidtools import (
dup_discriminant)
from sympy.polys.factortools import (
dup_factor_list)
from sympy.polys.polyerrors import (
RefinementFailed,
DomainError,
PolynomialError)
from sympy.polys.sqfreetools import (
dup_sqf_part, dup_sqf_list)
def dup_sturm(f, K):
"""
Computes the Sturm sequence of ``f`` in ``F[x]``.
Given a univariate, square-free polynomial ``f(x)`` returns the
associated Sturm sequence ``f_0(x), ..., f_n(x)`` defined by::
f_0(x), f_1(x) = f(x), f'(x)
f_n = -rem(f_{n-2}(x), f_{n-1}(x))
Examples
========
>>> from sympy.polys import ring, QQ
>>> R, x = ring("x", QQ)
>>> R.dup_sturm(x**3 - 2*x**2 + x - 3)
[x**3 - 2*x**2 + x - 3, 3*x**2 - 4*x + 1, 2/9*x + 25/9, -2079/4]
References
==========
.. [1] [Davenport88]_
"""
if not K.is_Field:
raise DomainError("Cannot compute Sturm sequence over %s" % K)
f = dup_sqf_part(f, K)
sturm = [f, dup_diff(f, 1, K)]
while sturm[-1]:
s = dup_rem(sturm[-2], sturm[-1], K)
sturm.append(dup_neg(s, K))
return sturm[:-1]
def dup_root_upper_bound(f, K):
"""Compute the LMQ upper bound for the positive roots of `f`;
LMQ (Local Max Quadratic) was developed by Akritas-Strzebonski-Vigklas.
References
==========
.. [1] Alkiviadis G. Akritas: "Linear and Quadratic Complexity Bounds on the
Values of the Positive Roots of Polynomials"
Journal of Universal Computer Science, Vol. 15, No. 3, 523-537, 2009.
"""
n, P = len(f), []
t = n * [K.one]
if dup_LC(f, K) < 0:
f = dup_neg(f, K)
f = list(reversed(f))
for i in range(0, n):
if f[i] >= 0:
continue
a, QL = K.log(-f[i], 2), []
for j in range(i + 1, n):
if f[j] <= 0:
continue
q = t[j] + a - K.log(f[j], 2)
QL.append([q // (j - i), j])
if not QL:
continue
q = min(QL)
t[q[1]] = t[q[1]] + 1
P.append(q[0])
if not P:
return None
else:
return K.get_field()(2)**(max(P) + 1)
def dup_root_lower_bound(f, K):
"""Compute the LMQ lower bound for the positive roots of `f`;
LMQ (Local Max Quadratic) was developed by Akritas-Strzebonski-Vigklas.
References
==========
.. [1] Alkiviadis G. Akritas: "Linear and Quadratic Complexity Bounds on the
Values of the Positive Roots of Polynomials"
Journal of Universal Computer Science, Vol. 15, No. 3, 523-537, 2009.
"""
bound = dup_root_upper_bound(dup_reverse(f), K)
if bound is not None:
return 1/bound
else:
return None
def dup_cauchy_upper_bound(f, K):
"""
Compute the Cauchy upper bound on the absolute value of all roots of f,
real or complex.
References
==========
.. [1] https://en.wikipedia.org/wiki/Geometrical_properties_of_polynomial_roots#Lagrange's_and_Cauchy's_bounds
"""
n = dup_degree(f)
if n < 1:
raise PolynomialError('Polynomial has no roots.')
if K.is_ZZ:
L = K.get_field()
f, K = dup_convert(f, K, L), L
elif not K.is_QQ or K.is_RR or K.is_CC:
# We need to compute absolute value, and we are not supporting cases
# where this would take us outside the domain (or its quotient field).
raise DomainError('Cauchy bound not supported over %s' % K)
else:
f = f[:]
while K.is_zero(f[-1]):
f.pop()
if len(f) == 1:
# Monomial. All roots are zero.
return K.zero
lc = f[0]
return K.one + max(abs(n / lc) for n in f[1:])
def dup_cauchy_lower_bound(f, K):
"""Compute the Cauchy lower bound on the absolute value of all non-zero
roots of f, real or complex."""
g = dup_reverse(f)
if len(g) < 2:
raise PolynomialError('Polynomial has no non-zero roots.')
if K.is_ZZ:
K = K.get_field()
b = dup_cauchy_upper_bound(g, K)
return K.one / b
def dup_mignotte_sep_bound_squared(f, K):
"""
Return the square of the Mignotte lower bound on separation between
distinct roots of f. The square is returned so that the bound lies in
K or its quotient field.
References
==========
.. [1] Mignotte, Maurice. "Some useful bounds." Computer algebra.
Springer, Vienna, 1982. 259-263.
https://people.dm.unipi.it/gianni/AC-EAG/Mignotte.pdf
"""
n = dup_degree(f)
if n < 2:
raise PolynomialError('Polynomials of degree < 2 have no distinct roots.')
if K.is_ZZ:
L = K.get_field()
f, K = dup_convert(f, K, L), L
elif not K.is_QQ or K.is_RR or K.is_CC:
# We need to compute absolute value, and we are not supporting cases
# where this would take us outside the domain (or its quotient field).
raise DomainError('Mignotte bound not supported over %s' % K)
D = dup_discriminant(f, K)
l2sq = dup_l2_norm_squared(f, K)
return K(3)*K.abs(D) / ( K(n)**(n+1) * l2sq**(n-1) )
def _mobius_from_interval(I, field):
"""Convert an open interval to a Mobius transform. """
s, t = I
a, c = field.numer(s), field.denom(s)
b, d = field.numer(t), field.denom(t)
return a, b, c, d
def _mobius_to_interval(M, field):
"""Convert a Mobius transform to an open interval. """
a, b, c, d = M
s, t = field(a, c), field(b, d)
if s <= t:
return (s, t)
else:
return (t, s)
def dup_step_refine_real_root(f, M, K, fast=False):
"""One step of positive real root refinement algorithm. """
a, b, c, d = M
if a == b and c == d:
return f, (a, b, c, d)
A = dup_root_lower_bound(f, K)
if A is not None:
A = K(int(A))
else:
A = K.zero
if fast and A > 16:
f = dup_scale(f, A, K)
a, c, A = A*a, A*c, K.one
if A >= K.one:
f = dup_shift(f, A, K)
b, d = A*a + b, A*c + d
if not dup_eval(f, K.zero, K):
return f, (b, b, d, d)
f, g = dup_shift(f, K.one, K), f
a1, b1, c1, d1 = a, a + b, c, c + d
if not dup_eval(f, K.zero, K):
return f, (b1, b1, d1, d1)
k = dup_sign_variations(f, K)
if k == 1:
a, b, c, d = a1, b1, c1, d1
else:
f = dup_shift(dup_reverse(g), K.one, K)
if not dup_eval(f, K.zero, K):
f = dup_rshift(f, 1, K)
a, b, c, d = b, a + b, d, c + d
return f, (a, b, c, d)
def dup_inner_refine_real_root(f, M, K, eps=None, steps=None, disjoint=None, fast=False, mobius=False):
"""Refine a positive root of `f` given a Mobius transform or an interval. """
F = K.get_field()
if len(M) == 2:
a, b, c, d = _mobius_from_interval(M, F)
else:
a, b, c, d = M
while not c:
f, (a, b, c, d) = dup_step_refine_real_root(f, (a, b, c,
d), K, fast=fast)
if eps is not None and steps is not None:
for i in range(0, steps):
if abs(F(a, c) - F(b, d)) >= eps:
f, (a, b, c, d) = dup_step_refine_real_root(f, (a, b, c, d), K, fast=fast)
else:
break
else:
if eps is not None:
while abs(F(a, c) - F(b, d)) >= eps:
f, (a, b, c, d) = dup_step_refine_real_root(f, (a, b, c, d), K, fast=fast)
if steps is not None:
for i in range(0, steps):
f, (a, b, c, d) = dup_step_refine_real_root(f, (a, b, c, d), K, fast=fast)
if disjoint is not None:
while True:
u, v = _mobius_to_interval((a, b, c, d), F)
if v <= disjoint or disjoint <= u:
break
else:
f, (a, b, c, d) = dup_step_refine_real_root(f, (a, b, c, d), K, fast=fast)
if not mobius:
return _mobius_to_interval((a, b, c, d), F)
else:
return f, (a, b, c, d)
def dup_outer_refine_real_root(f, s, t, K, eps=None, steps=None, disjoint=None, fast=False):
"""Refine a positive root of `f` given an interval `(s, t)`. """
a, b, c, d = _mobius_from_interval((s, t), K.get_field())
f = dup_transform(f, dup_strip([a, b]),
dup_strip([c, d]), K)
if dup_sign_variations(f, K) != 1:
raise RefinementFailed("there should be exactly one root in (%s, %s) interval" % (s, t))
return dup_inner_refine_real_root(f, (a, b, c, d), K, eps=eps, steps=steps, disjoint=disjoint, fast=fast)
def dup_refine_real_root(f, s, t, K, eps=None, steps=None, disjoint=None, fast=False):
"""Refine real root's approximating interval to the given precision. """
if K.is_QQ:
(_, f), K = dup_clear_denoms(f, K, convert=True), K.get_ring()
elif not K.is_ZZ:
raise DomainError("real root refinement not supported over %s" % K)
if s == t:
return (s, t)
if s > t:
s, t = t, s
negative = False
if s < 0:
if t <= 0:
f, s, t, negative = dup_mirror(f, K), -t, -s, True
else:
raise ValueError("Cannot refine a real root in (%s, %s)" % (s, t))
if negative and disjoint is not None:
if disjoint < 0:
disjoint = -disjoint
else:
disjoint = None
s, t = dup_outer_refine_real_root(
f, s, t, K, eps=eps, steps=steps, disjoint=disjoint, fast=fast)
if negative:
return (-t, -s)
else:
return ( s, t)
def dup_inner_isolate_real_roots(f, K, eps=None, fast=False):
"""Internal function for isolation positive roots up to given precision.
References
==========
1. Alkiviadis G. Akritas and Adam W. Strzebonski: A Comparative Study of Two Real Root
Isolation Methods . Nonlinear Analysis: Modelling and Control, Vol. 10, No. 4, 297-304, 2005.
2. Alkiviadis G. Akritas, Adam W. Strzebonski and Panagiotis S. Vigklas: Improving the
Performance of the Continued Fractions Method Using new Bounds of Positive Roots. Nonlinear
Analysis: Modelling and Control, Vol. 13, No. 3, 265-279, 2008.
"""
a, b, c, d = K.one, K.zero, K.zero, K.one
k = dup_sign_variations(f, K)
if k == 0:
return []
if k == 1:
roots = [dup_inner_refine_real_root(
f, (a, b, c, d), K, eps=eps, fast=fast, mobius=True)]
else:
roots, stack = [], [(a, b, c, d, f, k)]
while stack:
a, b, c, d, f, k = stack.pop()
A = dup_root_lower_bound(f, K)
if A is not None:
A = K(int(A))
else:
A = K.zero
if fast and A > 16:
f = dup_scale(f, A, K)
a, c, A = A*a, A*c, K.one
if A >= K.one:
f = dup_shift(f, A, K)
b, d = A*a + b, A*c + d
if not dup_TC(f, K):
roots.append((f, (b, b, d, d)))
f = dup_rshift(f, 1, K)
k = dup_sign_variations(f, K)
if k == 0:
continue
if k == 1:
roots.append(dup_inner_refine_real_root(
f, (a, b, c, d), K, eps=eps, fast=fast, mobius=True))
continue
f1 = dup_shift(f, K.one, K)
a1, b1, c1, d1, r = a, a + b, c, c + d, 0
if not dup_TC(f1, K):
roots.append((f1, (b1, b1, d1, d1)))
f1, r = dup_rshift(f1, 1, K), 1
k1 = dup_sign_variations(f1, K)
k2 = k - k1 - r
a2, b2, c2, d2 = b, a + b, d, c + d
if k2 > 1:
f2 = dup_shift(dup_reverse(f), K.one, K)
if not dup_TC(f2, K):
f2 = dup_rshift(f2, 1, K)
k2 = dup_sign_variations(f2, K)
else:
f2 = None
if k1 < k2:
a1, a2, b1, b2 = a2, a1, b2, b1
c1, c2, d1, d2 = c2, c1, d2, d1
f1, f2, k1, k2 = f2, f1, k2, k1
if not k1:
continue
if f1 is None:
f1 = dup_shift(dup_reverse(f), K.one, K)
if not dup_TC(f1, K):
f1 = dup_rshift(f1, 1, K)
if k1 == 1:
roots.append(dup_inner_refine_real_root(
f1, (a1, b1, c1, d1), K, eps=eps, fast=fast, mobius=True))
else:
stack.append((a1, b1, c1, d1, f1, k1))
if not k2:
continue
if f2 is None:
f2 = dup_shift(dup_reverse(f), K.one, K)
if not dup_TC(f2, K):
f2 = dup_rshift(f2, 1, K)
if k2 == 1:
roots.append(dup_inner_refine_real_root(
f2, (a2, b2, c2, d2), K, eps=eps, fast=fast, mobius=True))
else:
stack.append((a2, b2, c2, d2, f2, k2))
return roots
def _discard_if_outside_interval(f, M, inf, sup, K, negative, fast, mobius):
"""Discard an isolating interval if outside ``(inf, sup)``. """
F = K.get_field()
while True:
u, v = _mobius_to_interval(M, F)
if negative:
u, v = -v, -u
if (inf is None or u >= inf) and (sup is None or v <= sup):
if not mobius:
return u, v
else:
return f, M
elif (sup is not None and u > sup) or (inf is not None and v < inf):
return None
else:
f, M = dup_step_refine_real_root(f, M, K, fast=fast)
def dup_inner_isolate_positive_roots(f, K, eps=None, inf=None, sup=None, fast=False, mobius=False):
"""Iteratively compute disjoint positive root isolation intervals. """
if sup is not None and sup < 0:
return []
roots = dup_inner_isolate_real_roots(f, K, eps=eps, fast=fast)
F, results = K.get_field(), []
if inf is not None or sup is not None:
for f, M in roots:
result = _discard_if_outside_interval(f, M, inf, sup, K, False, fast, mobius)
if result is not None:
results.append(result)
elif not mobius:
for f, M in roots:
u, v = _mobius_to_interval(M, F)
results.append((u, v))
else:
results = roots
return results
def dup_inner_isolate_negative_roots(f, K, inf=None, sup=None, eps=None, fast=False, mobius=False):
"""Iteratively compute disjoint negative root isolation intervals. """
if inf is not None and inf >= 0:
return []
roots = dup_inner_isolate_real_roots(dup_mirror(f, K), K, eps=eps, fast=fast)
F, results = K.get_field(), []
if inf is not None or sup is not None:
for f, M in roots:
result = _discard_if_outside_interval(f, M, inf, sup, K, True, fast, mobius)
if result is not None:
results.append(result)
elif not mobius:
for f, M in roots:
u, v = _mobius_to_interval(M, F)
results.append((-v, -u))
else:
results = roots
return results
def _isolate_zero(f, K, inf, sup, basis=False, sqf=False):
"""Handle special case of CF algorithm when ``f`` is homogeneous. """
j, f = dup_terms_gcd(f, K)
if j > 0:
F = K.get_field()
if (inf is None or inf <= 0) and (sup is None or 0 <= sup):
if not sqf:
if not basis:
return [((F.zero, F.zero), j)], f
else:
return [((F.zero, F.zero), j, [K.one, K.zero])], f
else:
return [(F.zero, F.zero)], f
return [], f
def dup_isolate_real_roots_sqf(f, K, eps=None, inf=None, sup=None, fast=False, blackbox=False):
"""Isolate real roots of a square-free polynomial using the Vincent-Akritas-Strzebonski (VAS) CF approach.
References
==========
.. [1] Alkiviadis G. Akritas and Adam W. Strzebonski: A Comparative
Study of Two Real Root Isolation Methods. Nonlinear Analysis:
Modelling and Control, Vol. 10, No. 4, 297-304, 2005.
.. [2] Alkiviadis G. Akritas, Adam W. Strzebonski and Panagiotis S.
Vigklas: Improving the Performance of the Continued Fractions
Method Using New Bounds of Positive Roots. Nonlinear Analysis:
Modelling and Control, Vol. 13, No. 3, 265-279, 2008.
"""
if K.is_QQ:
(_, f), K = dup_clear_denoms(f, K, convert=True), K.get_ring()
elif not K.is_ZZ:
raise DomainError("isolation of real roots not supported over %s" % K)
if dup_degree(f) <= 0:
return []
I_zero, f = _isolate_zero(f, K, inf, sup, basis=False, sqf=True)
I_neg = dup_inner_isolate_negative_roots(f, K, eps=eps, inf=inf, sup=sup, fast=fast)
I_pos = dup_inner_isolate_positive_roots(f, K, eps=eps, inf=inf, sup=sup, fast=fast)
roots = sorted(I_neg + I_zero + I_pos)
if not blackbox:
return roots
else:
return [ RealInterval((a, b), f, K) for (a, b) in roots ]
def dup_isolate_real_roots(f, K, eps=None, inf=None, sup=None, basis=False, fast=False):
"""Isolate real roots using Vincent-Akritas-Strzebonski (VAS) continued fractions approach.
References
==========
.. [1] Alkiviadis G. Akritas and Adam W. Strzebonski: A Comparative
Study of Two Real Root Isolation Methods. Nonlinear Analysis:
Modelling and Control, Vol. 10, No. 4, 297-304, 2005.
.. [2] Alkiviadis G. Akritas, Adam W. Strzebonski and Panagiotis S.
Vigklas: Improving the Performance of the Continued Fractions
Method Using New Bounds of Positive Roots.
Nonlinear Analysis: Modelling and Control, Vol. 13, No. 3, 265-279, 2008.
"""
if K.is_QQ:
(_, f), K = dup_clear_denoms(f, K, convert=True), K.get_ring()
elif not K.is_ZZ:
raise DomainError("isolation of real roots not supported over %s" % K)
if dup_degree(f) <= 0:
return []
I_zero, f = _isolate_zero(f, K, inf, sup, basis=basis, sqf=False)
_, factors = dup_sqf_list(f, K)
if len(factors) == 1:
((f, k),) = factors
I_neg = dup_inner_isolate_negative_roots(f, K, eps=eps, inf=inf, sup=sup, fast=fast)
I_pos = dup_inner_isolate_positive_roots(f, K, eps=eps, inf=inf, sup=sup, fast=fast)
I_neg = [ ((u, v), k) for u, v in I_neg ]
I_pos = [ ((u, v), k) for u, v in I_pos ]
else:
I_neg, I_pos = _real_isolate_and_disjoin(factors, K,
eps=eps, inf=inf, sup=sup, basis=basis, fast=fast)
return sorted(I_neg + I_zero + I_pos)
def dup_isolate_real_roots_list(polys, K, eps=None, inf=None, sup=None, strict=False, basis=False, fast=False):
"""Isolate real roots of a list of square-free polynomial using Vincent-Akritas-Strzebonski (VAS) CF approach.
References
==========
.. [1] Alkiviadis G. Akritas and Adam W. Strzebonski: A Comparative
Study of Two Real Root Isolation Methods. Nonlinear Analysis:
Modelling and Control, Vol. 10, No. 4, 297-304, 2005.
.. [2] Alkiviadis G. Akritas, Adam W. Strzebonski and Panagiotis S.
Vigklas: Improving the Performance of the Continued Fractions
Method Using New Bounds of Positive Roots.
Nonlinear Analysis: Modelling and Control, Vol. 13, No. 3, 265-279, 2008.
"""
if K.is_QQ:
K, F, polys = K.get_ring(), K, polys[:]
for i, p in enumerate(polys):
polys[i] = dup_clear_denoms(p, F, K, convert=True)[1]
elif not K.is_ZZ:
raise DomainError("isolation of real roots not supported over %s" % K)
zeros, factors_dict = False, {}
if (inf is None or inf <= 0) and (sup is None or 0 <= sup):
zeros, zero_indices = True, {}
for i, p in enumerate(polys):
j, p = dup_terms_gcd(p, K)
if zeros and j > 0:
zero_indices[i] = j
for f, k in dup_factor_list(p, K)[1]:
f = tuple(f)
if f not in factors_dict:
factors_dict[f] = {i: k}
else:
factors_dict[f][i] = k
factors_list = []
for f, indices in factors_dict.items():
factors_list.append((list(f), indices))
I_neg, I_pos = _real_isolate_and_disjoin(factors_list, K, eps=eps,
inf=inf, sup=sup, strict=strict, basis=basis, fast=fast)
F = K.get_field()
if not zeros or not zero_indices:
I_zero = []
else:
if not basis:
I_zero = [((F.zero, F.zero), zero_indices)]
else:
I_zero = [((F.zero, F.zero), zero_indices, [K.one, K.zero])]
return sorted(I_neg + I_zero + I_pos)
def _disjoint_p(M, N, strict=False):
"""Check if Mobius transforms define disjoint intervals. """
a1, b1, c1, d1 = M
a2, b2, c2, d2 = N
a1d1, b1c1 = a1*d1, b1*c1
a2d2, b2c2 = a2*d2, b2*c2
if a1d1 == b1c1 and a2d2 == b2c2:
return True
if a1d1 > b1c1:
a1, c1, b1, d1 = b1, d1, a1, c1
if a2d2 > b2c2:
a2, c2, b2, d2 = b2, d2, a2, c2
if not strict:
return a2*d1 >= c2*b1 or b2*c1 <= d2*a1
else:
return a2*d1 > c2*b1 or b2*c1 < d2*a1
def _real_isolate_and_disjoin(factors, K, eps=None, inf=None, sup=None, strict=False, basis=False, fast=False):
"""Isolate real roots of a list of polynomials and disjoin intervals. """
I_pos, I_neg = [], []
for i, (f, k) in enumerate(factors):
for F, M in dup_inner_isolate_positive_roots(f, K, eps=eps, inf=inf, sup=sup, fast=fast, mobius=True):
I_pos.append((F, M, k, f))
for G, N in dup_inner_isolate_negative_roots(f, K, eps=eps, inf=inf, sup=sup, fast=fast, mobius=True):
I_neg.append((G, N, k, f))
for i, (f, M, k, F) in enumerate(I_pos):
for j, (g, N, m, G) in enumerate(I_pos[i + 1:]):
while not _disjoint_p(M, N, strict=strict):
f, M = dup_inner_refine_real_root(f, M, K, steps=1, fast=fast, mobius=True)
g, N = dup_inner_refine_real_root(g, N, K, steps=1, fast=fast, mobius=True)
I_pos[i + j + 1] = (g, N, m, G)
I_pos[i] = (f, M, k, F)
for i, (f, M, k, F) in enumerate(I_neg):
for j, (g, N, m, G) in enumerate(I_neg[i + 1:]):
while not _disjoint_p(M, N, strict=strict):
f, M = dup_inner_refine_real_root(f, M, K, steps=1, fast=fast, mobius=True)
g, N = dup_inner_refine_real_root(g, N, K, steps=1, fast=fast, mobius=True)
I_neg[i + j + 1] = (g, N, m, G)
I_neg[i] = (f, M, k, F)
if strict:
for i, (f, M, k, F) in enumerate(I_neg):
if not M[0]:
while not M[0]:
f, M = dup_inner_refine_real_root(f, M, K, steps=1, fast=fast, mobius=True)
I_neg[i] = (f, M, k, F)
break
for j, (g, N, m, G) in enumerate(I_pos):
if not N[0]:
while not N[0]:
g, N = dup_inner_refine_real_root(g, N, K, steps=1, fast=fast, mobius=True)
I_pos[j] = (g, N, m, G)
break
field = K.get_field()
I_neg = [ (_mobius_to_interval(M, field), k, f) for (_, M, k, f) in I_neg ]
I_pos = [ (_mobius_to_interval(M, field), k, f) for (_, M, k, f) in I_pos ]
if not basis:
I_neg = [ ((-v, -u), k) for ((u, v), k, _) in I_neg ]
I_pos = [ (( u, v), k) for ((u, v), k, _) in I_pos ]
else:
I_neg = [ ((-v, -u), k, f) for ((u, v), k, f) in I_neg ]
I_pos = [ (( u, v), k, f) for ((u, v), k, f) in I_pos ]
return I_neg, I_pos
def dup_count_real_roots(f, K, inf=None, sup=None):
"""Returns the number of distinct real roots of ``f`` in ``[inf, sup]``. """
if dup_degree(f) <= 0:
return 0
if not K.is_Field:
R, K = K, K.get_field()
f = dup_convert(f, R, K)
sturm = dup_sturm(f, K)
if inf is None:
signs_inf = dup_sign_variations([ dup_LC(s, K)*(-1)**dup_degree(s) for s in sturm ], K)
else:
signs_inf = dup_sign_variations([ dup_eval(s, inf, K) for s in sturm ], K)
if sup is None:
signs_sup = dup_sign_variations([ dup_LC(s, K) for s in sturm ], K)
else:
signs_sup = dup_sign_variations([ dup_eval(s, sup, K) for s in sturm ], K)
count = abs(signs_inf - signs_sup)
if inf is not None and not dup_eval(f, inf, K):
count += 1
return count
OO = 'OO' # Origin of (re, im) coordinate system
Q1 = 'Q1' # Quadrant #1 (++): re > 0 and im > 0
Q2 = 'Q2' # Quadrant #2 (-+): re < 0 and im > 0
Q3 = 'Q3' # Quadrant #3 (--): re < 0 and im < 0
Q4 = 'Q4' # Quadrant #4 (+-): re > 0 and im < 0
A1 = 'A1' # Axis #1 (+0): re > 0 and im = 0
A2 = 'A2' # Axis #2 (0+): re = 0 and im > 0
A3 = 'A3' # Axis #3 (-0): re < 0 and im = 0
A4 = 'A4' # Axis #4 (0-): re = 0 and im < 0
_rules_simple = {
# Q --> Q (same) => no change
(Q1, Q1): 0,
(Q2, Q2): 0,
(Q3, Q3): 0,
(Q4, Q4): 0,
# A -- CCW --> Q => +1/4 (CCW)
(A1, Q1): 1,
(A2, Q2): 1,
(A3, Q3): 1,
(A4, Q4): 1,
# A -- CW --> Q => -1/4 (CCW)
(A1, Q4): 2,
(A2, Q1): 2,
(A3, Q2): 2,
(A4, Q3): 2,
# Q -- CCW --> A => +1/4 (CCW)
(Q1, A2): 3,
(Q2, A3): 3,
(Q3, A4): 3,
(Q4, A1): 3,
# Q -- CW --> A => -1/4 (CCW)
(Q1, A1): 4,
(Q2, A2): 4,
(Q3, A3): 4,
(Q4, A4): 4,
# Q -- CCW --> Q => +1/2 (CCW)
(Q1, Q2): +5,
(Q2, Q3): +5,
(Q3, Q4): +5,
(Q4, Q1): +5,
# Q -- CW --> Q => -1/2 (CW)
(Q1, Q4): -5,
(Q2, Q1): -5,
(Q3, Q2): -5,
(Q4, Q3): -5,
}
_rules_ambiguous = {
# A -- CCW --> Q => { +1/4 (CCW), -9/4 (CW) }
(A1, OO, Q1): -1,
(A2, OO, Q2): -1,
(A3, OO, Q3): -1,
(A4, OO, Q4): -1,
# A -- CW --> Q => { -1/4 (CCW), +7/4 (CW) }
(A1, OO, Q4): -2,
(A2, OO, Q1): -2,
(A3, OO, Q2): -2,
(A4, OO, Q3): -2,
# Q -- CCW --> A => { +1/4 (CCW), -9/4 (CW) }
(Q1, OO, A2): -3,
(Q2, OO, A3): -3,
(Q3, OO, A4): -3,
(Q4, OO, A1): -3,
# Q -- CW --> A => { -1/4 (CCW), +7/4 (CW) }
(Q1, OO, A1): -4,
(Q2, OO, A2): -4,
(Q3, OO, A3): -4,
(Q4, OO, A4): -4,
# A -- OO --> A => { +1 (CCW), -1 (CW) }
(A1, A3): 7,
(A2, A4): 7,
(A3, A1): 7,
(A4, A2): 7,
(A1, OO, A3): 7,
(A2, OO, A4): 7,
(A3, OO, A1): 7,
(A4, OO, A2): 7,
# Q -- DIA --> Q => { +1 (CCW), -1 (CW) }
(Q1, Q3): 8,
(Q2, Q4): 8,
(Q3, Q1): 8,
(Q4, Q2): 8,
(Q1, OO, Q3): 8,
(Q2, OO, Q4): 8,
(Q3, OO, Q1): 8,
(Q4, OO, Q2): 8,
# A --- R ---> A => { +1/2 (CCW), -3/2 (CW) }
(A1, A2): 9,
(A2, A3): 9,
(A3, A4): 9,
(A4, A1): 9,
(A1, OO, A2): 9,
(A2, OO, A3): 9,
(A3, OO, A4): 9,
(A4, OO, A1): 9,
# A --- L ---> A => { +3/2 (CCW), -1/2 (CW) }
(A1, A4): 10,
(A2, A1): 10,
(A3, A2): 10,
(A4, A3): 10,
(A1, OO, A4): 10,
(A2, OO, A1): 10,
(A3, OO, A2): 10,
(A4, OO, A3): 10,
# Q --- 1 ---> A => { +3/4 (CCW), -5/4 (CW) }
(Q1, A3): 11,
(Q2, A4): 11,
(Q3, A1): 11,
(Q4, A2): 11,
(Q1, OO, A3): 11,
(Q2, OO, A4): 11,
(Q3, OO, A1): 11,
(Q4, OO, A2): 11,
# Q --- 2 ---> A => { +5/4 (CCW), -3/4 (CW) }
(Q1, A4): 12,
(Q2, A1): 12,
(Q3, A2): 12,
(Q4, A3): 12,
(Q1, OO, A4): 12,
(Q2, OO, A1): 12,
(Q3, OO, A2): 12,
(Q4, OO, A3): 12,
# A --- 1 ---> Q => { +5/4 (CCW), -3/4 (CW) }
(A1, Q3): 13,
(A2, Q4): 13,
(A3, Q1): 13,
(A4, Q2): 13,
(A1, OO, Q3): 13,
(A2, OO, Q4): 13,
(A3, OO, Q1): 13,
(A4, OO, Q2): 13,
# A --- 2 ---> Q => { +3/4 (CCW), -5/4 (CW) }
(A1, Q2): 14,
(A2, Q3): 14,
(A3, Q4): 14,
(A4, Q1): 14,
(A1, OO, Q2): 14,
(A2, OO, Q3): 14,
(A3, OO, Q4): 14,
(A4, OO, Q1): 14,
# Q --> OO --> Q => { +1/2 (CCW), -3/2 (CW) }
(Q1, OO, Q2): 15,
(Q2, OO, Q3): 15,
(Q3, OO, Q4): 15,
(Q4, OO, Q1): 15,
# Q --> OO --> Q => { +3/2 (CCW), -1/2 (CW) }
(Q1, OO, Q4): 16,
(Q2, OO, Q1): 16,
(Q3, OO, Q2): 16,
(Q4, OO, Q3): 16,
# A --> OO --> A => { +2 (CCW), 0 (CW) }
(A1, OO, A1): 17,
(A2, OO, A2): 17,
(A3, OO, A3): 17,
(A4, OO, A4): 17,
# Q --> OO --> Q => { +2 (CCW), 0 (CW) }
(Q1, OO, Q1): 18,
(Q2, OO, Q2): 18,
(Q3, OO, Q3): 18,
(Q4, OO, Q4): 18,
}
_values = {
0: [( 0, 1)],
1: [(+1, 4)],
2: [(-1, 4)],
3: [(+1, 4)],
4: [(-1, 4)],
-1: [(+9, 4), (+1, 4)],
-2: [(+7, 4), (-1, 4)],
-3: [(+9, 4), (+1, 4)],
-4: [(+7, 4), (-1, 4)],
+5: [(+1, 2)],
-5: [(-1, 2)],
7: [(+1, 1), (-1, 1)],
8: [(+1, 1), (-1, 1)],
9: [(+1, 2), (-3, 2)],
10: [(+3, 2), (-1, 2)],
11: [(+3, 4), (-5, 4)],
12: [(+5, 4), (-3, 4)],
13: [(+5, 4), (-3, 4)],
14: [(+3, 4), (-5, 4)],
15: [(+1, 2), (-3, 2)],
16: [(+3, 2), (-1, 2)],
17: [(+2, 1), ( 0, 1)],
18: [(+2, 1), ( 0, 1)],
}
def _classify_point(re, im):
"""Return the half-axis (or origin) on which (re, im) point is located. """
if not re and not im:
return OO
if not re:
if im > 0:
return A2
else:
return A4
elif not im:
if re > 0:
return A1
else:
return A3
def _intervals_to_quadrants(intervals, f1, f2, s, t, F):
"""Generate a sequence of extended quadrants from a list of critical points. """
if not intervals:
return []
Q = []
if not f1:
(a, b), _, _ = intervals[0]
if a == b == s:
if len(intervals) == 1:
if dup_eval(f2, t, F) > 0:
return [OO, A2]
else:
return [OO, A4]
else:
(a, _), _, _ = intervals[1]
if dup_eval(f2, (s + a)/2, F) > 0:
Q.extend([OO, A2])
f2_sgn = +1
else:
Q.extend([OO, A4])
f2_sgn = -1
intervals = intervals[1:]
else:
if dup_eval(f2, s, F) > 0:
Q.append(A2)
f2_sgn = +1
else:
Q.append(A4)
f2_sgn = -1
for (a, _), indices, _ in intervals:
Q.append(OO)
if indices[1] % 2 == 1:
f2_sgn = -f2_sgn
if a != t:
if f2_sgn > 0:
Q.append(A2)
else:
Q.append(A4)
return Q
if not f2:
(a, b), _, _ = intervals[0]
if a == b == s:
if len(intervals) == 1:
if dup_eval(f1, t, F) > 0:
return [OO, A1]
else:
return [OO, A3]
else:
(a, _), _, _ = intervals[1]
if dup_eval(f1, (s + a)/2, F) > 0:
Q.extend([OO, A1])
f1_sgn = +1
else:
Q.extend([OO, A3])
f1_sgn = -1
intervals = intervals[1:]
else:
if dup_eval(f1, s, F) > 0:
Q.append(A1)
f1_sgn = +1
else:
Q.append(A3)
f1_sgn = -1
for (a, _), indices, _ in intervals:
Q.append(OO)
if indices[0] % 2 == 1:
f1_sgn = -f1_sgn
if a != t:
if f1_sgn > 0:
Q.append(A1)
else:
Q.append(A3)
return Q
re = dup_eval(f1, s, F)
im = dup_eval(f2, s, F)
if not re or not im:
Q.append(_classify_point(re, im))
if len(intervals) == 1:
re = dup_eval(f1, t, F)
im = dup_eval(f2, t, F)
else:
(a, _), _, _ = intervals[1]
re = dup_eval(f1, (s + a)/2, F)
im = dup_eval(f2, (s + a)/2, F)
intervals = intervals[1:]
if re > 0:
f1_sgn = +1
else:
f1_sgn = -1
if im > 0:
f2_sgn = +1
else:
f2_sgn = -1
sgn = {
(+1, +1): Q1,
(-1, +1): Q2,
(-1, -1): Q3,
(+1, -1): Q4,
}
Q.append(sgn[(f1_sgn, f2_sgn)])
for (a, b), indices, _ in intervals:
if a == b:
re = dup_eval(f1, a, F)
im = dup_eval(f2, a, F)
cls = _classify_point(re, im)
if cls is not None:
Q.append(cls)
if 0 in indices:
if indices[0] % 2 == 1:
f1_sgn = -f1_sgn
if 1 in indices:
if indices[1] % 2 == 1:
f2_sgn = -f2_sgn
if not (a == b and b == t):
Q.append(sgn[(f1_sgn, f2_sgn)])
return Q
def _traverse_quadrants(Q_L1, Q_L2, Q_L3, Q_L4, exclude=None):
"""Transform sequences of quadrants to a sequence of rules. """
if exclude is True:
edges = [1, 1, 0, 0]
corners = {
(0, 1): 1,
(1, 2): 1,
(2, 3): 0,
(3, 0): 1,
}
else:
edges = [0, 0, 0, 0]
corners = {
(0, 1): 0,
(1, 2): 0,
(2, 3): 0,
(3, 0): 0,
}
if exclude is not None and exclude is not True:
exclude = set(exclude)
for i, edge in enumerate(['S', 'E', 'N', 'W']):
if edge in exclude:
edges[i] = 1
for i, corner in enumerate(['SW', 'SE', 'NE', 'NW']):
if corner in exclude:
corners[((i - 1) % 4, i)] = 1
QQ, rules = [Q_L1, Q_L2, Q_L3, Q_L4], []
for i, Q in enumerate(QQ):
if not Q:
continue
if Q[-1] == OO:
Q = Q[:-1]
if Q[0] == OO:
j, Q = (i - 1) % 4, Q[1:]
qq = (QQ[j][-2], OO, Q[0])
if qq in _rules_ambiguous:
rules.append((_rules_ambiguous[qq], corners[(j, i)]))
else:
raise NotImplementedError("3 element rule (corner): " + str(qq))
q1, k = Q[0], 1
while k < len(Q):
q2, k = Q[k], k + 1
if q2 != OO:
qq = (q1, q2)
if qq in _rules_simple:
rules.append((_rules_simple[qq], 0))
elif qq in _rules_ambiguous:
rules.append((_rules_ambiguous[qq], edges[i]))
else:
raise NotImplementedError("2 element rule (inside): " + str(qq))
else:
qq, k = (q1, q2, Q[k]), k + 1
if qq in _rules_ambiguous:
rules.append((_rules_ambiguous[qq], edges[i]))
else:
raise NotImplementedError("3 element rule (edge): " + str(qq))
q1 = qq[-1]
return rules
def _reverse_intervals(intervals):
"""Reverse intervals for traversal from right to left and from top to bottom. """
return [ ((b, a), indices, f) for (a, b), indices, f in reversed(intervals) ]
def _winding_number(T, field):
"""Compute the winding number of the input polynomial, i.e. the number of roots. """
return int(sum([ field(*_values[t][i]) for t, i in T ]) / field(2))
def dup_count_complex_roots(f, K, inf=None, sup=None, exclude=None):
"""Count all roots in [u + v*I, s + t*I] rectangle using Collins-Krandick algorithm. """
if not K.is_ZZ and not K.is_QQ:
raise DomainError("complex root counting is not supported over %s" % K)
if K.is_ZZ:
R, F = K, K.get_field()
else:
R, F = K.get_ring(), K
f = dup_convert(f, K, F)
if inf is None or sup is None:
_, lc = dup_degree(f), abs(dup_LC(f, F))
B = 2*max([ F.quo(abs(c), lc) for c in f ])
if inf is None:
(u, v) = (-B, -B)
else:
(u, v) = inf
if sup is None:
(s, t) = (+B, +B)
else:
(s, t) = sup
f1, f2 = dup_real_imag(f, F)
f1L1F = dmp_eval_in(f1, v, 1, 1, F)
f2L1F = dmp_eval_in(f2, v, 1, 1, F)
_, f1L1R = dup_clear_denoms(f1L1F, F, R, convert=True)
_, f2L1R = dup_clear_denoms(f2L1F, F, R, convert=True)
f1L2F = dmp_eval_in(f1, s, 0, 1, F)
f2L2F = dmp_eval_in(f2, s, 0, 1, F)
_, f1L2R = dup_clear_denoms(f1L2F, F, R, convert=True)
_, f2L2R = dup_clear_denoms(f2L2F, F, R, convert=True)
f1L3F = dmp_eval_in(f1, t, 1, 1, F)
f2L3F = dmp_eval_in(f2, t, 1, 1, F)
_, f1L3R = dup_clear_denoms(f1L3F, F, R, convert=True)
_, f2L3R = dup_clear_denoms(f2L3F, F, R, convert=True)
f1L4F = dmp_eval_in(f1, u, 0, 1, F)
f2L4F = dmp_eval_in(f2, u, 0, 1, F)
_, f1L4R = dup_clear_denoms(f1L4F, F, R, convert=True)
_, f2L4R = dup_clear_denoms(f2L4F, F, R, convert=True)
S_L1 = [f1L1R, f2L1R]
S_L2 = [f1L2R, f2L2R]
S_L3 = [f1L3R, f2L3R]
S_L4 = [f1L4R, f2L4R]
I_L1 = dup_isolate_real_roots_list(S_L1, R, inf=u, sup=s, fast=True, basis=True, strict=True)
I_L2 = dup_isolate_real_roots_list(S_L2, R, inf=v, sup=t, fast=True, basis=True, strict=True)
I_L3 = dup_isolate_real_roots_list(S_L3, R, inf=u, sup=s, fast=True, basis=True, strict=True)
I_L4 = dup_isolate_real_roots_list(S_L4, R, inf=v, sup=t, fast=True, basis=True, strict=True)
I_L3 = _reverse_intervals(I_L3)
I_L4 = _reverse_intervals(I_L4)
Q_L1 = _intervals_to_quadrants(I_L1, f1L1F, f2L1F, u, s, F)
Q_L2 = _intervals_to_quadrants(I_L2, f1L2F, f2L2F, v, t, F)
Q_L3 = _intervals_to_quadrants(I_L3, f1L3F, f2L3F, s, u, F)
Q_L4 = _intervals_to_quadrants(I_L4, f1L4F, f2L4F, t, v, F)
T = _traverse_quadrants(Q_L1, Q_L2, Q_L3, Q_L4, exclude=exclude)
return _winding_number(T, F)
def _vertical_bisection(N, a, b, I, Q, F1, F2, f1, f2, F):
"""Vertical bisection step in Collins-Krandick root isolation algorithm. """
(u, v), (s, t) = a, b
I_L1, I_L2, I_L3, I_L4 = I
Q_L1, Q_L2, Q_L3, Q_L4 = Q
f1L1F, f1L2F, f1L3F, f1L4F = F1
f2L1F, f2L2F, f2L3F, f2L4F = F2
x = (u + s) / 2
f1V = dmp_eval_in(f1, x, 0, 1, F)
f2V = dmp_eval_in(f2, x, 0, 1, F)
I_V = dup_isolate_real_roots_list([f1V, f2V], F, inf=v, sup=t, fast=True, strict=True, basis=True)
I_L1_L, I_L1_R = [], []
I_L2_L, I_L2_R = I_V, I_L2
I_L3_L, I_L3_R = [], []
I_L4_L, I_L4_R = I_L4, _reverse_intervals(I_V)
for I in I_L1:
(a, b), indices, h = I
if a == b:
if a == x:
I_L1_L.append(I)
I_L1_R.append(I)
elif a < x:
I_L1_L.append(I)
else:
I_L1_R.append(I)
else:
if b <= x:
I_L1_L.append(I)
elif a >= x:
I_L1_R.append(I)
else:
a, b = dup_refine_real_root(h, a, b, F.get_ring(), disjoint=x, fast=True)
if b <= x:
I_L1_L.append(((a, b), indices, h))
if a >= x:
I_L1_R.append(((a, b), indices, h))
for I in I_L3:
(b, a), indices, h = I
if a == b:
if a == x:
I_L3_L.append(I)
I_L3_R.append(I)
elif a < x:
I_L3_L.append(I)
else:
I_L3_R.append(I)
else:
if b <= x:
I_L3_L.append(I)
elif a >= x:
I_L3_R.append(I)
else:
a, b = dup_refine_real_root(h, a, b, F.get_ring(), disjoint=x, fast=True)
if b <= x:
I_L3_L.append(((b, a), indices, h))
if a >= x:
I_L3_R.append(((b, a), indices, h))
Q_L1_L = _intervals_to_quadrants(I_L1_L, f1L1F, f2L1F, u, x, F)
Q_L2_L = _intervals_to_quadrants(I_L2_L, f1V, f2V, v, t, F)
Q_L3_L = _intervals_to_quadrants(I_L3_L, f1L3F, f2L3F, x, u, F)
Q_L4_L = Q_L4
Q_L1_R = _intervals_to_quadrants(I_L1_R, f1L1F, f2L1F, x, s, F)
Q_L2_R = Q_L2
Q_L3_R = _intervals_to_quadrants(I_L3_R, f1L3F, f2L3F, s, x, F)
Q_L4_R = _intervals_to_quadrants(I_L4_R, f1V, f2V, t, v, F)
T_L = _traverse_quadrants(Q_L1_L, Q_L2_L, Q_L3_L, Q_L4_L, exclude=True)
T_R = _traverse_quadrants(Q_L1_R, Q_L2_R, Q_L3_R, Q_L4_R, exclude=True)
N_L = _winding_number(T_L, F)
N_R = _winding_number(T_R, F)
I_L = (I_L1_L, I_L2_L, I_L3_L, I_L4_L)
Q_L = (Q_L1_L, Q_L2_L, Q_L3_L, Q_L4_L)
I_R = (I_L1_R, I_L2_R, I_L3_R, I_L4_R)
Q_R = (Q_L1_R, Q_L2_R, Q_L3_R, Q_L4_R)
F1_L = (f1L1F, f1V, f1L3F, f1L4F)
F2_L = (f2L1F, f2V, f2L3F, f2L4F)
F1_R = (f1L1F, f1L2F, f1L3F, f1V)
F2_R = (f2L1F, f2L2F, f2L3F, f2V)
a, b = (u, v), (x, t)
c, d = (x, v), (s, t)
D_L = (N_L, a, b, I_L, Q_L, F1_L, F2_L)
D_R = (N_R, c, d, I_R, Q_R, F1_R, F2_R)
return D_L, D_R
def _horizontal_bisection(N, a, b, I, Q, F1, F2, f1, f2, F):
"""Horizontal bisection step in Collins-Krandick root isolation algorithm. """
(u, v), (s, t) = a, b
I_L1, I_L2, I_L3, I_L4 = I
Q_L1, Q_L2, Q_L3, Q_L4 = Q
f1L1F, f1L2F, f1L3F, f1L4F = F1
f2L1F, f2L2F, f2L3F, f2L4F = F2
y = (v + t) / 2
f1H = dmp_eval_in(f1, y, 1, 1, F)
f2H = dmp_eval_in(f2, y, 1, 1, F)
I_H = dup_isolate_real_roots_list([f1H, f2H], F, inf=u, sup=s, fast=True, strict=True, basis=True)
I_L1_B, I_L1_U = I_L1, I_H
I_L2_B, I_L2_U = [], []
I_L3_B, I_L3_U = _reverse_intervals(I_H), I_L3
I_L4_B, I_L4_U = [], []
for I in I_L2:
(a, b), indices, h = I
if a == b:
if a == y:
I_L2_B.append(I)
I_L2_U.append(I)
elif a < y:
I_L2_B.append(I)
else:
I_L2_U.append(I)
else:
if b <= y:
I_L2_B.append(I)
elif a >= y:
I_L2_U.append(I)
else:
a, b = dup_refine_real_root(h, a, b, F.get_ring(), disjoint=y, fast=True)
if b <= y:
I_L2_B.append(((a, b), indices, h))
if a >= y:
I_L2_U.append(((a, b), indices, h))
for I in I_L4:
(b, a), indices, h = I
if a == b:
if a == y:
I_L4_B.append(I)
I_L4_U.append(I)
elif a < y:
I_L4_B.append(I)
else:
I_L4_U.append(I)
else:
if b <= y:
I_L4_B.append(I)
elif a >= y:
I_L4_U.append(I)
else:
a, b = dup_refine_real_root(h, a, b, F.get_ring(), disjoint=y, fast=True)
if b <= y:
I_L4_B.append(((b, a), indices, h))
if a >= y:
I_L4_U.append(((b, a), indices, h))
Q_L1_B = Q_L1
Q_L2_B = _intervals_to_quadrants(I_L2_B, f1L2F, f2L2F, v, y, F)
Q_L3_B = _intervals_to_quadrants(I_L3_B, f1H, f2H, s, u, F)
Q_L4_B = _intervals_to_quadrants(I_L4_B, f1L4F, f2L4F, y, v, F)
Q_L1_U = _intervals_to_quadrants(I_L1_U, f1H, f2H, u, s, F)
Q_L2_U = _intervals_to_quadrants(I_L2_U, f1L2F, f2L2F, y, t, F)
Q_L3_U = Q_L3
Q_L4_U = _intervals_to_quadrants(I_L4_U, f1L4F, f2L4F, t, y, F)
T_B = _traverse_quadrants(Q_L1_B, Q_L2_B, Q_L3_B, Q_L4_B, exclude=True)
T_U = _traverse_quadrants(Q_L1_U, Q_L2_U, Q_L3_U, Q_L4_U, exclude=True)
N_B = _winding_number(T_B, F)
N_U = _winding_number(T_U, F)
I_B = (I_L1_B, I_L2_B, I_L3_B, I_L4_B)
Q_B = (Q_L1_B, Q_L2_B, Q_L3_B, Q_L4_B)
I_U = (I_L1_U, I_L2_U, I_L3_U, I_L4_U)
Q_U = (Q_L1_U, Q_L2_U, Q_L3_U, Q_L4_U)
F1_B = (f1L1F, f1L2F, f1H, f1L4F)
F2_B = (f2L1F, f2L2F, f2H, f2L4F)
F1_U = (f1H, f1L2F, f1L3F, f1L4F)
F2_U = (f2H, f2L2F, f2L3F, f2L4F)
a, b = (u, v), (s, y)
c, d = (u, y), (s, t)
D_B = (N_B, a, b, I_B, Q_B, F1_B, F2_B)
D_U = (N_U, c, d, I_U, Q_U, F1_U, F2_U)
return D_B, D_U
def _depth_first_select(rectangles):
"""Find a rectangle of minimum area for bisection. """
min_area, j = None, None
for i, (_, (u, v), (s, t), _, _, _, _) in enumerate(rectangles):
area = (s - u)*(t - v)
if min_area is None or area < min_area:
min_area, j = area, i
return rectangles.pop(j)
def _rectangle_small_p(a, b, eps):
"""Return ``True`` if the given rectangle is small enough. """
(u, v), (s, t) = a, b
if eps is not None:
return s - u < eps and t - v < eps
else:
return True
def dup_isolate_complex_roots_sqf(f, K, eps=None, inf=None, sup=None, blackbox=False):
"""Isolate complex roots of a square-free polynomial using Collins-Krandick algorithm. """
if not K.is_ZZ and not K.is_QQ:
raise DomainError("isolation of complex roots is not supported over %s" % K)
if dup_degree(f) <= 0:
return []
if K.is_ZZ:
F = K.get_field()
else:
F = K
f = dup_convert(f, K, F)
lc = abs(dup_LC(f, F))
B = 2*max([ F.quo(abs(c), lc) for c in f ])
(u, v), (s, t) = (-B, F.zero), (B, B)
if inf is not None:
u = inf
if sup is not None:
s = sup
if v < 0 or t <= v or s <= u:
raise ValueError("not a valid complex isolation rectangle")
f1, f2 = dup_real_imag(f, F)
f1L1 = dmp_eval_in(f1, v, 1, 1, F)
f2L1 = dmp_eval_in(f2, v, 1, 1, F)
f1L2 = dmp_eval_in(f1, s, 0, 1, F)
f2L2 = dmp_eval_in(f2, s, 0, 1, F)
f1L3 = dmp_eval_in(f1, t, 1, 1, F)
f2L3 = dmp_eval_in(f2, t, 1, 1, F)
f1L4 = dmp_eval_in(f1, u, 0, 1, F)
f2L4 = dmp_eval_in(f2, u, 0, 1, F)
S_L1 = [f1L1, f2L1]
S_L2 = [f1L2, f2L2]
S_L3 = [f1L3, f2L3]
S_L4 = [f1L4, f2L4]
I_L1 = dup_isolate_real_roots_list(S_L1, F, inf=u, sup=s, fast=True, strict=True, basis=True)
I_L2 = dup_isolate_real_roots_list(S_L2, F, inf=v, sup=t, fast=True, strict=True, basis=True)
I_L3 = dup_isolate_real_roots_list(S_L3, F, inf=u, sup=s, fast=True, strict=True, basis=True)
I_L4 = dup_isolate_real_roots_list(S_L4, F, inf=v, sup=t, fast=True, strict=True, basis=True)
I_L3 = _reverse_intervals(I_L3)
I_L4 = _reverse_intervals(I_L4)
Q_L1 = _intervals_to_quadrants(I_L1, f1L1, f2L1, u, s, F)
Q_L2 = _intervals_to_quadrants(I_L2, f1L2, f2L2, v, t, F)
Q_L3 = _intervals_to_quadrants(I_L3, f1L3, f2L3, s, u, F)
Q_L4 = _intervals_to_quadrants(I_L4, f1L4, f2L4, t, v, F)
T = _traverse_quadrants(Q_L1, Q_L2, Q_L3, Q_L4)
N = _winding_number(T, F)
if not N:
return []
I = (I_L1, I_L2, I_L3, I_L4)
Q = (Q_L1, Q_L2, Q_L3, Q_L4)
F1 = (f1L1, f1L2, f1L3, f1L4)
F2 = (f2L1, f2L2, f2L3, f2L4)
rectangles, roots = [(N, (u, v), (s, t), I, Q, F1, F2)], []
while rectangles:
N, (u, v), (s, t), I, Q, F1, F2 = _depth_first_select(rectangles)
if s - u > t - v:
D_L, D_R = _vertical_bisection(N, (u, v), (s, t), I, Q, F1, F2, f1, f2, F)
N_L, a, b, I_L, Q_L, F1_L, F2_L = D_L
N_R, c, d, I_R, Q_R, F1_R, F2_R = D_R
if N_L >= 1:
if N_L == 1 and _rectangle_small_p(a, b, eps):
roots.append(ComplexInterval(a, b, I_L, Q_L, F1_L, F2_L, f1, f2, F))
else:
rectangles.append(D_L)
if N_R >= 1:
if N_R == 1 and _rectangle_small_p(c, d, eps):
roots.append(ComplexInterval(c, d, I_R, Q_R, F1_R, F2_R, f1, f2, F))
else:
rectangles.append(D_R)
else:
D_B, D_U = _horizontal_bisection(N, (u, v), (s, t), I, Q, F1, F2, f1, f2, F)
N_B, a, b, I_B, Q_B, F1_B, F2_B = D_B
N_U, c, d, I_U, Q_U, F1_U, F2_U = D_U
if N_B >= 1:
if N_B == 1 and _rectangle_small_p(a, b, eps):
roots.append(ComplexInterval(
a, b, I_B, Q_B, F1_B, F2_B, f1, f2, F))
else:
rectangles.append(D_B)
if N_U >= 1:
if N_U == 1 and _rectangle_small_p(c, d, eps):
roots.append(ComplexInterval(
c, d, I_U, Q_U, F1_U, F2_U, f1, f2, F))
else:
rectangles.append(D_U)
_roots, roots = sorted(roots, key=lambda r: (r.ax, r.ay)), []
for root in _roots:
roots.extend([root.conjugate(), root])
if blackbox:
return roots
else:
return [ r.as_tuple() for r in roots ]
def dup_isolate_all_roots_sqf(f, K, eps=None, inf=None, sup=None, fast=False, blackbox=False):
"""Isolate real and complex roots of a square-free polynomial ``f``. """
return (
dup_isolate_real_roots_sqf( f, K, eps=eps, inf=inf, sup=sup, fast=fast, blackbox=blackbox),
dup_isolate_complex_roots_sqf(f, K, eps=eps, inf=inf, sup=sup, blackbox=blackbox))
def dup_isolate_all_roots(f, K, eps=None, inf=None, sup=None, fast=False):
"""Isolate real and complex roots of a non-square-free polynomial ``f``. """
if not K.is_ZZ and not K.is_QQ:
raise DomainError("isolation of real and complex roots is not supported over %s" % K)
_, factors = dup_sqf_list(f, K)
if len(factors) == 1:
((f, k),) = factors
real_part, complex_part = dup_isolate_all_roots_sqf(
f, K, eps=eps, inf=inf, sup=sup, fast=fast)
real_part = [ ((a, b), k) for (a, b) in real_part ]
complex_part = [ ((a, b), k) for (a, b) in complex_part ]
return real_part, complex_part
else:
raise NotImplementedError( "only trivial square-free polynomials are supported")
class RealInterval:
"""A fully qualified representation of a real isolation interval. """
def __init__(self, data, f, dom):
"""Initialize new real interval with complete information. """
if len(data) == 2:
s, t = data
self.neg = False
if s < 0:
if t <= 0:
f, s, t, self.neg = dup_mirror(f, dom), -t, -s, True
else:
raise ValueError("Cannot refine a real root in (%s, %s)" % (s, t))
a, b, c, d = _mobius_from_interval((s, t), dom.get_field())
f = dup_transform(f, dup_strip([a, b]),
dup_strip([c, d]), dom)
self.mobius = a, b, c, d
else:
self.mobius = data[:-1]
self.neg = data[-1]
self.f, self.dom = f, dom
@property
def func(self):
return RealInterval
@property
def args(self):
i = self
return (i.mobius + (i.neg,), i.f, i.dom)
def __eq__(self, other):
if type(other) is not type(self):
return False
return self.args == other.args
@property
def a(self):
"""Return the position of the left end. """
field = self.dom.get_field()
a, b, c, d = self.mobius
if not self.neg:
if a*d < b*c:
return field(a, c)
return field(b, d)
else:
if a*d > b*c:
return -field(a, c)
return -field(b, d)
@property
def b(self):
"""Return the position of the right end. """
was = self.neg
self.neg = not was
rv = -self.a
self.neg = was
return rv
@property
def dx(self):
"""Return width of the real isolating interval. """
return self.b - self.a
@property
def center(self):
"""Return the center of the real isolating interval. """
return (self.a + self.b)/2
@property
def max_denom(self):
"""Return the largest denominator occurring in either endpoint. """
return max(self.a.denominator, self.b.denominator)
def as_tuple(self):
"""Return tuple representation of real isolating interval. """
return (self.a, self.b)
def __repr__(self):
return "(%s, %s)" % (self.a, self.b)
def __contains__(self, item):
"""
Say whether a complex number belongs to this real interval.
Parameters
==========
item : pair (re, im) or number re
Either a pair giving the real and imaginary parts of the number,
or else a real number.
"""
if isinstance(item, tuple):
re, im = item
else:
re, im = item, 0
return im == 0 and self.a <= re <= self.b
def is_disjoint(self, other):
"""Return ``True`` if two isolation intervals are disjoint. """
if isinstance(other, RealInterval):
return (self.b < other.a or other.b < self.a)
assert isinstance(other, ComplexInterval)
return (self.b < other.ax or other.bx < self.a
or other.ay*other.by > 0)
def _inner_refine(self):
"""Internal one step real root refinement procedure. """
if self.mobius is None:
return self
f, mobius = dup_inner_refine_real_root(
self.f, self.mobius, self.dom, steps=1, mobius=True)
return RealInterval(mobius + (self.neg,), f, self.dom)
def refine_disjoint(self, other):
"""Refine an isolating interval until it is disjoint with another one. """
expr = self
while not expr.is_disjoint(other):
expr, other = expr._inner_refine(), other._inner_refine()
return expr, other
def refine_size(self, dx):
"""Refine an isolating interval until it is of sufficiently small size. """
expr = self
while not (expr.dx < dx):
expr = expr._inner_refine()
return expr
def refine_step(self, steps=1):
"""Perform several steps of real root refinement algorithm. """
expr = self
for _ in range(steps):
expr = expr._inner_refine()
return expr
def refine(self):
"""Perform one step of real root refinement algorithm. """
return self._inner_refine()
class ComplexInterval:
"""A fully qualified representation of a complex isolation interval.
The printed form is shown as (ax, bx) x (ay, by) where (ax, ay)
and (bx, by) are the coordinates of the southwest and northeast
corners of the interval's rectangle, respectively.
Examples
========
>>> from sympy import CRootOf, S
>>> from sympy.abc import x
>>> CRootOf.clear_cache() # for doctest reproducibility
>>> root = CRootOf(x**10 - 2*x + 3, 9)
>>> i = root._get_interval(); i
(3/64, 3/32) x (9/8, 75/64)
The real part of the root lies within the range [0, 3/4] while
the imaginary part lies within the range [9/8, 3/2]:
>>> root.n(3)
0.0766 + 1.14*I
The width of the ranges in the x and y directions on the complex
plane are:
>>> i.dx, i.dy
(3/64, 3/64)
The center of the range is
>>> i.center
(9/128, 147/128)
The northeast coordinate of the rectangle bounding the root in the
complex plane is given by attribute b and the x and y components
are accessed by bx and by:
>>> i.b, i.bx, i.by
((3/32, 75/64), 3/32, 75/64)
The southwest coordinate is similarly given by i.a
>>> i.a, i.ax, i.ay
((3/64, 9/8), 3/64, 9/8)
Although the interval prints to show only the real and imaginary
range of the root, all the information of the underlying root
is contained as properties of the interval.
For example, an interval with a nonpositive imaginary range is
considered to be the conjugate. Since the y values of y are in the
range [0, 1/4] it is not the conjugate:
>>> i.conj
False
The conjugate's interval is
>>> ic = i.conjugate(); ic
(3/64, 3/32) x (-75/64, -9/8)
NOTE: the values printed still represent the x and y range
in which the root -- conjugate, in this case -- is located,
but the underlying a and b values of a root and its conjugate
are the same:
>>> assert i.a == ic.a and i.b == ic.b
What changes are the reported coordinates of the bounding rectangle:
>>> (i.ax, i.ay), (i.bx, i.by)
((3/64, 9/8), (3/32, 75/64))
>>> (ic.ax, ic.ay), (ic.bx, ic.by)
((3/64, -75/64), (3/32, -9/8))
The interval can be refined once:
>>> i # for reference, this is the current interval
(3/64, 3/32) x (9/8, 75/64)
>>> i.refine()
(3/64, 3/32) x (9/8, 147/128)
Several refinement steps can be taken:
>>> i.refine_step(2) # 2 steps
(9/128, 3/32) x (9/8, 147/128)
It is also possible to refine to a given tolerance:
>>> tol = min(i.dx, i.dy)/2
>>> i.refine_size(tol)
(9/128, 21/256) x (9/8, 291/256)
A disjoint interval is one whose bounding rectangle does not
overlap with another. An interval, necessarily, is not disjoint with
itself, but any interval is disjoint with a conjugate since the
conjugate rectangle will always be in the lower half of the complex
plane and the non-conjugate in the upper half:
>>> i.is_disjoint(i), i.is_disjoint(i.conjugate())
(False, True)
The following interval j is not disjoint from i:
>>> close = CRootOf(x**10 - 2*x + 300/S(101), 9)
>>> j = close._get_interval(); j
(75/1616, 75/808) x (225/202, 1875/1616)
>>> i.is_disjoint(j)
False
The two can be made disjoint, however:
>>> newi, newj = i.refine_disjoint(j)
>>> newi
(39/512, 159/2048) x (2325/2048, 4653/4096)
>>> newj
(3975/51712, 2025/25856) x (29325/25856, 117375/103424)
Even though the real ranges overlap, the imaginary do not, so
the roots have been resolved as distinct. Intervals are disjoint
when either the real or imaginary component of the intervals is
distinct. In the case above, the real components have not been
resolved (so we do not know, yet, which root has the smaller real
part) but the imaginary part of ``close`` is larger than ``root``:
>>> close.n(3)
0.0771 + 1.13*I
>>> root.n(3)
0.0766 + 1.14*I
"""
def __init__(self, a, b, I, Q, F1, F2, f1, f2, dom, conj=False):
"""Initialize new complex interval with complete information. """
# a and b are the SW and NE corner of the bounding interval,
# (ax, ay) and (bx, by), respectively, for the NON-CONJUGATE
# root (the one with the positive imaginary part); when working
# with the conjugate, the a and b value are still non-negative
# but the ay, by are reversed and have oppositite sign
self.a, self.b = a, b
self.I, self.Q = I, Q
self.f1, self.F1 = f1, F1
self.f2, self.F2 = f2, F2
self.dom = dom
self.conj = conj
@property
def func(self):
return ComplexInterval
@property
def args(self):
i = self
return (i.a, i.b, i.I, i.Q, i.F1, i.F2, i.f1, i.f2, i.dom, i.conj)
def __eq__(self, other):
if type(other) is not type(self):
return False
return self.args == other.args
@property
def ax(self):
"""Return ``x`` coordinate of south-western corner. """
return self.a[0]
@property
def ay(self):
"""Return ``y`` coordinate of south-western corner. """
if not self.conj:
return self.a[1]
else:
return -self.b[1]
@property
def bx(self):
"""Return ``x`` coordinate of north-eastern corner. """
return self.b[0]
@property
def by(self):
"""Return ``y`` coordinate of north-eastern corner. """
if not self.conj:
return self.b[1]
else:
return -self.a[1]
@property
def dx(self):
"""Return width of the complex isolating interval. """
return self.b[0] - self.a[0]
@property
def dy(self):
"""Return height of the complex isolating interval. """
return self.b[1] - self.a[1]
@property
def center(self):
"""Return the center of the complex isolating interval. """
return ((self.ax + self.bx)/2, (self.ay + self.by)/2)
@property
def max_denom(self):
"""Return the largest denominator occurring in either endpoint. """
return max(self.ax.denominator, self.bx.denominator,
self.ay.denominator, self.by.denominator)
def as_tuple(self):
"""Return tuple representation of the complex isolating
interval's SW and NE corners, respectively. """
return ((self.ax, self.ay), (self.bx, self.by))
def __repr__(self):
return "(%s, %s) x (%s, %s)" % (self.ax, self.bx, self.ay, self.by)
def conjugate(self):
"""This complex interval really is located in lower half-plane. """
return ComplexInterval(self.a, self.b, self.I, self.Q,
self.F1, self.F2, self.f1, self.f2, self.dom, conj=True)
def __contains__(self, item):
"""
Say whether a complex number belongs to this complex rectangular
region.
Parameters
==========
item : pair (re, im) or number re
Either a pair giving the real and imaginary parts of the number,
or else a real number.
"""
if isinstance(item, tuple):
re, im = item
else:
re, im = item, 0
return self.ax <= re <= self.bx and self.ay <= im <= self.by
def is_disjoint(self, other):
"""Return ``True`` if two isolation intervals are disjoint. """
if isinstance(other, RealInterval):
return other.is_disjoint(self)
if self.conj != other.conj: # above and below real axis
return True
re_distinct = (self.bx < other.ax or other.bx < self.ax)
if re_distinct:
return True
im_distinct = (self.by < other.ay or other.by < self.ay)
return im_distinct
def _inner_refine(self):
"""Internal one step complex root refinement procedure. """
(u, v), (s, t) = self.a, self.b
I, Q = self.I, self.Q
f1, F1 = self.f1, self.F1
f2, F2 = self.f2, self.F2
dom = self.dom
if s - u > t - v:
D_L, D_R = _vertical_bisection(1, (u, v), (s, t), I, Q, F1, F2, f1, f2, dom)
if D_L[0] == 1:
_, a, b, I, Q, F1, F2 = D_L
else:
_, a, b, I, Q, F1, F2 = D_R
else:
D_B, D_U = _horizontal_bisection(1, (u, v), (s, t), I, Q, F1, F2, f1, f2, dom)
if D_B[0] == 1:
_, a, b, I, Q, F1, F2 = D_B
else:
_, a, b, I, Q, F1, F2 = D_U
return ComplexInterval(a, b, I, Q, F1, F2, f1, f2, dom, self.conj)
def refine_disjoint(self, other):
"""Refine an isolating interval until it is disjoint with another one. """
expr = self
while not expr.is_disjoint(other):
expr, other = expr._inner_refine(), other._inner_refine()
return expr, other
def refine_size(self, dx, dy=None):
"""Refine an isolating interval until it is of sufficiently small size. """
if dy is None:
dy = dx
expr = self
while not (expr.dx < dx and expr.dy < dy):
expr = expr._inner_refine()
return expr
def refine_step(self, steps=1):
"""Perform several steps of complex root refinement algorithm. """
expr = self
for _ in range(steps):
expr = expr._inner_refine()
return expr
def refine(self):
"""Perform one step of complex root refinement algorithm. """
return self._inner_refine()