from __future__ import division
import numpy as np
import functools
import docrep
from .CountRoots import count_roots
from .ApproximateRoots import approximate_roots
from .RootFinder import find_roots, MultiplicityError
from .DemoRootFinder import demo_find_roots
from .Misc import doc_tab_to_space, docstrings, remove_para
from .Paths import ComplexPath
[docs]class Contour(object):
"""
A base class for contours in the complex plane.
Attributes
----------
centralPoint : complex
The point at the center of the contour.
area : float
The surface area of the contour.
"""
def __init__(self, segments):
self.segments = np.array(segments, dtype=object)
[docs] def __call__(self, t):
"""
The point on the contour corresponding the value of the
parameter t.
Parameters
----------
t : float
A real number :math:`0\leq t \leq 1` which parameterises
the contour.
Returns
-------
complex
A point on the contour.
Example
-------
>>> from cxroots.Paths import Circle
>>> c = Circle(0,1) # Circle |z|=1 parameterised by e^{it}
>>> c(0.25)
(6.123233995736766e-17+1j)
>>> c(0) == c(1)
True
"""
t = np.array(t)
N = len(self.segments)
segmentIndex = np.array(N*t, dtype=int)
segmentIndex = np.mod(segmentIndex, N)
segment = self.segments[segmentIndex]
if hasattr(segmentIndex, '__iter__'):
return np.array([self.segments[i](N*t[ti]%1) for ti, i in enumerate(segmentIndex)])
else:
return self.segments[segmentIndex](N*t%1)
@property
def centralPoint(self):
raise NotImplemented('centralPoint needs to be implemented in the subclass.')
@property
def area(self):
raise NotImplemented('area needs to be implemented in the subclass.')
[docs] def contains(self, z):
"""
Tests whether the point z is within the contour.
Parameters
----------
z : complex
Returns
-------
bool
True if z lies within the contour and false otherwise.
"""
raise NotImplemented('contains() needs to be implemented in the subclass.')
@functools.wraps(ComplexPath.plot)
def plot(self, *args, **kwargs):
self._sizePlot()
for segment in self.segments:
segment.plot(*args, **kwargs)
def _sizePlot(self):
import matplotlib.pyplot as plt
t = np.linspace(0,1,1000)
z = self(t)
xpad = (max(np.real(z))-min(np.real(z)))*0.1
ypad = (max(np.imag(z))-min(np.imag(z)))*0.1
xmin = min(np.real(z))-xpad
xmax = max(np.real(z))+xpad
ymin = min(np.imag(z))-ypad
ymax = max(np.imag(z))+ypad
plt.xlim([xmin, xmax])
plt.ylim([ymin, ymax])
[docs] def show(self, saveFile=None, **plotKwargs):
"""
Shows the contour as a 2D plot in the complex plane. Requires
Matplotlib.
Parameters
----------
saveFile : str (optional)
If given then the plot will be saved to disk with name
'saveFile'. If saveFile=None the plot is shown on-screen.
**plotKwargs
Key word arguments are as in :meth:`~cxroots.Contour.Contour.plot`.
"""
import matplotlib.pyplot as plt
self.plot(**plotKwargs)
if saveFile is not None:
plt.savefig(saveFile, bbox_inches='tight')
plt.close()
else:
plt.show()
[docs] def subdivisions(self, axis='alternating'):
"""
A generator for possible subdivisions of the contour.
Parameters
----------
axis : str, 'alternating' or any element of self.axisName.
The axis along which the line subdividing the contour is a
constant (eg. subdividing a circle along the radial axis
will give an outer annulus and an inner circle). If
alternating then the dividing axis will always be different
to the dividing axis used to create the contour which is now
being divided.
Yields
------
tuple
A tuple with two contours which subdivide the original
contour.
"""
if axis == 'alternating':
if hasattr(self,'_createdBySubdivisionAxis'):
axis = (self._createdBySubdivisionAxis + 1)%len(self.axisName)
else:
axis = 0
for divisionFactor in divisionFactorGen():
yield self.subdivide(axis, divisionFactor)
[docs] def distance(self, z):
"""
Get the distance from the point z in the complex plane to the
nearest point on the contour.
Parameters
----------
z : complex
The point from which to measure the distance to the closest
point on the contour to z.
Returns
-------
float
The distance from z to the point on the contour which is
closest to z.
"""
return min(segment.distance(z) for segment in self.segments)
@functools.wraps(ComplexPath.integrate)
def integrate(self, f, **integrationKwargs):
return sum([segment.integrate(f, **integrationKwargs) for segment in self.segments])
@remove_para('C')
@functools.wraps(count_roots)
def count_roots(self, f, df=None, **kwargs):
return count_roots(self, f, df, **kwargs)
@remove_para('C')
@functools.wraps(approximate_roots)
def approximate_roots(self, N, f, df=None, **kwargs):
return approximate_roots(self, N, f, df, **kwargs)
@remove_para('originalContour')
@functools.wraps(find_roots)
def roots(self, f, df=None, **kwargs):
return find_roots(self, f, df, **kwargs)
@remove_para('C')
@functools.wraps(demo_find_roots)
def demo_roots(self, *args, **kwargs):
return demo_find_roots(self, *args, **kwargs)
def divisionFactorGen():
"""A generator for divisionFactors."""
yield 0.3 # being off-center is a better first choice for certain problems
x = 0.5
yield x
for diff in np.linspace(0, 0.5, int(1+10/2.))[1:-1]:
yield x + diff
yield x - diff