diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/graph.py | 58 | ||||
-rw-r--r-- | lib/great_circle_arc.py | 262 | ||||
-rw-r--r-- | lib/polygon.py | 13 | ||||
-rw-r--r-- | lib/skyline.py | 108 |
4 files changed, 232 insertions, 209 deletions
diff --git a/lib/graph.py b/lib/graph.py index 491f2fa..a32c3be 100644 --- a/lib/graph.py +++ b/lib/graph.py @@ -60,14 +60,14 @@ class Graph: .. note:: This class is not meant to be used directly. Instead, use - `sphere.polygon.SphericalPolygon.union` and - `sphere.polygon.SphericalPolygon.intersection`. + `~sphere.polygon.SphericalPolygon.union` and + `~sphere.polygon.SphericalPolygon.intersection`. """ class Node: - """ - A `Node` represents a single point, connected by an arbitrary - number of `Edge` objects to other `Node` objects. + """ + A `~Graph.Node` represents a single point, connected by an arbitrary + number of `~Graph.Edge` objects to other `~Graph.Node` objects. """ def __init__(self, point, source_polygons=[]): """ @@ -91,12 +91,12 @@ class Graph: Parameters ---------- - edge : `Edge` instance + edge : `~Graph.Edge` instance The edge to follow away from. Returns ------- - other : `Edge` instance + other : `~Graph.Edge` instance The other edge. """ edges = list(self._edges) @@ -109,11 +109,14 @@ class Graph: def equals(self, other, thres=2e-8): """ Returns `True` if the location of this and the *other* - `Node` are the same. + `~Graph.Node` are the same. Parameters ---------- - thres: float + other : `~Graph.Node` instance + The other node. + + thres : float If difference is smaller than this, points are equal. The default value of 2e-8 radians is set based on emphirical test cases. Relative threshold based on @@ -126,14 +129,14 @@ class Graph: class Edge: """ - An `Edge` represents a connection between exactly two `Node` - objects. This `Edge` class has no direction. + An `~Graph.Edge` represents a connection between exactly two + `~Graph.Node` objects. This `~Graph.Edge` class has no direction. """ def __init__(self, A, B, source_polygons=[]): """ Parameters ---------- - A, B : `Node` instances + A, B : `~Graph.Node` instances source_polygon : `~sphere.polygon.SphericalPolygon` instance, optional The polygon this edge came from. Used for bookkeeping. @@ -154,11 +157,11 @@ class Graph: Parameters ---------- - node : `Node` instance + node : `~Graph.Node` instance Returns ------- - other : `Node` instance + other : `~Graph.Node` instance """ nodes = self._nodes try: @@ -168,11 +171,11 @@ class Graph: def equals(self, other): """ - Returns True if the other edge is between the same two nodes. + Returns `True` if the other edge is between the same two nodes. Parameters ---------- - other : `Edge` instance + other : `~Graph.Edge` instance Returns ------- @@ -261,7 +264,7 @@ class Graph: Returns ------- - node : `Node` instance + node : `~Graph.Node` instance The new node """ new_node = self.Node(point, source_polygons) @@ -285,7 +288,7 @@ class Graph: Parameters ---------- - node : `Node` instance + node : `~Graph.Node` instance """ assert node in self._nodes @@ -304,14 +307,14 @@ class Graph: Parameters ---------- - A, B : `Node` instances + A, B : `~Graph.Node` instances source_polygons : `~sphere.polygon.SphericalPolygon` instance, optional The polygon(s) this edge came from. Used for bookkeeping. Returns ------- - edge : `Edge` instance + edge : `~Graph.Edge` instance The new edge """ assert A in self._nodes @@ -338,7 +341,7 @@ class Graph: Parameters ---------- - edge : `Edge` instance + edge : `~Graph.Edge` instance """ assert edge in self._edges @@ -354,21 +357,22 @@ class Graph: def split_edge(self, edge, node): """ - Splits an `Edge` *edge* at `Node` *node*, removing *edge* and - replacing it with two new `Edge` instances. It is intended - that *E* is along the original edge, but that is not enforced. + Splits an `~Graph.Edge` *edge* at `~Graph.Node` *node*, removing + *edge* and replacing it with two new `~Graph.Edge` instances. + It is intended that *E* is along the original edge, but that is + not enforced. Parameters ---------- - edge : `Edge` instance + edge : `~Graph.Edge` instance The edge to split - node : `Node` instance + node : `~Graph.Node` instance The node to insert Returns ------- - edgeA, edgeB : `Edge` instances + edgeA, edgeB : `~Graph.Edge` instances The two new edges on either side of *node*. """ assert edge in self._edges diff --git a/lib/great_circle_arc.py b/lib/great_circle_arc.py index 399cb3d..d5e78f2 100644 --- a/lib/great_circle_arc.py +++ b/lib/great_circle_arc.py @@ -53,138 +53,145 @@ try: except ImportError: HAS_C_UFUNCS = False +if HAS_C_UFUNCS: + inner1d = math_util.inner1d +else: + from numpy.core.umath_tests import inner1d + __all__ = ['angle', 'intersection', 'intersects', 'length', 'midpoint', 'interpolate'] -if not HAS_C_UFUNCS: - from numpy.core.umath_tests import inner1d +def _fast_cross(a, b): + """ + This is a reimplementation of `numpy.cross` that only does 3D x + 3D, and is therefore faster since it doesn't need any + conditionals. + """ + if HAS_C_UFUNCS: + return math_util.cross(a, b) + + cp = np.empty(np.broadcast(a, b).shape) + aT = a.T + bT = b.T + cpT = cp.T + + cpT[0] = aT[1]*bT[2] - aT[2]*bT[1] + cpT[1] = aT[2]*bT[0] - aT[0]*bT[2] + cpT[2] = aT[0]*bT[1] - aT[1]*bT[0] + + return cp + +def _cross_and_normalize(A, B): + if HAS_C_UFUNCS: + return math_util.cross_and_norm(A, B) + + T = _fast_cross(A, B) + # Normalization + l = np.sqrt(np.sum(T ** 2, axis=-1)) + l = np.expand_dims(l, 2) + # Might get some divide-by-zeros, but we don't care + with np.errstate(invalid='ignore'): + TN = T / l + return TN - def _fast_cross(a, b): - """ - This is a reimplementation of `numpy.cross` that only does 3D x - 3D, and is therefore faster since it doesn't need any - conditionals. - """ - cp = np.empty(np.broadcast(a, b).shape) - aT = a.T - bT = b.T - cpT = cp.T - - cpT[0] = aT[1]*bT[2] - aT[2]*bT[1] - cpT[1] = aT[2]*bT[0] - aT[0]*bT[2] - cpT[2] = aT[0]*bT[1] - aT[1]*bT[0] - - return cp - - def _cross_and_normalize(A, B): - T = _fast_cross(A, B) - # Normalization - l = np.sqrt(np.sum(T ** 2, axis=-1)) - l = np.expand_dims(l, 2) - # Might get some divide-by-zeros, but we don't care - with np.errstate(invalid='ignore'): - TN = T / l - return TN - - def intersection(A, B, C, D): - r""" - Returns the point of intersection between two great circle arcs. - The arcs are defined between the points *AB* and *CD*. Either *A* - and *B* or *C* and *D* may be arrays of points, but not both. - - Parameters - ---------- - A, B : (*x*, *y*, *z*) triples or Nx3 arrays of triples - Endpoints of the first great circle arc. - - C, D : (*x*, *y*, *z*) triples or Nx3 arrays of triples - Endpoints of the second great circle arc. - - Returns - ------- - T : (*x*, *y*, *z*) triples or Nx3 arrays of triples - If the given arcs intersect, the intersection is returned. If - the arcs do not intersect, the triple is set to all NaNs. - - Notes - ----- - The basic intersection is computed using linear algebra as follows - [1]_: - - .. math:: - - T = \lVert(A × B) × (C × D)\rVert - - To determine the correct sign (i.e. hemisphere) of the - intersection, the following four values are computed: - - .. math:: - - s_1 = ((A × B) × A) · T - - s_2 = (B × (A × B)) · T - - s_3 = ((C × D) × C) · T - - s_4 = (D × (C × D)) · T - - For :math:`s_n`, if all positive :math:`T` is returned as-is. If - all negative, :math:`T` is multiplied by :math:`-1`. Otherwise - the intersection does not exist and is undefined. - - References - ---------- - - .. [1] Method explained in an `e-mail - <http://www.mathworks.com/matlabcentral/newsreader/view_thread/276271>`_ - by Roger Stafford. - - http://www.mathworks.com/matlabcentral/newsreader/view_thread/276271 - """ - A = np.asanyarray(A) - B = np.asanyarray(B) - C = np.asanyarray(C) - D = np.asanyarray(D) - - A, B = np.broadcast_arrays(A, B) - C, D = np.broadcast_arrays(C, D) - - ABX = _fast_cross(A, B) - CDX = _fast_cross(C, D) - T = _cross_and_normalize(ABX, CDX) - T_ndim = len(T.shape) - - if T_ndim > 1: - s = np.zeros(T.shape[0]) - else: - s = np.zeros(1) - s += np.sign(inner1d(_fast_cross(ABX, A), T)) - s += np.sign(inner1d(_fast_cross(B, ABX), T)) - s += np.sign(inner1d(_fast_cross(CDX, C), T)) - s += np.sign(inner1d(_fast_cross(D, CDX), T)) - if T_ndim > 1: - s = np.expand_dims(s, 2) - - cross = np.where(s == -4, -T, np.where(s == 4, T, np.nan)) - - # If they share a common point, it's not an intersection. This - # gets around some rounding-error/numerical problems with the - # above. - equals = (np.all(A == C, axis = -1) | - np.all(A == D, axis = -1) | - np.all(B == C, axis = -1) | - np.all(B == D, axis = -1)) - - equals = np.expand_dims(equals, 2) - - return np.where(equals, np.nan, cross) -else: - inner1d = math_util.inner1d - _fast_cross = math_util.cross - _cross_and_normalize = math_util.cross_and_norm - intersection = math_util.intersection + +def intersection(A, B, C, D): + r""" + Returns the point of intersection between two great circle arcs. + The arcs are defined between the points *AB* and *CD*. Either *A* + and *B* or *C* and *D* may be arrays of points, but not both. + + Parameters + ---------- + A, B : (*x*, *y*, *z*) triples or Nx3 arrays of triples + Endpoints of the first great circle arc. + + C, D : (*x*, *y*, *z*) triples or Nx3 arrays of triples + Endpoints of the second great circle arc. + + Returns + ------- + T : (*x*, *y*, *z*) triples or Nx3 arrays of triples + If the given arcs intersect, the intersection is returned. If + the arcs do not intersect, the triple is set to all NaNs. + + Notes + ----- + The basic intersection is computed using linear algebra as follows + [1]_: + + .. math:: + + T = \lVert(A × B) × (C × D)\rVert + + To determine the correct sign (i.e. hemisphere) of the + intersection, the following four values are computed: + + .. math:: + + s_1 = ((A × B) × A) · T + + s_2 = (B × (A × B)) · T + + s_3 = ((C × D) × C) · T + + s_4 = (D × (C × D)) · T + + For :math:`s_n`, if all positive :math:`T` is returned as-is. If + all negative, :math:`T` is multiplied by :math:`-1`. Otherwise + the intersection does not exist and is undefined. + + References + ---------- + + .. [1] Method explained in an `e-mail + <http://www.mathworks.com/matlabcentral/newsreader/view_thread/276271>`_ + by Roger Stafford. + + http://www.mathworks.com/matlabcentral/newsreader/view_thread/276271 + """ + if HAS_C_UFUNCS: + return math_util.intersection(A, B, C, D) + + A = np.asanyarray(A) + B = np.asanyarray(B) + C = np.asanyarray(C) + D = np.asanyarray(D) + + A, B = np.broadcast_arrays(A, B) + C, D = np.broadcast_arrays(C, D) + + ABX = _fast_cross(A, B) + CDX = _fast_cross(C, D) + T = _cross_and_normalize(ABX, CDX) + T_ndim = len(T.shape) + + if T_ndim > 1: + s = np.zeros(T.shape[0]) + else: + s = np.zeros(1) + s += np.sign(inner1d(_fast_cross(ABX, A), T)) + s += np.sign(inner1d(_fast_cross(B, ABX), T)) + s += np.sign(inner1d(_fast_cross(CDX, C), T)) + s += np.sign(inner1d(_fast_cross(D, CDX), T)) + if T_ndim > 1: + s = np.expand_dims(s, 2) + + cross = np.where(s == -4, -T, np.where(s == 4, T, np.nan)) + + # If they share a common point, it's not an intersection. This + # gets around some rounding-error/numerical problems with the + # above. + equals = (np.all(A == C, axis = -1) | + np.all(A == D, axis = -1) | + np.all(B == C, axis = -1) | + np.all(B == D, axis = -1)) + + equals = np.expand_dims(equals, 2) + + return np.where(equals, np.nan, cross) def length(A, B, degrees=True): @@ -261,7 +268,8 @@ def angle(A, B, C, degrees=True): Parameters ---------- - A, B, C : (*x*, *y*, *z*) triples or Nx3 arrays of triples. + A, B, C : (*x*, *y*, *z*) triples or Nx3 arrays of triples + Points on sphere. degrees : bool, optional If `True` (default) the result is returned in decimal degrees, diff --git a/lib/polygon.py b/lib/polygon.py index 8047ab1..fd67042 100644 --- a/lib/polygon.py +++ b/lib/polygon.py @@ -115,12 +115,14 @@ class SphericalPolygon(object): return self._inside def to_radec(self): - """Convert `SphericalPolygon` footprint to RA and DEC. + """ + Convert `SphericalPolygon` footprint to RA and DEC. Returns ------- - List of RA and DEC pairs (in degrees) corresponding to - `self.points`. + zip(ra,dec) : list of tuples + List of (*ra*, *dec*) pairs in degrees corresponding + to `points`. """ ra, dec = vector.vector_to_radec(self.points[:,0], self.points[:,1], self.points[:,2], degrees=True) @@ -314,7 +316,7 @@ class SphericalPolygon(object): Parameters ---------- - preserve_order: bool + preserve_order : bool Preserve original order? If `True`, polygon is rotated around min point. If `False`, all points are sorted in ascending order. @@ -361,7 +363,8 @@ class SphericalPolygon(object): Returns ------- - `True` or `False` + is_eq : bool + `True` or `False`. """ self_n = len(self.points) diff --git a/lib/skyline.py b/lib/skyline.py index 113192e..db4d6b8 100644 --- a/lib/skyline.py +++ b/lib/skyline.py @@ -33,11 +33,9 @@ # OF THE POSSIBILITY OF SUCH DAMAGE. """ -Manage outlines on the sky. - This module provides support for working with footprints on the sky. Primary use case would use the following -generalized steps:: +generalized steps: #. Initialize `SkyLine` objects for each input image. This object would be the union of all the input @@ -72,16 +70,6 @@ This process will work reasonably fast as most operations are performed using the `SkyLine` objects and WCS information solely, not image data itself. -.. note:: Requires Python 2.7 or later. - -:Authors: Pey Lian Lim, Warren Hack, Michael Droettboom - -:Organization: Space Telescope Science Institute - -:History: - * 2012-05-25 PLL started coding. Original class - structure by WJH. Parent class by MD. - """ from __future__ import division, print_function, absolute_import @@ -96,26 +84,28 @@ from stwcs.distortion.utils import output_wcs # LOCAL from .polygon import SphericalPolygon -__all__ = ['SkyLine'] +__all__ = ['SkyLineMember', 'SkyLine'] __version__ = '0.3a' __vdate__ = '20-Jun-2012' class SkyLineMember(object): + """ + Container for `SkyLine` members with these attributes: + + * `fname`: Image name (with path if given) + * `ext`: Extension read + * `wcs`: `HSTWCS` object the data data + * `polygon`: `~sphere.polygon.SphericalPolygon` object of the data + """ def __init__(self, fname, ext): """ - Container for `SkyLine` members. - - Given FITS image and extension, will store its - `SphericalPolygon` instance from WCS under - `polygon`. - Parameters ---------- - fname: str + fname : str FITS image. - ext: int + ext : int Image extension. """ @@ -145,17 +135,22 @@ class SkyLineMember(object): return self._polygon class SkyLine(object): + """ + Manage outlines on the sky. + + Each `SkyLine` has a list of `~SkyLine.members` and + a composite `~SkyLine.polygon` with all the + functionalities of `~sphere.polygon.SphericalPolygon`. + """ def __init__(self, fname, extname='SCI'): """ - Initialize `SkyLine` object instance. - Parameters ---------- - fname: str + fname : str FITS image. `None` to create empty `SkyLine`. - extname: str + extname : str EXTNAME to use. SCI is recommended for normal HST images. PRIMARY if image is single ext. @@ -179,7 +174,7 @@ class SkyLine(object): self.polygon = SphericalPolygon([]) def __getattr__(self, what): - """Control attribute access to `SphericalPolygon`.""" + """Control attribute access to `~sphere.polygon.SphericalPolygon`.""" if what in ('from_radec', 'from_cone', 'from_wcs', 'multi_union', 'multi_intersection', '_find_new_inside',): @@ -197,23 +192,31 @@ class SkyLine(object): @property def polygon(self): - """`SphericalPolygon` portion of `SkyLine`.""" + """ + `~sphere.polygon.SphericalPolygon` portion of `SkyLine` + that contains the composite skyline from `members` + belonging to *self*. + + """ return self._polygon @polygon.setter def polygon(self, value): - """Deep copy a `SphericalPolygon`.""" assert isinstance(value, SphericalPolygon) - self._polygon = copy(value) + self._polygon = copy(value) # Deep copy @property def members(self): - """List of `SkyLineMember` objects.""" + """ + List of `SkyLineMember` objects that belong to *self*. + Duplicate members are discarded. Members are kept in + the order of their additions to *self*. + + """ return self._members @members.setter def members(self, values): - """Make sure `SkyLineMember` entries are unique.""" self._members = [] # Not using set to preserve order @@ -226,8 +229,8 @@ class SkyLine(object): def to_wcs(self): """ - Combine WCS objects from all `members` and return a - new WCS object. If no `members`, return `None`. + Combine `HSTWCS` objects from all `members` and return + a new `HSTWCS` object. If no `members`, return `None`. """ wcs = None @@ -244,15 +247,15 @@ class SkyLine(object): Parameters ---------- - self: obj + self : obj `SkyLine` instance. - given_members: list + given_members : list List of `SkyLineMember` to consider. Returns ------- - new_members: list + new_members : list List of `SkyLineMember` belonging to *self*. """ @@ -310,16 +313,16 @@ class SkyLine(object): Parameters ---------- - skylines: list + skylines : list A list of `SkyLine` instances. Returns ------- - max_skyline: `SkyLine` instance or `None` + max_skyline : `SkyLine` instance or `None` `SkyLine` that overlaps the most or `None` if no overlap found. This is *not* a copy. - max_overlap_area: float + max_overlap_area : float Area of intersection. """ @@ -342,13 +345,13 @@ class SkyLine(object): Parameters ---------- - skylines: list + skylines : list A list of `SkyLine` instances. Returns ------- - max_pair: tuple - Pair of `SkyLine` instances with max overlap + max_pair : tuple + Pair of `SkyLine` objects with max overlap among given *skylines*. If no overlap found, return `None`. These are *not* copies. @@ -371,24 +374,29 @@ class SkyLine(object): """ Mosaic all overlapping *skylines*. + A pair of skylines with the most overlap is used as + a starting point. Then a skyline that overlaps the + most with the mosaic is used, and so forth until no + overlapping skyline is found. + Parameters ---------- - skylines: list - A list of `SkyLine` instances. + skylines : list + A list of `SkyLine` objects. Returns ------- - mosaic: `SkyLine` instance or `None` + mosaic : `SkyLine` instance or `None` Union of all overlapping *skylines*, or `None` if no overlap found. - included: list - List of image names added to `mosaic` in the order + included : list + List of image names added to mosaic in the order of addition. - excluded: list + excluded : list List of image names excluded because they do not - overlap with `mosaic`. + overlap with mosaic. """ out_order = [] |