from __future__ import division
import numpy as np
from collections import deque
import warnings
import functools
from .IterativeMethods import iterateToRoot
from .CountRoots import RootError
from .RootResult import RootResult
from .Misc import doc_tab_to_space, docstrings, NumberOfRootsChanged
class MultiplicityError(RuntimeError):
pass
class countCalls:
"""
Count how many times a given function is called.
"""
def __init__(self, func):
self.func = func
self.calls = 0
self.points = 0
def __call__(self, z):
self.calls += 1
if hasattr(z, '__len__'):
self.points += len(z)
else:
self.points += 1
return self.func(z)
@docstrings.get_sectionsf('find_roots_gen')
@docstrings.dedent
@doc_tab_to_space
def find_roots_gen(originalContour, f, df=None, guessRoots=[], guessRootSymmetry=None,
newtonStepTol=1e-14, attemptIterBest=True, newtonMaxIter=50, rootErrTol=1e-10,
absTol=0, relTol=1e-12, integerTol=0.1, NIntAbsTol=0.07, M=5, errStop=1e-10,
intMethod='quad', divMin=3, divMax=15, m=2, verbose=False):
"""
A generator which at each step takes a contour and either finds all
the zeros of f within it or subdivides it further. Based on the
algorithm in [KB]_.
Parameters
----------
originalContour : :class:`Contour <cxroots.Contour.Contour>`
The contour which bounds the region in which all the roots of
f(z) are sought.
f : function
A function of a single complex variable, z, which is analytic
within the contour and has no poles or roots on the contour.
df : function, optional
A function of a single complex variable which is the derivative
of the function f(z). If df is not given then it will be
approximated with a finite difference formula.
guessRoots : list, optional
A list of known roots or guesses for roots (they are checked
before being accepted).
guessRootSymmetry : function, optional
A function of a single complex variable, z, which returns a list
of all points which are expected to be roots of f, given that z
is a root of f.
newtonStepTol : float, optional
The required accuracy of the root. The iterative method used to
give a final value for each root will exit if the step size, dx,
between sucessive iterations satisfies abs(dx) < newtonStepTol
and iterBestAttempt is False.
attemptIterBest : bool, optional
If True then the iterative method used to refine the roots will
exit when error of the previous iteration, x0, was at least as
good as the current iteration, x, in the sense that
abs(f(x)) >= abs(f(x0)) and the previous iteration satisfied
abs(dx0) < newtonStepTol. In this case the preivous iteration
is returned as the approximation of the root.
newtonMaxIter : int, optional
The iterative method used to give a final value for each root
will exit if the number of iterations exceeds newtonMaxIter.
rootErrTol : float, optional
A complex value z is considered a root if abs(f(z)) < rootErrTol
absTol : float, optional
Absolute error tolerance used by the contour integration.
relTol : float, optional
Relative error tolerance used by the contour integration.
integerTol : float, optional
A number is considered an integer if it is within integerTol of
an integer. Used when determing if the value for the number of
roots within a contour and the values of the computed
multiplicities of roots are acceptably close to integers.
NIntAbsTol : float, optional
The absolute error tolerance used for the contour integration
when determining the number of roots within a contour. Since
the result of this integration must be an integer it can be much
less accurate than usual.
M : int, optional
If the number of roots (including multiplicites) within a
contour is greater than M then the contour is subdivided
further. M must be greater than or equal to the largest
multiplcity of any root.
errStop : float, optional
The number of distinct roots within a contour, n, is determined
by checking if all the elements of a list of contour integrals
involving formal orthogonal polynomials are sufficently close to
zero, ie. that the absolute value of each element is < errStop.
If errStop is too large/small then n may be smaller/larger than
it actually is.
intMethod : {'quad', 'romb'}, optional
If 'quad' then :func:`scipy.integrate.quad` is used to perform the
integral. If 'romb' then Romberg integraion, using
:func:`scipy.integrate.romb`, is performed instead. Typically, quad is
the better choice but it requires that the real and imaginary
parts of each integral are calculated sepeartely, in addition,
if df is not provided, 'quad' will require additional function
evaluations to approximate df at each point that f is evaluated
at. If evaluating f is expensive then 'romb' may be more
efficient since it computes the real and imaginary parts
simultaniously and if df is not provided it will approximate it
using only the values of f that would be required by the
integration routine in any case.
divMin : int, optional
If the Romberg integration method is used then divMin is the
minimum number of divisions before the Romberg integration
routine is allowed to exit.
divMax : int, optional
If the Romberg integration method is used then divMax is the
maximum number of divisions before the Romberg integration
routine exits.
m : int, optional
Only used if df=None and method='quad'. The argument order=m is
passed to :func:`numdifftools.Derivative` and is the order of the error
term in the Taylor approximation. m must be even.
verbose : bool, optional
If True certain messages concerning the rootfinding process will
be printed.
Yields
------
list
Roots of f(z) within the contour originalContour
list
Multiplicites of roots
deque
The contours which still contain roots
int
Remaining number of roots to be found within the contour
References
----------
.. [KB] Peter Kravanja, Marc Van Barel, "Computing the Zeros of
Anayltic Functions", Springer (2000)
"""
from .contours.Circle import Circle
# wrap f to record the number of function calls
f = countCalls(f)
countKwargs = {'f':f, 'df':df, 'NIntAbsTol':NIntAbsTol, 'integerTol':integerTol,
'divMin':divMin, 'divMax':divMax, 'm':m, 'intMethod':intMethod, 'verbose':verbose}
try:
# compute the total number of zeros, including multiplicities, within the originally given contour
originalContour._numberOfRoots = originalContour.count_roots(**countKwargs)
except RuntimeError:
raise RuntimeError("""
Integration along the initial contour has failed.
There is likely a root on or close to the initial contour.
Try changing the initial contour, if possible.""")
if verbose:
print('Total number of roots (counting multiplicities) within the original contour =', originalContour._numberOfRoots)
roots = []
multiplicities = []
failedContours = []
contours = deque()
contours.append(originalContour)
def subdivide(parentContour):
"""Given a contour, parentContour, subdivide it into multiple contours."""
if verbose:
print('Subdividing', parentContour)
numberOfRoots = None
for subcontours in parentContour.subdivisions():
# if a contour has already been used and caused an error then skip it
failedContourDesc = list(map(str, failedContours))
if np.any([contourDesc in failedContourDesc for contourDesc in list(map(str, subcontours))]):
continue
# if a contour is near to a known root then skip it
for root in roots:
if np.any(np.abs([contour.distance(root) for contour in subcontours]) < 0.01):
continue
try:
numberOfRoots = [contour.count_roots(**countKwargs) for contour in np.array(subcontours)]
while parentContour._numberOfRoots != sum(numberOfRoots):
if verbose:
print('Number of roots in sub contours not adding up to parent contour.')
print('Recomputing number of roots in parent and child contours with NIntAbsTol = ', .5*NIntAbsTol)
tempCountKwargs = countKwargs.copy()
tempCountKwargs['NIntAbsTol'] *= 0.5
parentContour._numberOfRoots = parentContour.count_roots(**tempCountKwargs)
numberOfRoots = [contour.count_roots(**tempCountKwargs) for contour in np.array(subcontours)]
if parentContour._numberOfRoots == sum(numberOfRoots):
break
except RootError:
# If the number of zeros within either of the new contours is not an integer then it is
# likely that the introduced line which subdivides 'parentContour' lies on a zero.
# To avoid this we will try to place the subdividing line at a different point along
# the division axis
if verbose:
print('RootError encountered when subdivding', parentContour, 'into:')
print(subcontours[0])
print(subcontours[1])
continue
if numberOfRoots is None or parentContour._numberOfRoots != sum(numberOfRoots):
# The list of subdivisions has been exhaused and still the number of enclosed zeros does not add up
raise RuntimeError("""Unable to subdivide contour:
\t%s
""" % parentContour)
# record number of roots within each sub-contour
for i, contour in enumerate(subcontours):
contour._numberOfRoots = numberOfRoots[i]
# add these sub-contours to the list of contours to find the roots in
contours.extend([contour for i, contour in enumerate(subcontours) if numberOfRoots[i] != 0])
def remove_siblings_children(C):
"""
Remove the contour C and all its siblings and children from the
list of contours to be examined/subdivided.
"""
try:
contours.remove(C)
except ValueError:
pass
# get sibling and child contours
# siblings:
relations = C._parentContour._childcontours
relations.remove(C)
# children:
if hasattr(C, '_childcontours'):
relations.extend(C._childcontours)
# interate over all relations
for relation in relations:
remove_relations(relation)
def addRoot(root, multiplicity):
# check that the root we have found is distinct from the ones we already have
if not roots or np.all(abs(np.array(roots) - root) > newtonStepTol):
# add the root to the list if it is within the original contour
if originalContour.contains(root):
roots.append(root)
multiplicities.append(multiplicity)
if verbose: print('Recorded root', root, 'multiplicity', multiplicity)
elif verbose: print('Root', root, 'ignored as not within original contour.')
# check to see if there are any other roots implied by the given symmetry
if guessRootSymmetry is not None:
for x0 in guessRootSymmetry(root):
# first check that x0 is distinct from the roots we already have
if np.all(abs(np.array(roots) - x0) > newtonStepTol):
if verbose:
print(root, 'is a root so guessRootSymmetry suggests that', x0, 'might also be a root.')
root = iterateToRoot(x0, f, df, newtonStepTol, rootErrTol, newtonMaxIter, attemptIterBest, verbose)
if root is not None:
contours.append(Circle(root, 1e-3))
contours[-1]._numberOfRoots = contours[-1].count_roots(**countKwargs)
elif verbose:
print('Already recorded root', root)
# Add contours surrounding known roots so that they will be checked
for root in guessRoots:
contours.append(Circle(root, 1e-3))
contours[-1]._numberOfRoots = contours[-1].count_roots(**countKwargs)
while contours:
# yield the initial state here so that the animation in demo_find_roots shows the first frame
totFoundRoots = sum(int(round(multiplicity.real)) for root, multiplicity in zip(roots, multiplicities))
yield roots, multiplicities, contours, originalContour._numberOfRoots - totFoundRoots
contour = contours.pop()
if verbose: print(contour._numberOfRoots, 'roots in', contour)
# if a known root is too near to this contour then reverse the subdivision that created it
if np.any([contour.distance(root) < newtonStepTol for root in roots]):
# remove the contour together with its children and siblings
remove_siblings_children(contour)
# put the parent contour back into the list of contours to be subdivided again
contours.append(contour._parentContour)
# do not use this contour again
failedContours.append(contour)
continue
# if the contour is smaller than the newtonStepTol then just assume that
# the root is at the center of the contour, print a warning and move on
if contour.area < newtonStepTol:
root = iterateToRoot(contour.centralPoint, f, df, newtonStepTol, rootErrTol, newtonMaxIter, attemptIterBest, verbose)
if root is None or abs(f(root)) > abs(f(contour.centralPoint)) or not contour.contains(root):
root = contour.centralPoint
warnings.warn("The area of the interior of this contour with is smaller than newtonStepTol! Try increasing rootTol" \
"The point z = %f + %fi has been recorded as a root of multiplicity %i." \
"The error |f(z)| = "%(root.real, contour.centralPoint.imag, contour._numberOfRoots) + str(abs(f(root))) \
)
addRoot(root, contour._numberOfRoots)
continue
# if all the roots within the contour have been located then continue to the next contour
numberOfKnownRootsInContour = sum([int(round(multiplicity.real)) for root, multiplicity in zip(roots, multiplicities) if contour.contains(root)])
if contour._numberOfRoots == numberOfKnownRootsInContour:
continue
# if there are too many roots within the contour then subdivide or
# if there are any known roots within the contour then also subdivide
# (so as not to waste time re-approximating these roots)
if numberOfKnownRootsInContour > 0 or contour._numberOfRoots > M:
subdivide(contour)
continue
### Approximate the roots in this contour
if intMethod == 'romb':
# Check to see if the number of roots has changed after new values of f have been sampled
def callback(I, err, numberOfDiv):
if numberOfDiv > contour._numberOfDivisionsForN:
if verbose: print('--- Checking N using the newly sampled values of f ---')
new_N = contour.count_roots(f, df, NIntAbsTol=NIntAbsTol, integerTol=integerTol,
divMin=numberOfDiv, divMax=divMax, m=m, intMethod=intMethod, verbose=verbose)
if verbose: print('------------------------------------------------------')
if new_N != contour._numberOfRoots:
if verbose:
print('N has been recalculated using more samples of f')
contour._numberOfRoots = new_N
raise NumberOfRootsChanged("""The additional function evaluations of f taken while
approximating the roots within the contour have been shown that the number of roots
of f within the contour is %i rather than the supplied %i."""%(new_N, contour._numberOfRoots))
else:
callback = None
try:
approxRoots, approxMultiplicities = contour.approximate_roots(contour._numberOfRoots, f, df,
absTol=absTol, relTol=relTol, errStop=errStop, divMin=divMin, divMax=divMax,
m=m, rootTol=newtonStepTol, intMethod=intMethod, callback=callback, verbose=verbose)
except NumberOfRootsChanged:
if verbose: print('The number of roots within the contour have been reevaluated.')
if contour._numberOfRoots > M:
subdivide(contour)
else:
contours.append(contour)
continue
for approxRoot, approxMultiplicity in list(zip(approxRoots, approxMultiplicities)):
if verbose: print('approxRoot', approxRoot, 'approxMultiplicity', approxMultiplicity)
# check that the multiplicity is close to an integer
multiplicity = round(approxMultiplicity.real)
if abs(multiplicity - approxMultiplicity.real) > integerTol or abs(approxMultiplicity.imag) > integerTol or multiplicity < 1:
continue
# attempt to refine the root
root = iterateToRoot(approxRoot, f, df, newtonStepTol, rootErrTol, newtonMaxIter, attemptIterBest, verbose)
if root is None or abs(f(approxRoot)) < abs(f(root)):
# stick with the original approximation
root = approxRoot
if abs(f(root)) < rootErrTol:
if np.any(np.abs(np.round(approxMultiplicities) - approxMultiplicities) > integerTol):
# the computed multiplicity might be unreliable so make a contour focused on that point instead
if hasattr(contour, '_shrinkingRadius'):
contour._shrinkingRadius *= 0.5
contours.append(Circle(root, contour._shrinkingRadius))
else:
contours.append(Circle(root, 1e-3))
contours[-1]._shrinkingRadius = 1e-3
contours[-1]._numberOfRoots = contours[-1].count_roots(**countKwargs)
else:
addRoot(root, multiplicity)
# if the root turns out to be very close to the contour then this may have
# introduced an error. Therefore, compute the multiplicity of this root
# directly and disregard this contour (repeat its parent's subdivision).
if contour.distance(root) < newtonStepTol:
# remove the contour and any relations
remove_siblings_children(contour)
# put the parent contour back into the list of contours to subdivide again
parent = contour._parentContour
contours.append(parent)
# do not use this contour again
failedContours.append(contour)
# if we haven't found all the roots then subdivide further
numberOfKnownRootsInContour = sum([int(round(multiplicity.real)) for root, multiplicity in zip(roots, multiplicities) if contour.contains(root)])
if contour._numberOfRoots != numberOfKnownRootsInContour and contour not in failedContours:
subdivide(contour)
# delete cache for original contour incase this contour is being reused
for segment in originalContour.segments:
segment._integralCache = {}
segment._trapValuesCache = {}
if verbose:
print('Completed rootfinding with', f.calls, 'evaluations of f at', f.points, 'points')
print(RootResult(roots, multiplicities, originalContour))
totFoundRoots = sum(int(round(multiplicity.real)) for root, multiplicity in zip(roots, multiplicities))
yield roots, multiplicities, contours, originalContour._numberOfRoots - totFoundRoots
@docstrings.dedent
@doc_tab_to_space
@functools.wraps(find_roots_gen, assigned=('__module__', '__name__'))
def find_roots(originalContour, f, df=None, **kwargs):
"""
Find all the roots of the complex analytic function f within the
given contour.
Parameters
----------
%(find_roots_gen.parameters)s
Returns
-------
result : :class:`RootResult <cxroots.RootResult.RootResult>`
A container for the roots and their multiplicities.
"""
rootFinder = find_roots_gen(originalContour, f, df, **kwargs)
for roots, multiplicities, contours, numberOfRemainingRoots in rootFinder:
pass
return RootResult(roots, multiplicities, originalContour)