"""
Miscellaneous package utilities.
.. include:: ../include/links.rst
"""
from itertools import chain, combinations
from IPython import embed
import numpy
[docs]def all_subclasses(cls):
"""
Collect all the subclasses of the provided class.
The search follows the inheritance to the highest-level class. Intermediate
base classes are included in the returned set, but not the base class itself.
Thanks to:
https://stackoverflow.com/questions/3862310/how-to-find-all-the-subclasses-of-a-class-given-its-name
Args:
cls (object):
The base class
Returns:
:obj:`set`: The unique set of derived classes, including any
intermediate base classes in the inheritance thread.
"""
return set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in all_subclasses(c)])
[docs]def string_table(tbl, delimeter='print', has_header=True):
"""
Provided the array of data, format it with equally spaced columns
and add a header (first row) and contents delimeter.
Args:
tbl (`numpy.ndarray`_):
Array of string representations of the data to print.
delimeter (:obj:`str`, optional):
If the first row in the table containts the column headers (see
``has_header``), this sets the delimeter between first table row and
the column data. Use ``'print'`` for a simple line of hyphens,
anything else results in an ``rst`` style table formatting.
has_header (:obj:`bool`, optional):
The first row in ``tbl`` contains the column headers.
Returns:
:obj:`str`: Single long string with the data table.
"""
nrows, ncols = tbl.shape
col_width = [numpy.amax([len(dij) for dij in dj]) for dj in tbl.T]
_nrows = nrows
start = 1
if delimeter != 'print':
_nrows += 2
start += 1
if has_header:
_nrows += 1
start += 1
row_string = ['']*_nrows
for i in range(start,nrows+start-1):
row_string[i] = ' '.join([tbl[1+i-start,j].ljust(col_width[j]) for j in range(ncols)])
if delimeter == 'print':
# Heading row
row_string[0] = ' '.join([tbl[0,j].ljust(col_width[j]) for j in range(ncols)])
# Delimiter
if has_header:
row_string[1] = '-'*len(row_string[0])
return '\n'.join(row_string)+'\n'
# For an rst table
row_string[0] = ' '.join([ '='*col_width[j] for j in range(ncols)])
row_string[1] = ' '.join([tbl[0,j].ljust(col_width[j]) for j in range(ncols)])
if has_header:
row_string[2] = row_string[0]
row_string[-1] = row_string[0]
return '\n'.join(row_string)+'\n'
[docs]def powerset(iterable, reverse=False):
""""
Construct an iterable that steps through all combinations of the
provided iterable.
This is pulled from the recipes provided by the itertools
documentation.
Examples:
Get all unique combinations of the list [1,2,3]:
>>> list(powerset([1,2,3]))
[() (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)]
Args:
iterable (iterable):
An iterable object
reverse (:obj:`bool`, optional):
Reverse the order (only roughly) of the iterable by placing
the longer sequences first.
Returns:
`itertools.chain`: Iterable object that returns the sequence of
combinations.
"""
rng = range(len(iterable)+1)[::-1] if reverse else range(len(iterable)+1)
return chain.from_iterable(combinations(iterable, r) for r in rng)
[docs]def polygon_winding_number(polygon, point):
"""
Determine the winding number of a 2D polygon about a point.
The code does **not** check if the polygon is simple (no interesecting line
segments). Algorithm taken from Numerical Recipes Section 21.4.
Args:
polygon (`numpy.ndarray`_):
An Nx2 array containing the x,y coordinates of a polygon.
The points should be ordered either counter-clockwise or
clockwise.
point (`numpy.ndarray`_):
One or more points for the winding number calculation.
Must be either a 2-element array for a single (x,y) pair,
or an Nx2 array with N (x,y) points.
Returns:
:obj:`int`, `numpy.ndarray`_: The winding number of each point with
respect to the provided polygon. Points inside the polygon have winding
numbers of 1 or -1; see :func:`point_inside_polygon`.
Raises:
ValueError:
Raised if ``polygon`` is not 2D, if ``polygon`` does not have two
columns, or if the last axis of ``point`` does not have 2 and only 2
elements.
"""
# Check input shape is for 2D only
if len(polygon.shape) != 2:
raise ValueError('Polygon must be an Nx2 array.')
if polygon.shape[1] != 2:
raise ValueError('Polygon must be in two dimensions.')
_point = numpy.atleast_2d(point)
if _point.shape[1] != 2:
raise ValueError('Point must contain two elements.')
# Get the winding number
nvert = polygon.shape[0]
npnt = _point.shape[0]
dl = numpy.roll(polygon, 1, axis=0)[None,:,:] - _point[:,None,:]
dr = polygon[None,:,:] - point[:,None,:]
dx = dl[...,0]*dr[...,1] - dl[...,1]*dr[...,0]
indx_l = dl[...,1] > 0
indx_r = dr[...,1] > 0
wind = numpy.zeros((npnt, nvert), dtype=int)
wind[indx_l & numpy.logical_not(indx_r) & (dx < 0)] = -1
wind[numpy.logical_not(indx_l) & indx_r & (dx > 0)] = 1
return numpy.sum(wind, axis=1)[0] if point.ndim == 1 else numpy.sum(wind, axis=1)
[docs]def point_inside_polygon(polygon, point):
"""
Determine if one or more points is inside the provided polygon.
Primarily a wrapper for :func:`polygon_winding_number`, that
returns True for each point that is inside the polygon.
Args:
polygon (`numpy.ndarray`_):
An Nx2 array containing the x,y coordinates of a polygon.
The points should be ordered either counter-clockwise or
clockwise.
point (`numpy.ndarray`_):
One or more points for the winding number calculation.
Must be either a 2-element array for a single (x,y) pair,
or an Nx2 array with N (x,y) points.
Returns:
:obj:`bool`, `numpy.ndarray`: Boolean indicating whether or not each
point is within the polygon.
"""
return numpy.absolute(polygon_winding_number(polygon, point)) == 1