summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/__init__.py5
-rw-r--r--lib/graph.py909
-rw-r--r--lib/great_circle_arc.py368
-rw-r--r--lib/polygon.py831
-rw-r--r--lib/test/__init__.py1
-rw-r--r--lib/test/benchmarks.py39
-rw-r--r--lib/test/data/1904-66_TAN.fits.gzbin113196 -> 0 bytes
-rw-r--r--lib/test/data/2chipA.fits.gzbin227579 -> 0 bytes
-rw-r--r--lib/test/data/2chipB.fits.gzbin227493 -> 0 bytes
-rw-r--r--lib/test/data/2chipC.fits.gzbin183594 -> 0 bytes
-rw-r--r--lib/test/test.py277
-rw-r--r--lib/test/test_intersection.py189
-rw-r--r--lib/test/test_shared.py12
-rw-r--r--lib/test/test_skyline.py225
-rw-r--r--lib/test/test_union.py215
-rw-r--r--lib/test/test_util.py14
-rw-r--r--lib/vector.py243
17 files changed, 0 insertions, 3328 deletions
diff --git a/lib/__init__.py b/lib/__init__.py
deleted file mode 100644
index 6a03273..0000000
--- a/lib/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-import sys
-
-if sys.version_info[0] >= 3:
- # Python 3 compatibility
- __builtins__['xrange'] = range
diff --git a/lib/graph.py b/lib/graph.py
deleted file mode 100644
index d8ccf75..0000000
--- a/lib/graph.py
+++ /dev/null
@@ -1,909 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2011 Association of Universities for Research in
-# Astronomy (AURA)
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-#
-# 1. Redistributions of source code must retain the above
-# copyright notice, this list of conditions and the following
-# disclaimer.
-#
-# 2. Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following
-# disclaimer in the documentation and/or other materials
-# provided with the distribution.
-#
-# 3. The name of AURA and its representatives may not be used to
-# endorse or promote products derived from this software without
-# specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR
-# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT,
-# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
-# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
-# OF THE POSSIBILITY OF SUCH DAMAGE.
-
-"""
-This contains the code that does the actual unioning of regions.
-"""
-# TODO: Weak references for memory management problems?
-from __future__ import absolute_import, division, unicode_literals, print_function
-
-# STDLIB
-import itertools
-import weakref
-
-# THIRD-PARTY
-import numpy as np
-
-# LOCAL
-from . import great_circle_arc
-from . import vector
-
-# Set to True to enable some sanity checks
-DEBUG = True
-
-
-class Graph:
- """
- A graph of nodes connected by edges. The graph is used to build
- unions between polygons.
-
- .. note::
- This class is not meant to be used directly. Instead, use
- `~sphere.polygon.SphericalPolygon.union` and
- `~sphere.polygon.SphericalPolygon.intersection`.
- """
-
- class Node:
- """
- 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=[]):
- """
- Parameters
- ----------
- point : 3-sequence (*x*, *y*, *z*) coordinate
-
- source_polygon : `~sphere.polygon.SphericalPolygon` instance, optional
- The polygon(s) this node came from. Used for bookkeeping.
- """
- point = vector.normalize_vector(*point)
-
- self._point = np.asanyarray(point)
- self._source_polygons = set(source_polygons)
- self._edges = weakref.WeakSet()
-
- def __repr__(self):
- return "Node(%s %d)" % (str(self._point), len(self._edges))
-
- def follow(self, edge):
- """
- Follows from one edge to another across this node.
-
- Parameters
- ----------
- edge : `~Graph.Edge` instance
- The edge to follow away from.
-
- Returns
- -------
- other : `~Graph.Edge` instance
- The other edge.
- """
- edges = list(self._edges)
- assert len(edges) == 2
- try:
- return edges[not edges.index(edge)]
- except IndexError:
- raise ValueError("Following from disconnected edge")
-
- def equals(self, other, thres=2e-8):
- """
- Returns `True` if the location of this and the *other*
- `~Graph.Node` are the same.
-
- Parameters
- ----------
- 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
- the actual sizes of polygons is not implemented.
- """
- # return np.array_equal(self._point, other._point)
- return great_circle_arc.length(self._point, other._point,
- degrees=False) < thres
-
-
- class Edge:
- """
- 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 : `~Graph.Node` instances
-
- source_polygon : `~sphere.polygon.SphericalPolygon` instance, optional
- The polygon this edge came from. Used for bookkeeping.
- """
- self._nodes = [A, B]
- for node in self._nodes:
- node._edges.add(self)
- self._source_polygons = set(source_polygons)
-
- def __repr__(self):
- nodes = self._nodes
- return "Edge(%s -> %s)" % (nodes[0]._point, nodes[1]._point)
-
- def follow(self, node):
- """
- Follow along the edge from the given *node* to the other
- node.
-
- Parameters
- ----------
- node : `~Graph.Node` instance
-
- Returns
- -------
- other : `~Graph.Node` instance
- """
- nodes = self._nodes
- try:
- return nodes[not nodes.index(node)]
- except IndexError:
- raise ValueError("Following from disconnected node")
-
- def equals(self, other):
- """
- Returns `True` if the other edge is between the same two nodes.
-
- Parameters
- ----------
- other : `~Graph.Edge` instance
-
- Returns
- -------
- equals : bool
- """
- if (self._nodes[0].equals(other._nodes[0]) and
- self._nodes[1].equals(other._nodes[1])):
- return True
- if (self._nodes[1].equals(other._nodes[0]) and
- self._nodes[0].equals(other._nodes[1])):
- return True
- return False
-
-
- def __init__(self, polygons):
- """
- Parameters
- ----------
- polygons : sequence of `~sphere.polygon.SphericalPolygon` instances
- Build a graph from this initial set of polygons.
- """
- self._nodes = set()
- self._edges = set()
- self._source_polygons = set()
- self._start_node = None
-
- self.add_polygons(polygons)
-
- def add_polygons(self, polygons):
- """
- Add more polygons to the graph.
-
- .. note::
- Must be called before `union` or `intersection`.
-
- Parameters
- ----------
- polygons : sequence of `~sphere.polygon.SphericalPolygon` instances
- Set of polygons to add to the graph
- """
- for polygon in polygons:
- self.add_polygon(polygon)
-
- def add_polygon(self, polygon):
- """
- Add a single polygon to the graph.
-
- .. note::
- Must be called before `union` or `intersection`.
-
- Parameters
- ----------
- polygon : `~sphere.polygon.SphericalPolygon` instance
- Polygon to add to the graph
- """
- points = polygon._points
-
- if len(points) < 3:
- raise ValueError("Too few points in polygon")
-
- self._source_polygons.add(polygon)
-
- start_node = nodeA = self.add_node(points[0], [polygon])
- if self._start_node is None:
- self._start_node = start_node
- for i in range(1, len(points) - 1):
- nodeB = self.add_node(points[i], [polygon])
- # Don't create self-pointing edges
- if nodeB is not nodeA:
- self.add_edge(nodeA, nodeB, [polygon])
- nodeA = nodeB
- # Close the polygon
- self.add_edge(nodeA, start_node, [polygon])
-
- def add_node(self, point, source_polygons=[]):
- """
- Add a node to the graph. It will be disconnected until used
- in a call to `add_edge`.
-
- Parameters
- ----------
- point : 3-sequence (*x*, *y*, *z*) coordinate
-
- source_polygon : `~sphere.polygon.SphericalPolygon` instance, optional
- The polygon this node came from. Used for bookkeeping.
-
- Returns
- -------
- node : `~Graph.Node` instance
- The new node
- """
- new_node = self.Node(point, source_polygons)
-
- # Don't add nodes that already exist. Update the existing
- # node's source_polygons list to include the new polygon.
- for node in self._nodes:
- if node.equals(new_node):
- node._source_polygons.update(source_polygons)
- return node
-
- self._nodes.add(new_node)
- return new_node
-
- def remove_node(self, node):
- """
- Removes a node and all of the edges that touch it.
-
- .. note::
- It is assumed that *Node* is already a part of the graph.
-
- Parameters
- ----------
- node : `~Graph.Node` instance
- """
- assert node in self._nodes
-
- for edge in list(node._edges):
- nodeB = edge.follow(node)
- nodeB._edges.remove(edge)
- self._edges.remove(edge)
- self._nodes.remove(node)
-
- def add_edge(self, A, B, source_polygons=[]):
- """
- Add an edge between two nodes.
-
- .. note::
- It is assumed both nodes already belong to the graph.
-
- Parameters
- ----------
- A, B : `~Graph.Node` instances
-
- source_polygons : `~sphere.polygon.SphericalPolygon` instance, optional
- The polygon(s) this edge came from. Used for bookkeeping.
-
- Returns
- -------
- edge : `~Graph.Edge` instance
- The new edge
- """
- assert A in self._nodes
- assert B in self._nodes
-
- # Don't add any edges that already exist. Update the edge's
- # source polygons list to include the new polygon. Care needs
- # to be taken here to not create an Edge until we know we need
- # one, otherwise the Edge will get hooked up to the nodes but
- # be orphaned.
- for edge in self._edges:
- if ((A.equals(edge._nodes[0]) and
- B.equals(edge._nodes[1])) or
- (B.equals(edge._nodes[0]) and
- A.equals(edge._nodes[1]))):
- edge._source_polygons.update(source_polygons)
- return edge
-
- new_edge = self.Edge(A, B, source_polygons)
- self._edges.add(new_edge)
- return new_edge
-
- def remove_edge(self, edge):
- """
- Remove an edge from the graph. The nodes it points to remain intact.
-
- .. note::
- It is assumed that *edge* is already a part of the graph.
-
- Parameters
- ----------
- edge : `~Graph.Edge` instance
- """
- assert edge in self._edges
-
- A, B = edge._nodes
- A._edges.remove(edge)
- if len(A._edges) == 0:
- self.remove_node(A)
- if A is not B:
- B._edges.remove(edge)
- if len(B._edges) == 0:
- self.remove_node(B)
- self._edges.remove(edge)
-
- def split_edge(self, edge, node):
- """
- 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 : `~Graph.Edge` instance
- The edge to split
-
- node : `~Graph.Node` instance
- The node to insert
-
- Returns
- -------
- edgeA, edgeB : `~Graph.Edge` instances
- The two new edges on either side of *node*.
- """
- assert edge in self._edges
- assert node in self._nodes
-
- A, B = edge._nodes
- edgeA = self.add_edge(A, node, edge._source_polygons)
- edgeB = self.add_edge(node, B, edge._source_polygons)
- if edge not in (edgeA, edgeB):
- self.remove_edge(edge)
- return [edgeA, edgeB]
-
- def _sanity_check(self, title, node_is_2=False):
- """
- For debugging purposes: assert that edges and nodes are
- connected to each other correctly and there are no orphaned
- edges or nodes.
- """
- if not DEBUG:
- return
-
- try:
- unique_edges = set()
- for edge in self._edges:
- for node in edge._nodes:
- assert edge in node._edges
- assert node in self._nodes
- edge_repr = [tuple(x._point) for x in edge._nodes]
- edge_repr.sort()
- edge_repr = tuple(edge_repr)
- # assert edge_repr not in unique_edges
- unique_edges.add(edge_repr)
-
- for node in self._nodes:
- if node_is_2:
- assert len(node._edges) == 2
- else:
- assert len(node._edges) >= 2
- for edge in node._edges:
- assert node in edge._nodes
- assert edge in self._edges
- except AssertionError as e:
- import traceback
- traceback.print_exc()
- self._dump_graph(title=title)
- raise
-
- def _dump_graph(self, title=None, lon_0=0, lat_0=90,
- projection='vandg', func=lambda x: len(x._edges)):
- from mpl_toolkits.basemap import Basemap
- from matplotlib import pyplot as plt
- fig = plt.figure()
- m = Basemap()
-
- counts = {}
- for node in self._nodes:
- count = func(node)
- counts.setdefault(count, [])
- counts[count].append(list(node._point))
-
- minx = np.inf
- miny = np.inf
- maxx = -np.inf
- maxy = -np.inf
- for k, v in counts.items():
- v = np.array(v)
- ra, dec = vector.vector_to_radec(v[:, 0], v[:, 1], v[:, 2])
- x, y = m(ra, dec)
- m.plot(x, y, 'o', label=str(k))
- for x0 in x:
- minx = min(x0, minx)
- maxx = max(x0, maxx)
- for y0 in y:
- miny = min(y0, miny)
- maxy = max(y0, maxy)
-
- for edge in list(self._edges):
- A, B = [x._point for x in edge._nodes]
- r0, d0 = vector.vector_to_radec(A[0], A[1], A[2])
- r1, d1 = vector.vector_to_radec(B[0], B[1], B[2])
- m.drawgreatcircle(r0, d0, r1, d1, color='blue')
-
- plt.xlim(minx, maxx)
- plt.ylim(miny, maxy)
- if title:
- plt.title("%s, %d v, %d e" % (
- title, len(self._nodes), len(self._edges)))
- plt.legend()
- plt.show()
-
- def union(self):
- """
- Once all of the polygons have been added to the graph,
- join the polygons together.
-
- Returns
- -------
- points : Nx3 array of (*x*, *y*, *z*) points
- This is a list of points outlining the union of the
- polygons that were given to the constructor. If the
- original polygons are disjunct or contain holes, cut lines
- will be included in the output.
- """
- self._remove_cut_lines()
- self._sanity_check("union - remove cut lines")
- self._find_all_intersections()
- self._sanity_check("union - find all intersections")
- self._remove_interior_edges()
- self._sanity_check("union - remove interior edges")
- self._remove_3ary_edges()
- self._sanity_check("union - remove 3ary edges")
- self._remove_orphaned_nodes()
- self._sanity_check("union - remove orphan nodes", True)
- return self._trace()
-
- def intersection(self):
- """
- Once all of the polygons have been added to the graph,
- calculate the intersection.
-
- Returns
- -------
- points : Nx3 array of (*x*, *y*, *z*) points
- This is a list of points outlining the intersection of the
- polygons that were given to the constructor. If the
- resulting polygons are disjunct or contain holes, cut lines
- will be included in the output.
- """
- self._remove_cut_lines()
- self._sanity_check("intersection - remove cut lines")
- self._find_all_intersections()
- self._sanity_check("intersection - find all intersections")
- self._remove_exterior_edges()
- self._sanity_check("intersection - remove exterior edges")
- self._remove_3ary_edges(large_first=True)
- self._sanity_check("intersection - remove 3ary edges")
- self._remove_orphaned_nodes()
- self._sanity_check("intersection - remove orphan nodes", True)
- return self._trace()
-
- def _remove_cut_lines(self):
- """
- Removes any cutlines that may already have existed in the
- input polygons. This is so any cutlines in the final result
- will be optimized to be as short as possible and won't
- intersect each other.
-
- This works by finding coincident edges that are reverse to
- each other, and then splicing around them.
- """
- # As this proceeds, edges are removed from the graph. It
- # iterates over a static list of all edges that exist at the
- # start, so each time one is selected, we need to ensure it
- # still exists as part of the graph.
-
- # This transforms the following (where = is the cut line)
- #
- # \ /
- # A' + + B'
- # | |
- # A +====================+ B
- #
- # D +====================+ C
- # | |
- # D' + + C'
- # / \
- #
- # to this:
- #
- # \ /
- # A' + + B'
- # | |
- # A + + C
- # | |
- # D' + + C'
- # / \
- #
-
- edges = list(self._edges)
- for i in xrange(len(edges)):
- AB = edges[i]
- if AB not in self._edges:
- continue
- A, B = AB._nodes
- for j in xrange(i + 1, len(edges)):
- CD = edges[j]
- if CD not in self._edges:
- continue
- C, D = CD._nodes
- # To be a cutline, the candidate edges need to run in
- # the opposite direction, hence A == D and B == C, not
- # A == C and B == D.
- if (A.equals(D) and B.equals(C)):
- # Create new edges A -> D' and C -> B'
- self.add_edge(
- A, D.follow(CD).follow(D),
- AB._source_polygons | CD._source_polygons)
- self.add_edge(
- C, B.follow(AB).follow(B),
- AB._source_polygons | CD._source_polygons)
-
- # Remove B and D which are identical to C and A
- # respectively. We do not need to remove AB and
- # CD because this will remove it for us.
- self.remove_node(D)
- self.remove_node(B)
-
- break
-
- def _find_all_intersections(self):
- """
- Find all the intersecting edges in the graph. For each
- intersecting pair, four new edges are created around the
- intersection point.
- """
- def get_edge_points(edges):
- return (np.array([x._nodes[0]._point for x in edges]),
- np.array([x._nodes[1]._point for x in edges]))
-
- # For speed, we want to vectorize all of the intersection
- # calculations. Therefore, there is a list of edges, and two
- # arrays containing the end points of those edges. They all
- # need to have things added and removed from them at the same
- # time to keep them in sync, but of course the interface for
- # doing so is different between Python lists and numpy arrays.
-
- edges = list(self._edges)
- starts, ends = get_edge_points(edges)
-
- # First, split all edges by any nodes that intersect them
- while len(edges) > 1:
- AB = edges.pop(0)
- A = starts[0]; starts = starts[1:] # numpy equiv of "pop(0)"
- B = ends[0]; ends = ends[1:] # numpy equiv of "pop(0)"
-
- distance = great_circle_arc.length(A, B)
- for node in self._nodes:
- if node not in AB._nodes:
- distanceA = great_circle_arc.length(node._point, A)
- distanceB = great_circle_arc.length(node._point, B)
- if np.abs((distanceA + distanceB) - distance) < 1e-8:
- newA, newB = self.split_edge(AB, node)
-
- new_edges = [
- edge for edge in (newA, newB)
- if edge not in edges]
-
- edges = new_edges + edges
- new_starts, new_ends = get_edge_points(new_edges)
- starts = np.vstack(
- (new_starts, starts))
- ends = np.vstack(
- (new_ends, ends))
- break
-
- edges = list(self._edges)
- starts, ends = get_edge_points(edges)
-
- # Next, calculate edge-to-edge intersections and break
- # edges on the intersection point.
- while len(edges) > 1:
- AB = edges.pop(0)
- A = starts[0]; starts = starts[1:] # numpy equiv of "pop(0)"
- B = ends[0]; ends = ends[1:] # numpy equiv of "pop(0)"
-
- # Calculate the intersection points between AB and all
- # other remaining edges
- with np.errstate(invalid='ignore'):
- intersections = great_circle_arc.intersection(
- A, B, starts, ends)
- # intersects is `True` everywhere intersections has an
- # actual intersection
- intersects = np.isfinite(intersections[..., 0])
-
- intersection_indices = np.nonzero(intersects)[0]
-
- # Iterate through the candidate intersections, if any --
- # we want to eliminate intersections that only intersect
- # at the end points
- for j in intersection_indices:
- C = starts[j]
- D = ends[j]
- CD = edges[j]
- E = intersections[j]
-
- # This is a bona-fide intersection, and E is the
- # point at which the two lines intersect. Make a
- # new node for it -- this must belong to the all
- # of the source polygons of both of the edges that
- # crossed.
-
- # A
- # |
- # C--E--D
- # |
- # B
-
- E = self.add_node(
- E, AB._source_polygons | CD._source_polygons)
- newA, newB = self.split_edge(AB, E)
- newC, newD = self.split_edge(CD, E)
-
- new_edges = [
- edge for edge in (newA, newB, newC, newD)
- if edge not in edges]
-
- # Delete CD, and push the new edges to the
- # front so they will be tested for intersection
- # against all remaining edges.
- del edges[j] # CD
- edges = new_edges + edges
- new_starts, new_ends = get_edge_points(new_edges)
- starts = np.vstack(
- (new_starts, starts[:j], starts[j+1:]))
- ends = np.vstack(
- (new_ends, ends[:j], ends[j+1:]))
- break
-
- def _remove_interior_edges(self):
- """
- Removes any nodes that are contained inside other polygons.
- What's left is the (possibly disjunct) outline.
- """
- polygons = self._source_polygons
-
- for edge in self._edges:
- edge._count = 0
- for polygon in polygons:
- if (not polygon in edge._source_polygons and
- polygon.intersects_arc(
- edge._nodes[0]._point, edge._nodes[1]._point)):
- edge._count += 1
-
- for edge in list(self._edges):
- if edge._count >= 1:
- self.remove_edge(edge)
-
- def _remove_exterior_edges(self):
- """
- Removes any edges that are not contained in all of the source
- polygons. What's left is the (possibly disjunct) outline.
- """
- polygons = self._source_polygons
-
- for edge in self._edges:
- edge._count = 0
- for polygon in polygons:
- if (polygon in edge._source_polygons or
- polygon.intersects_arc(
- edge._nodes[0]._point, edge._nodes[1]._point)):
- edge._count += 1
-
- for edge in list(self._edges):
- if edge._count < len(polygons):
- self.remove_edge(edge)
-
- def _remove_3ary_edges(self, large_first=False):
- """
- Remove edges between pairs of nodes that have 3 or more edges.
- This removes triangles that can't be traced.
- """
- if large_first:
- max_ary = 0
- for node in self._nodes:
- max_ary = max(len(node._edges), max_ary)
- order = range(max_ary + 1, 2, -1)
- else:
- order = [3]
-
- for i in order:
- removals = []
- for edge in list(self._edges):
- if (len(edge._nodes[0]._edges) >= i and
- len(edge._nodes[1]._edges) >= i):
- removals.append(edge)
-
- for edge in removals:
- if edge in self._edges:
- self.remove_edge(edge)
-
- def _remove_orphaned_nodes(self):
- """
- Remove nodes with fewer than 2 edges.
- """
- while True:
- removes = []
- for node in list(self._nodes):
- if len(node._edges) < 2:
- removes.append(node)
- if len(removes):
- for node in removes:
- self.remove_node(node)
- else:
- break
-
- def _trace(self):
- """
- Given a graph that has had cutlines removed and all
- intersections found, traces it to find a resulting single
- polygon.
- """
- polygons = []
- edges = set(self._edges) # copy
- seen_nodes = set()
- while len(edges):
- polygon = []
- # Carefully pick out an "original" edge first. Synthetic
- # edges may not be pointing in the right direction to
- # properly calculate the area.
- for edge in edges:
- if len(edge._source_polygons) == 1:
- break
- edges.remove(edge)
- start_node = node = edge._nodes[0]
- while True:
- # TODO: Do we need this if clause any more?
- if len(polygon):
- if not np.array_equal(polygon[-1], node._point):
- polygon.append(node._point)
- else:
- polygon.append(node._point)
- edge = node.follow(edge)
- edges.discard(edge)
- node = edge.follow(node)
- if node is start_node:
- polygon.append(node._point)
- break
-
- polygons.append(np.asarray(polygon))
-
- if len(polygons) == 1:
- return polygons[0]
- elif len(polygons) == 0:
- return []
- else:
- return self._join(polygons)
-
- def _join(self, polygons):
- """
- If the graph is disjunct, joins the parts with cutlines.
-
- The closest nodes between each pair that don't intersect
- any other edges are used as cutlines.
-
- TODO: This is not optimal, because the closest distance
- between two polygons may not in fact be between two vertices,
- but may be somewhere along an edge.
- """
- def do_join(polygons):
- all_polygons = polygons[:]
-
- skipped = 0
-
- polyA = polygons.pop(0)
- while len(polygons):
- polyB = polygons.pop(0)
-
- # If fewer than 3 edges, it's not a polygon,
- # just throw it out
- if len(polyB) < 4:
- continue
-
- # Find the closest set of vertices between polyA and
- # polyB that don't cross any of the edges in *any* of
- # the polygons
- closest = np.inf
- closest_pair_idx = (None, None)
- for a in xrange(len(polyA) - 1):
- A = polyA[a]
- distances = great_circle_arc.length(A, polyB[:-1])
- b = np.argmin(distances)
- distance = distances[b]
- if distance < closest:
- B = polyB[b]
- # Does this candidate line cross other edges?
- crosses = False
- for poly in all_polygons:
- if np.any(
- great_circle_arc.intersects(
- A, B, poly[:-1], poly[1:])):
- crosses = True
- break
- if not crosses:
- closest = distance
- closest_pair_idx = (a, b)
-
- if not np.isfinite(closest):
- # We didn't find a pair of points that don't cross
- # something else, so we want to try to join another
- # polygon. Defer the current polygon until later.
- if len(polygons) in (0, skipped):
- return None
- polygons.append(polyB)
- skipped += 1
- else:
- # Splice the two polygons together using a cut
- # line
- a, b = closest_pair_idx
- new_poly = np.vstack((
- # polyA up to and including the cut point
- polyA[:a+1],
-
- # polyB starting with the cut point and
- # wrapping around back to the cut point.
- # Ignore the last point in polyB, because it
- # is the same as the first
- np.roll(polyB[:-1], -b, 0),
-
- # The cut point on polyB
- polyB[b:b+1],
-
- # the rest of polyA, starting with the cut
- # point
- polyA[a:]
- ))
-
- skipped = 0
- polyA = new_poly
-
- return polyA
-
- for permutation in itertools.permutations(polygons):
- poly = do_join(list(permutation))
- if poly is not None:
- return poly
-
- raise RuntimeError("Could not find cut points")
diff --git a/lib/great_circle_arc.py b/lib/great_circle_arc.py
deleted file mode 100644
index d5e78f2..0000000
--- a/lib/great_circle_arc.py
+++ /dev/null
@@ -1,368 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2011 Association of Universities for Research in
-# Astronomy (AURA)
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-#
-# 1. Redistributions of source code must retain the above
-# copyright notice, this list of conditions and the following
-# disclaimer.
-#
-# 2. Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following
-# disclaimer in the documentation and/or other materials
-# provided with the distribution.
-#
-# 3. The name of AURA and its representatives may not be used to
-# endorse or promote products derived from this software without
-# specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR
-# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT,
-# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
-# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
-# OF THE POSSIBILITY OF SUCH DAMAGE.
-
-"""
-The `sphere.great_circle_arc` module contains functions for computing
-the length, intersection, angle and midpoint of great circle arcs.
-
-Great circles are circles on the unit sphere whose center is
-coincident with the center of the sphere. Great circle arcs are the
-section of those circles between two points on the unit sphere.
-"""
-
-from __future__ import with_statement, division, absolute_import, unicode_literals
-
-# THIRD-PARTY
-import numpy as np
-
-
-try:
- from . import math_util
- HAS_C_UFUNCS = True
-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']
-
-
-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 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):
- r"""
- Returns the angular distance between two points (in vector space)
- on the unit sphere.
-
- Parameters
- ----------
- A, B : (*x*, *y*, *z*) triples or Nx3 arrays of triples
- The endpoints of the great circle arc, in vector space.
-
- degrees : bool, optional
- If `True` (default) the result is returned in decimal degrees,
- otherwise radians.
-
- Returns
- -------
- length : scalar or array of scalars
- The angular length of the great circle arc.
-
- Notes
- -----
- The length is computed using the following:
-
- .. math::
-
- \Delta = \arccos(A \dot B)
- """
- A = np.asanyarray(A)
- B = np.asanyarray(B)
-
- dot = inner1d(A, B)
- dot = np.clip(dot, -1.0, 1.0)
- with np.errstate(invalid='ignore'):
- result = np.arccos(dot)
-
- if degrees:
- return np.rad2deg(result)
- else:
- return result
-
-
-def intersects(A, B, C, D):
- """
- Returns `True` if the great circle arcs between *AB* and *CD*
- intersect. 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
- -------
- intersects : bool or array of bool
- If the given arcs intersect, the intersection is returned as
- `True`.
- """
- with np.errstate(invalid='ignore'):
- intersections = intersection(A, B, C, D)
- return np.isfinite(intersections[..., 0])
-
-
-def angle(A, B, C, degrees=True):
- """
- Returns the angle at *B* between *AB* and *BC*.
-
- This always returns the shortest angle < π.
-
- Parameters
- ----------
- 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,
- otherwise radians.
-
- Returns
- -------
- angle : float or array of floats
- The angle at *B* between *AB* and *BC*.
-
- References
- ----------
-
- .. [1] Miller, Robert D. Computing the area of a spherical
- polygon. Graphics Gems IV. 1994. Academic Press.
- """
- A = np.asanyarray(A)
- B = np.asanyarray(B)
- C = np.asanyarray(C)
-
- A, B, C = np.broadcast_arrays(A, B, C)
-
- ABX = _fast_cross(A, B)
- ABX = _cross_and_normalize(B, ABX)
- BCX = _fast_cross(C, B)
- BCX = _cross_and_normalize(B, BCX)
- with np.errstate(invalid='ignore'):
- angle = np.arccos(inner1d(ABX, BCX))
-
- if degrees:
- angle = np.rad2deg(angle)
- return angle
-
-
-def midpoint(A, B):
- """
- Returns the midpoint on the great circle arc between *A* and *B*.
-
- Parameters
- ----------
- A, B : (*x*, *y*, *z*) triples or Nx3 arrays of triples
- The endpoints of the great circle arc. It is assumed that
- these points are already normalized.
-
- Returns
- -------
- midpoint : (*x*, *y*, *z*) triple or Nx3 arrays of triples
- The midpoint between *A* and *B*, normalized on the unit
- sphere.
- """
- P = (A + B) / 2.0
- # Now normalize...
- l = np.sqrt(np.sum(P * P, axis=-1))
- l = np.expand_dims(l, 2)
- return P / l
-
-
-def interpolate(A, B, steps=50):
- r"""
- Interpolate along the great circle arc.
-
- Parameters
- ----------
- A, B : (*x*, *y*, *z*) triples or Nx3 arrays of triples
- The endpoints of the great circle arc. It is assumed thats
- these points are already normalized.
-
- steps : int
- The number of interpolation steps
-
- Returns
- -------
- array : (*x*, *y*, *z*) triples
- The points interpolated along the great circle arc
-
- Notes
- -----
-
- This uses Slerp interpolation where *Ω* is the angle subtended by
- the arc, and *t* is the parameter 0 <= *t* <= 1.
-
- .. math::
-
- \frac{\sin((1 - t)\Omega)}{\sin \Omega}A + \frac{\sin(t \Omega)}{\sin \Omega}B
- """
- steps = max(steps, 2)
- t = np.linspace(0.0, 1.0, steps, endpoint=True).reshape((steps, 1))
-
- omega = length(A, B, degrees=False)
- if omega == 0.0:
- offsets = t
- else:
- sin_omega = np.sin(omega)
- offsets = np.sin(t * omega) / sin_omega
-
- return offsets[::-1] * A + offsets * B
diff --git a/lib/polygon.py b/lib/polygon.py
deleted file mode 100644
index e9ad379..0000000
--- a/lib/polygon.py
+++ /dev/null
@@ -1,831 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2011 Association of Universities for Research in
-# Astronomy (AURA)
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-#
-# 1. Redistributions of source code must retain the above
-# copyright notice, this list of conditions and the following
-# disclaimer.
-#
-# 2. Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following
-# disclaimer in the documentation and/or other materials
-# provided with the distribution.
-#
-# 3. The name of AURA and its representatives may not be used to
-# endorse or promote products derived from this software without
-# specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR
-# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT,
-# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
-# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
-# OF THE POSSIBILITY OF SUCH DAMAGE.
-
-"""
-The `polygon` module defines the `SphericalPolygon` class for managing
-polygons on the unit sphere.
-"""
-from __future__ import division, print_function, unicode_literals, absolute_import
-
-# STDLIB
-from copy import copy, deepcopy
-
-# THIRD-PARTY
-import numpy as np
-
-# LOCAL
-from . import great_circle_arc
-from . import vector
-
-__all__ = ['SphericalPolygon']
-
-
-class SphericalPolygon(object):
- r"""
- Polygons are represented by both a set of points (in Cartesian
- (*x*, *y*, *z*) normalized on the unit sphere), and an inside
- point. The inside point is necessary, because both the inside and
- outside of the polygon are finite areas on the great sphere, and
- therefore we need a way of specifying which is which.
- """
-
- def __init__(self, points, inside=None):
- r"""
- Parameters
- ----------
- points : An Nx3 array of (*x*, *y*, *z*) triples in vector space
- These points define the boundary of the polygon. It must
- be "closed", i.e., the last point is the same as the first.
-
- It may contain zero points, in which it defines the null
- polygon. It may not contain one, two or three points.
- Four points are needed to define a triangle, since the
- polygon must be closed.
-
- inside : An (*x*, *y*, *z*) triple, optional
- This point must be inside the polygon. If not provided, the
- mean of the points will be used.
- """
- if len(points) == 0:
- # handle special case of initializing with an empty list of
- # vertices (ticket #1079).
- self._inside = np.zeros(3)
- self._points = np.asanyarray(points)
- return
- elif len(points) < 3:
- raise ValueError("Polygon made of too few points")
- else:
- assert np.array_equal(points[0], points[-1]), 'Polygon is not closed'
-
- self._points = np.asanyarray(points)
-
- if inside is None:
- self._inside = np.mean(points[:-1], axis=0)
- else:
- self._inside = np.asanyarray(inside)
-
- # TODO: Detect self-intersection and fix
-
- def __copy__(self):
- return deepcopy(self)
-
- def __repr__(self):
- return '%s(%r, %r)' % (self.__class__.__name__,
- self.points, self.inside)
-
- @property
- def points(self):
- """
- The points defining the polygon. It is an Nx3 array of
- (*x*, *y*, *z*) vectors. The polygon will be explicitly
- closed, i.e., the first and last points are the same.
- """
- return self._points
-
- @property
- def inside(self):
- """
- Get the inside point of the polygon.
- """
- return self._inside
-
- def to_radec(self):
- """
- Convert `SphericalPolygon` footprint to RA and DEC.
-
- Returns
- -------
- ra, dec : list of float
- List of *ra* and *dec* in degrees corresponding
- to `points`.
- """
- return vector.vector_to_radec(self.points[:,0], self.points[:,1],
- self.points[:,2], degrees=True)
-
- @classmethod
- def from_radec(cls, ra, dec, center=None, degrees=True):
- r"""
- Create a new `SphericalPolygon` from a list of (*ra*, *dec*)
- points.
-
- Parameters
- ----------
- ra, dec : 1-D arrays of the same length
- The vertices of the polygon in right-ascension and
- declination. It must be \"closed\", i.e., that is, the
- last point is the same as the first.
-
- center : (*ra*, *dec*) pair, optional
- A point inside of the polygon to define its inside. If no
- *center* point is provided, the mean of the polygon's
- points in vector space will be used. That approach may
- not work for concave polygons.
-
- degrees : bool, optional
- If `True`, (default) *ra* and *dec* are in decimal degrees,
- otherwise in radians.
-
- Returns
- -------
- polygon : `SphericalPolygon` object
- """
- # Convert to Cartesian
- x, y, z = vector.radec_to_vector(ra, dec, degrees=degrees)
-
- if center is None:
- xc = x.mean()
- yc = y.mean()
- zc = z.mean()
- center = vector.normalize_vector(xc, yc, zc)
- else:
- center = vector.radec_to_vector(*center, degrees=degrees)
-
- return cls(np.dstack((x, y, z))[0], center)
-
- @classmethod
- def from_cone(cls, ra, dec, radius, degrees=True, steps=16.0):
- r"""
- Create a new `SphericalPolygon` from a cone (otherwise known
- as a "small circle") defined using (*ra*, *dec*, *radius*).
-
- The cone is not represented as an ideal circle on the sphere,
- but as a series of great circle arcs. The resolution of this
- conversion can be controlled using the *steps* parameter.
-
- Parameters
- ----------
- ra, dec : float scalars
- This defines the center of the cone
-
- radius : float scalar
- The radius of the cone
-
- degrees : bool, optional
- If `True`, (default) *ra*, *dec* and *radius* are in
- decimal degrees, otherwise in radians.
-
- steps : int, optional
- The number of steps to use when converting the small
- circle to a polygon.
-
- Returns
- -------
- polygon : `SphericalPolygon` object
- """
- u, v, w = vector.radec_to_vector(ra, dec, degrees=degrees)
- if degrees:
- radius = np.deg2rad(radius)
-
- # Get an arbitrary perpendicular vector. This be be obtained
- # by crossing (u, v, w) with any unit vector that is not itself.
- which_min = np.argmin([u, v, w])
- if which_min == 0:
- perp = np.cross([u, v, w], [1., 0., 0.])
- elif which_min == 1:
- perp = np.cross([u, v, w], [0., 1., 0.])
- else:
- perp = np.cross([u, v, w], [0., 0., 1.])
-
- # Rotate by radius around the perpendicular vector to get the
- # "pen"
- x, y, z = vector.rotate_around(
- u, v, w, perp[0], perp[1], perp[2], radius, degrees=False)
-
- # Then rotate the pen around the center point all 360 degrees
- C = np.linspace(0, np.pi * 2.0, steps)
- # Ensure that the first and last elements are exactly the
- # same. 2π should equal 0, but with rounding error that isn't
- # always the case.
- C[-1] = 0
- C = C[::-1]
- X, Y, Z = vector.rotate_around(x, y, z, u, v, w, C, degrees=False)
-
- return cls(np.dstack((X, Y, Z))[0], (u, v, w))
-
- @classmethod
- def from_wcs(cls, fitspath, steps=1, crval=None):
- r"""
- Create a new `SphericalPolygon` from the footprint of a FITS
- WCS specification.
-
- This method requires having `pywcs` and `pyfits` installed.
-
- Parameters
- ----------
- fitspath : path to a FITS file, `pyfits.Header`, or `pywcs.WCS`
- Refers to a FITS header containing a WCS specification.
-
- steps : int, optional
- The number of steps along each edge to convert into
- polygon edges.
-
- Returns
- -------
- polygon : `SphericalPolygon` object
- """
- import pywcs
- import pyfits
-
- if isinstance(fitspath, pyfits.Header):
- header = fitspath
- wcs = pywcs.WCS(header)
- elif isinstance(fitspath, pywcs.WCS):
- wcs = fitspath
- else:
- wcs = pywcs.WCS(fitspath)
- if crval is not None:
- wcs.wcs.crval = crval
- xa, ya = [wcs.naxis1, wcs.naxis2]
-
- length = steps * 4 + 1
- X = np.empty(length)
- Y = np.empty(length)
-
- # Now define each of the 4 edges of the quadrilateral
- X[0 :steps ] = np.linspace(1, xa, steps, False)
- Y[0 :steps ] = 1
- X[steps :steps*2] = xa
- Y[steps :steps*2] = np.linspace(1, ya, steps, False)
- X[steps*2:steps*3] = np.linspace(xa, 1, steps, False)
- Y[steps*2:steps*3] = ya
- X[steps*3:steps*4] = 1
- Y[steps*3:steps*4] = np.linspace(ya, 1, steps, False)
- X[-1] = 1
- Y[-1] = 1
-
- # Use wcslib to convert to (ra, dec)
- ra, dec = wcs.all_pix2sky(X, Y, 1)
-
- # Convert to Cartesian
- x, y, z = vector.radec_to_vector(ra, dec)
-
- # Calculate an inside point
- ra, dec = wcs.all_pix2sky(xa / 2.0, ya / 2.0, 1)
- xc, yc, zc = vector.radec_to_vector(ra, dec)
-
- return cls(np.dstack((x, y, z))[0], (xc[0], yc[0], zc[0]))
-
- def _unique_points(self):
- """
- Return a copy of `points` with duplicates removed.
- Order is preserved.
-
- .. note:: Output cannot be used to build a new
- polygon.
- """
- val = []
- for p in self.points:
- v = tuple(p)
- if v not in val:
- val.append(v)
- return np.array(val)
-
- def _sorted_points(self, preserve_order=True, unique=False):
- """
- Return a copy of `points` sorted such that smallest
- (*x*, *y*, *z*) is on top.
-
- .. note:: Output cannot be used to build a new
- polygon.
-
- Parameters
- ----------
- preserve_order : bool
- Preserve original order? If `True`, polygon is
- rotated around min point. If `False`, all points
- are sorted in ascending order.
-
- unique : bool
- Exclude duplicates.
- """
- if len(self.points) == 0:
- return []
-
- if unique:
- pts = self._unique_points()
- else:
- pts = self.points
-
- idx = np.lexsort((pts[:,0], pts[:,1], pts[:,2]))
-
- if preserve_order:
- i_min = idx[0]
- val = np.vstack([pts[i_min:], pts[:i_min]])
- else:
- val = pts[idx]
-
- return val
-
- def same_points_as(self, other, do_sort=True, thres=0.01):
- """
- Determines if this `SphericalPolygon` points are the same
- as the other. Number of points and areas are also compared.
-
- When `do_sort` is `True`, even when *self* and *other*
- have same points, they might not be equivalent because
- the order of the points defines the polygon.
-
- Parameters
- ----------
- other : `SphericalPolygon`
-
- do_sort : bool
- Compare sorted unique points.
-
- thres : float
- Fraction of area to use in equality decision.
-
- Returns
- -------
- is_eq : bool
- `True` or `False`.
- """
- self_n = len(self.points)
-
- if self_n != len(other.points):
- return False
-
- if self_n == 0:
- return True
-
- self_a = self.area()
- is_same_limit = thres * self_a
-
- if np.abs(self_a - other.area()) > is_same_limit:
- return False
-
- if do_sort:
- self_pts = self._sorted_points(preserve_order=False, unique=True)
- other_pts = other._sorted_points(preserve_order=False, unique=True)
- else:
- self_pts = self.points
- other_pts = other.points
-
- is_eq = True
-
- for self_p, other_p in zip(self_pts, other_pts):
- x_sum = 0.0
-
- for a,b in zip(self_p, other_p):
- x_sum += (a - b) ** 2
-
- if np.sqrt(x_sum) > is_same_limit:
- is_eq = False
- break
-
- return is_eq
-
- def contains_point(self, point):
- r"""
- Determines if this `SphericalPolygon` contains a given point.
-
- Parameters
- ----------
- point : an (*x*, *y*, *z*) triple
- The point to test.
-
- Returns
- -------
- contains : bool
- Returns `True` if the polygon contains the given *point*.
- """
- P = self._points
- r = self._inside
- point = np.asanyarray(point)
-
- intersects = great_circle_arc.intersects(P[:-1], P[1:], r, point)
- crossings = np.sum(intersects)
-
- return (crossings % 2) == 0
-
- def intersects_poly(self, other):
- r"""
- Determines if this `SphericalPolygon` intersects another
- `SphericalPolygon`.
-
- This method is much faster than actually computing the
- intersection region between two polygons.
-
- Parameters
- ----------
- other : `SphericalPolygon`
-
- Returns
- -------
- intersects : bool
- Returns `True` if this polygon intersects the *other*
- polygon.
-
- Notes
- -----
-
- The algorithm proceeds as follows:
-
- 1. Determine if any single point of one polygon is contained
- within the other.
-
- 2. Deal with the case where only the edges overlap as in::
-
- : o---------o
- : o----+---------+----o
- : | | | |
- : o----+---------+----o
- : o---------o
-
- In this case, an edge from one polygon must cross an
- edge from the other polygon.
- """
- assert isinstance(other, SphericalPolygon)
-
- # The easy case is in which a point of one polygon is
- # contained in the other polygon.
- for point in other._points:
- if self.contains_point(point):
- return True
- for point in self._points:
- if other.contains_point(point):
- return True
-
- # The hard case is when only the edges overlap, as in:
- #
- # o---------o
- # o----+---------+----o
- # | | | |
- # o----+---------+----o
- # o---------o
- #
- for i in range(len(self._points) - 1):
- A = self._points[i]
- B = self._points[i+1]
- if np.any(great_circle_arc.intersects(
- A, B, other._points[:-1], other._points[1:])):
- return True
- return False
-
- def intersects_arc(self, a, b):
- """
- Determines if this `SphericalPolygon` intersects or contains
- the given arc.
- """
- return self.contains_point(great_circle_arc.midpoint(a, b))
-
- if self.contains_point(a) and self.contains_point(b):
- return True
-
- P = self._points
- intersects = great_circle_arc.intersects(P[:-1], P[1:], a, b)
- if np.any(intersects):
- return True
- return False
-
- def area(self):
- r"""
- Returns the area of the polygon on the unit sphere.
-
- The algorithm is not able to compute the area of polygons
- that are larger than half of the sphere. Therefore, the
- area will always be less than 2π.
-
- The area is computed by transforming the polygon to two
- dimensions using the `Lambert azimuthal equal-area projection
- <http://en.wikipedia.org/wiki/Lambert_azimuthal_equal-area_projection>`_
-
- .. math::
-
- X = \sqrt{\frac{2}{1-z}}x
-
- .. math::
-
- Y = \sqrt{\frac{2}{1-z}}y
-
- The individual great arc circle segments are interpolated
- before doing the transformation so that the curves are not
- straightened in the process.
-
- It then uses a standard 2D algorithm to compute the area.
-
- .. math::
-
- A = \left| \sum^n_{i=0} X_i Y_{i+1} - X_{i+1}Y_i \right|
- """
- if len(self._points) < 3:
- #return np.float64(0.0)
- return np.array(0.0)
-
- points = self._points.copy()
-
- # Rotate polygon so that center of polygon is at north pole
- centroid = np.mean(points[:-1], axis=0)
- centroid = vector.normalize_vector(*centroid)
- points = self._points - (centroid + np.array([0, 0, 1]))
- vector.normalize_vector(
- points[:, 0], points[:, 1], points[:, 2], inplace=True)
-
- X = []
- Y = []
- for A, B in zip(points[:-1], points[1:]):
- length = great_circle_arc.length(A, B, degrees=True)
- interp = great_circle_arc.interpolate(A, B, length * 4)
- x, y, z = vector.normalize_vector(
- interp[:, 0], interp[:, 1], interp[:, 2], inplace=True)
- x, y = vector.equal_area_proj(x, y, z)
- X.extend(x)
- Y.extend(y)
-
- X = np.array(X)
- Y = np.array(Y)
-
- return np.abs(np.sum(X[:-1] * Y[1:] - X[1:] * Y[:-1]) * 0.5 * np.pi)
-
- def union(self, other):
- """
- Return a new `SphericalPolygon` that is the union of *self*
- and *other*.
-
- If the polygons are disjoint, they result will be connected
- using cut lines. For example::
-
- : o---------o
- : | |
- : o---------o=====o----------o
- : | |
- : o----------o
-
- Parameters
- ----------
- other : `SphericalPolygon`
-
- Returns
- -------
- polygon : `SphericalPolygon` object
-
- See also
- --------
- multi_union
-
- Notes
- -----
- For implementation details, see the :mod:`~sphere.graph`
- module.
- """
- from . import graph
- g = graph.Graph([self, other])
-
- polygon = g.union()
-
- return self.__class__(polygon, self._inside)
-
- @classmethod
- def multi_union(cls, polygons):
- """
- Return a new `SphericalPolygon` that is the union of all of the
- polygons in *polygons*.
-
- Parameters
- ----------
- polygons : sequence of `SphericalPolygon`
-
- Returns
- -------
- polygon : `SphericalPolygon` object
-
- See also
- --------
- union
- """
- assert len(polygons)
- for polygon in polygons:
- assert isinstance(polygon, SphericalPolygon)
-
- from . import graph
-
- g = graph.Graph(polygons)
- polygon = g.union()
- return cls(polygon, polygons[0]._inside)
-
- @staticmethod
- def _find_new_inside(points, polygons):
- """
- Finds an acceptable inside point inside of *points* that is
- also inside of *polygons*. Used by the intersection
- algorithm, and is really only useful in that context because
- it requires existing polygons with known inside points.
- """
- if len(points) < 4:
- return np.array([0, 0, 0])
-
- # Special case for a triangle
- if len(points) == 4:
- return np.sum(points[:3]) / 3.0
-
- for i in range(len(points) - 1):
- A = points[i]
- # Skip the adjacent point, since it is by definition on
- # the edge of the polygon, not potentially running through
- # the middle.
- for j in range(i + 2, len(points) - 1):
- B = points[j]
- C = great_circle_arc.midpoint(A, B)
- in_all = True
- for polygon in polygons:
- if not polygon.contains_point(C):
- in_all = False
- break
- if in_all:
- return C
-
- raise RuntimeError("Suitable inside point could not be found")
-
- def intersection(self, other):
- """
- Return a new `SphericalPolygon` that is the intersection of
- *self* and *other*.
-
- If the intersection is empty, a `SphericalPolygon` with zero
- points will be returned.
-
- If the result is disjoint, the pieces will be connected using
- cut lines. For example::
-
- : o---------o
- : | |
- : o---------o=====o----------o
- : | |
- : o----------o
-
- Parameters
- ----------
- other : `SphericalPolygon`
-
- Returns
- -------
- polygon : `SphericalPolygon` object
-
- Notes
- -----
- For implementation details, see the :mod:`~sphere.graph`
- module.
- """
- # if not self.intersects_poly(other):
- # return self.__class__([], [0, 0, 0])
-
- from . import graph
- if len(self._points) < 3 or len(other._points) < 3:
- return self.__class__([], [0, 0, 0])
-
- g = graph.Graph([self, other])
-
- polygon = g.intersection()
-
- inside = self._find_new_inside(polygon, [self, other])
-
- return self.__class__(polygon, inside)
-
- @classmethod
- def multi_intersection(cls, polygons, method='parallel'):
- """
- Return a new `SphericalPolygon` that is the intersection of
- all of the polygons in *polygons*.
-
- Parameters
- ----------
- polygons : sequence of `SphericalPolygon`
-
- method : 'parallel' or 'serial', optional
- Specifies the method that is used to perform the
- intersections:
-
- - 'parallel' (default): A graph is built using all of
- the polygons, and the intersection operation is computed on
- the entire thing globally.
-
- - 'serial': The polygon is built in steps by adding one
- polygon at a time and computing the intersection at
- each step.
-
- This option is provided because one may be faster than the
- other depending on context, but it primarily exposed for
- testing reasons. Both modes should theoretically provide
- equivalent results.
-
- Returns
- -------
- polygon : `SphericalPolygon` object
- """
- assert len(polygons)
- for polygon in polygons:
- assert isinstance(polygon, SphericalPolygon)
-
- # for i in range(len(polygons)):
- # polyA = polygons[i]
- # for j in range(i + 1, len(polygons)):
- # polyB = polygons[j]
- # if not polyA.intersects_poly(polyB):
- # return cls([], [0, 0, 0])
-
- from . import graph
-
- if method.lower() == 'parallel':
- g = graph.Graph(polygons)
- polygon = g.intersection()
- inside = cls._find_new_inside(polygon, polygons)
- return cls(polygon, inside)
- elif method.lower() == 'serial':
- result = copy(polygons[0])
- for polygon in polygons[1:]:
- result = result.intersection(polygon)
- # If we have a null intersection already, we don't
- # need to go any further.
- if len(result._points) < 3:
- return result
- return result
- else:
- raise ValueError("method must be 'parallel' or 'serial'")
-
- def overlap(self, other):
- r"""
- Returns the fraction of *self* that is overlapped by *other*.
-
- Let *self* be *a* and *other* be *b*, then the overlap is
- defined as:
-
- .. math::
-
- \frac{S_a}{S_{a \cap b}}
-
- Parameters
- ----------
- other : `SphericalPolygon`
-
- Returns
- -------
- frac : float
- The fraction of *self* that is overlapped by *other*.
- """
- s1 = self.area()
- intersection = self.intersection(other)
- s2 = intersection.area()
- return s2 / s1
-
- def draw(self, m, **plot_args):
- """
- Draws the polygon in a matplotlib.Basemap axes.
-
- Parameters
- ----------
- m : Basemap axes object
-
- **plot_args : Any plot arguments to pass to basemap
- """
- if not len(self._points):
- return
- if not len(plot_args):
- plot_args = {'color': 'blue'}
- points = self._points
-
- for A, B in zip(points[0:-1], points[1:]):
- length = great_circle_arc.length(A, B, degrees=True)
- if not np.isfinite(length):
- length = 2
- interpolated = great_circle_arc.interpolate(A, B, length * 4)
- ra, dec = vector.vector_to_radec(
- interpolated[:, 0], interpolated[:, 1], interpolated[:, 2],
- degrees=True)
- for r0, d0, r1, d1 in zip(ra[0:-1], dec[0:-1], ra[1:], dec[1:]):
- m.drawgreatcircle(r0, d0, r1, d1, **plot_args)
-
- ra, dec = vector.vector_to_radec(
- *self._inside, degrees=True)
- x, y = m(ra, dec)
- m.scatter(x, y, 1, **plot_args)
diff --git a/lib/test/__init__.py b/lib/test/__init__.py
deleted file mode 100644
index 8b13789..0000000
--- a/lib/test/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/lib/test/benchmarks.py b/lib/test/benchmarks.py
deleted file mode 100644
index 91ef8db..0000000
--- a/lib/test/benchmarks.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import os
-import sys
-import time
-
-import numpy as np
-from sphere import *
-from test_util import *
-from test_shared import resolve_imagename
-
-def point_in_poly_lots():
- image_name = resolve_imagename(ROOT_DIR,'1904-66_TAN.fits')
-
- poly1 = SphericalPolygon.from_wcs(image_name, 64, crval=[0, 87])
- poly2 = SphericalPolygon.from_wcs(image_name, 64, crval=[20, 89])
- poly3 = SphericalPolygon.from_wcs(image_name, 64, crval=[180, 89])
-
- points = get_point_set(density=25)
-
- count = 0
- for point in points:
- if poly1.contains_point(point) or poly2.contains_point(point) or \
- poly3.contains_point(point):
- count += 1
-
- assert count == 5
- assert poly1.intersects_poly(poly2)
- assert not poly1.intersects_poly(poly3)
- assert not poly2.intersects_poly(poly3)
-
-if __name__ == '__main__':
- for benchmark in [point_in_poly_lots]:
- t = time.time()
- sys.stdout.write(benchmark.__name__)
- sys.stdout.write('...')
- sys.stdout.flush()
- benchmark()
- sys.stdout.write(' %.03fs\n' % (time.time() - t))
- sys.stdout.flush()
-
diff --git a/lib/test/data/1904-66_TAN.fits.gz b/lib/test/data/1904-66_TAN.fits.gz
deleted file mode 100644
index 34ff168..0000000
--- a/lib/test/data/1904-66_TAN.fits.gz
+++ /dev/null
Binary files differ
diff --git a/lib/test/data/2chipA.fits.gz b/lib/test/data/2chipA.fits.gz
deleted file mode 100644
index 27a68ab..0000000
--- a/lib/test/data/2chipA.fits.gz
+++ /dev/null
Binary files differ
diff --git a/lib/test/data/2chipB.fits.gz b/lib/test/data/2chipB.fits.gz
deleted file mode 100644
index 209d5f9..0000000
--- a/lib/test/data/2chipB.fits.gz
+++ /dev/null
Binary files differ
diff --git a/lib/test/data/2chipC.fits.gz b/lib/test/data/2chipC.fits.gz
deleted file mode 100644
index 10af627..0000000
--- a/lib/test/data/2chipC.fits.gz
+++ /dev/null
Binary files differ
diff --git a/lib/test/test.py b/lib/test/test.py
deleted file mode 100644
index b551090..0000000
--- a/lib/test/test.py
+++ /dev/null
@@ -1,277 +0,0 @@
-from __future__ import absolute_import
-
-import os
-import random
-
-import numpy as np
-from numpy.testing import assert_almost_equal, assert_array_less
-
-from .. import graph
-from .. import great_circle_arc
-from .. import polygon
-from .. import vector
-
-from .test_util import *
-from .test_shared import resolve_imagename
-
-graph.DEBUG = True
-
-
-def test_normalize_vector():
- x, y, z = np.ogrid[-100:100:11,-100:100:11,-100:100:11]
- xn, yn, zn = vector.normalize_vector(x.flatten(), y.flatten(), z.flatten())
- l = np.sqrt(xn ** 2 + yn ** 2 + zn ** 2)
- assert_almost_equal(l, 1.0)
-
-
-def test_radec_to_vector():
- npx, npy, npz = vector.radec_to_vector(np.arange(-360, 360, 1), 90)
- assert_almost_equal(npx, 0.0)
- assert_almost_equal(npy, 0.0)
- assert_almost_equal(npz, 1.0)
-
- spx, spy, spz = vector.radec_to_vector(np.arange(-360, 360, 1), -90)
- assert_almost_equal(spx, 0.0)
- assert_almost_equal(spy, 0.0)
- assert_almost_equal(spz, -1.0)
-
- eqx, eqy, eqz = vector.radec_to_vector(np.arange(-360, 360, 1), 0)
- assert_almost_equal(eqz, 0.0)
-
-
-def test_vector_to_radec():
- ra, dec = vector.vector_to_radec(0, 0, 1)
- assert_almost_equal(dec, 90)
-
- ra, dec = vector.vector_to_radec(0, 0, -1)
- assert_almost_equal(dec, -90)
-
- ra, dec = vector.vector_to_radec(1, 1, 0)
- assert_almost_equal(ra, 45.0)
- assert_almost_equal(dec, 0.0)
-
-
-def test_intersects_poly_simple():
- ra1 = [-10, 10, 10, -10, -10]
- dec1 = [30, 30, 0, 0, 30]
- poly1 = polygon.SphericalPolygon.from_radec(ra1, dec1)
-
- ra2 = [-5, 15, 15, -5, -5]
- dec2 = [20, 20, -10, -10, 20]
- poly2 = polygon.SphericalPolygon.from_radec(ra2, dec2)
-
- assert poly1.intersects_poly(poly2)
-
- # Make sure it isn't order-dependent
- ra1 = ra1[::-1]
- dec1 = dec1[::-1]
- poly1 = polygon.SphericalPolygon.from_radec(ra1, dec1)
-
- ra2 = ra2[::-1]
- dec2 = dec2[::-1]
- poly2 = polygon.SphericalPolygon.from_radec(ra2, dec2)
-
- assert poly1.intersects_poly(poly2)
-
-
-def test_intersects_poly_fully_contained():
- ra1 = [-10, 10, 10, -10, -10]
- dec1 = [30, 30, 0, 0, 30]
- poly1 = polygon.SphericalPolygon.from_radec(ra1, dec1)
-
- ra2 = [-5, 5, 5, -5, -5]
- dec2 = [20, 20, 10, 10, 20]
- poly2 = polygon.SphericalPolygon.from_radec(ra2, dec2)
-
- assert poly1.intersects_poly(poly2)
-
- # Make sure it isn't order-dependent
- ra1 = ra1[::-1]
- dec1 = dec1[::-1]
- poly1 = polygon.SphericalPolygon.from_radec(ra1, dec1)
-
- ra2 = ra2[::-1]
- dec2 = dec2[::-1]
- poly2 = polygon.SphericalPolygon.from_radec(ra2, dec2)
-
- assert poly1.intersects_poly(poly2)
-
-
-def test_hard_intersects_poly():
- ra1 = [-10, 10, 10, -10, -10]
- dec1 = [30, 30, 0, 0, 30]
- poly1 = polygon.SphericalPolygon.from_radec(ra1, dec1)
-
- ra2 = [-20, 20, 20, -20, -20]
- dec2 = [20, 20, 10, 10, 20]
- poly2 = polygon.SphericalPolygon.from_radec(ra2, dec2)
-
- assert poly1.intersects_poly(poly2)
-
- # Make sure it isn't order-dependent
- ra1 = ra1[::-1]
- dec1 = dec1[::-1]
- poly1 = polygon.SphericalPolygon.from_radec(ra1, dec1)
-
- ra2 = ra2[::-1]
- dec2 = dec2[::-1]
- poly2 = polygon.SphericalPolygon.from_radec(ra2, dec2)
-
- assert poly1.intersects_poly(poly2)
-
-
-def test_not_intersects_poly():
- ra1 = [-10, 10, 10, -10, -10]
- dec1 = [30, 30, 5, 5, 30]
- poly1 = polygon.SphericalPolygon.from_radec(ra1, dec1)
-
- ra2 = [-20, 20, 20, -20, -20]
- dec2 = [-20, -20, -10, -10, -20]
- poly2 = polygon.SphericalPolygon.from_radec(ra2, dec2)
-
- assert not poly1.intersects_poly(poly2)
-
- # Make sure it isn't order-dependent
- ra1 = ra1[::-1]
- dec1 = dec1[::-1]
- poly1 = polygon.SphericalPolygon.from_radec(ra1, dec1)
-
- ra2 = ra2[::-1]
- dec2 = dec2[::-1]
- poly2 = polygon.SphericalPolygon.from_radec(ra2, dec2)
-
- assert not poly1.intersects_poly(poly2)
-
-
-def test_point_in_poly():
- point = np.asarray([-0.27475449, 0.47588873, -0.83548781])
- points = np.asarray([[ 0.04821217, -0.29877206, 0.95310589],
- [ 0.04451801, -0.47274119, 0.88007608],
- [-0.14916503, -0.46369786, 0.87334649],
- [-0.16101648, -0.29210164, 0.94273555],
- [ 0.04821217, -0.29877206, 0.95310589]])
- inside = np.asarray([-0.03416009, -0.36858623, 0.9289657])
- poly = polygon.SphericalPolygon(points, inside)
- assert not poly.contains_point(point)
-
-
-def test_point_in_poly_lots():
- import pyfits
- fits = pyfits.open(resolve_imagename(ROOT_DIR, '1904-66_TAN.fits'))
- header = fits[0].header
-
- poly1 = polygon.SphericalPolygon.from_wcs(
- header, 1, crval=[0, 87])
- poly2 = polygon.SphericalPolygon.from_wcs(
- header, 1, crval=[20, 89])
- poly3 = polygon.SphericalPolygon.from_wcs(
- header, 1, crval=[180, 89])
-
- points = get_point_set()
- count = 0
- for point in points:
- if poly1.contains_point(point) or poly2.contains_point(point) or \
- poly3.contains_point(point):
- count += 1
-
- assert count == 5
- assert poly1.intersects_poly(poly2)
- assert not poly1.intersects_poly(poly3)
- assert not poly2.intersects_poly(poly3)
-
-
-def test_great_circle_arc_intersection():
- A = [-10, -10]
- B = [10, 10]
-
- C = [-25, 10]
- D = [15, -10]
-
- E = [-20, 40]
- F = [20, 40]
-
- correct = [0.99912414, -0.02936109, -0.02981403]
-
- A = vector.radec_to_vector(*A)
- B = vector.radec_to_vector(*B)
- C = vector.radec_to_vector(*C)
- D = vector.radec_to_vector(*D)
- E = vector.radec_to_vector(*E)
- F = vector.radec_to_vector(*F)
-
- assert great_circle_arc.intersects(A, B, C, D)
- r = great_circle_arc.intersection(A, B, C, D)
- assert r.shape == (3,)
- assert_almost_equal(r, correct)
-
- assert np.all(great_circle_arc.intersects([A], [B], [C], [D]))
- r = great_circle_arc.intersection([A], [B], [C], [D])
- assert r.shape == (1, 3)
- assert_almost_equal(r, [correct])
-
- assert np.all(great_circle_arc.intersects([A], [B], C, D))
- r = great_circle_arc.intersection([A], [B], C, D)
- assert r.shape == (1, 3)
- assert_almost_equal(r, [correct])
-
- assert not np.all(great_circle_arc.intersects([A, E], [B, F], [C], [D]))
- r = great_circle_arc.intersection([A, E], [B, F], [C], [D])
- assert r.shape == (2, 3)
- assert_almost_equal(r[0], correct)
- assert np.all(np.isnan(r[1]))
-
- # Test parallel arcs
- r = great_circle_arc.intersection(A, B, A, B)
- assert np.all(np.isnan(r))
-
-
-def test_great_circle_arc_length():
- A = [90, 0]
- B = [-90, 0]
- A = vector.radec_to_vector(*A)
- B = vector.radec_to_vector(*B)
- assert great_circle_arc.length(A, B) == 180.0
-
- A = [135, 0]
- B = [-90, 0]
- A = vector.radec_to_vector(*A)
- B = vector.radec_to_vector(*B)
- assert great_circle_arc.length(A, B) == 135.0
-
- A = [0, 0]
- B = [0, 90]
- A = vector.radec_to_vector(*A)
- B = vector.radec_to_vector(*B)
- assert great_circle_arc.length(A, B) == 90.0
-
-
-def test_great_circle_arc_angle():
- A = [1, 0, 0]
- B = [0, 1, 0]
- C = [0, 0, 1]
- assert great_circle_arc.angle(A, B, C) == 90.0
-
- # TODO: More angle tests
-
-
-def test_cone():
- random.seed(0)
- for i in range(50):
- ra = random.randrange(-180, 180)
- dec = random.randrange(20, 90)
- cone = polygon.SphericalPolygon.from_cone(ra, dec, 8, steps=64)
-
-
-def test_area():
- triangles = [
- ([[90, 0], [0, -45], [0, 45], [90, 0]], np.pi * 0.5),
- ([[90, 0], [0, -22.5], [0, 22.5], [90, 0]], np.pi * 0.25),
- ([[90, 0], [0, -11.25], [0, 11.25], [90, 0]], np.pi * 0.125)
- ]
-
- for tri, area in triangles:
- tri = np.array(tri)
- x, y, z = vector.radec_to_vector(tri[:,1], tri[:,0])
- points = np.dstack((x, y, z))[0]
- poly = polygon.SphericalPolygon(points)
- calc_area = poly.area()
diff --git a/lib/test/test_intersection.py b/lib/test/test_intersection.py
deleted file mode 100644
index 6dacb3d..0000000
--- a/lib/test/test_intersection.py
+++ /dev/null
@@ -1,189 +0,0 @@
-from __future__ import print_function, absolute_import
-
-# STDLIB
-import functools
-import itertools
-import math
-import os
-import random
-import sys
-
-# THIRD-PARTY
-import numpy as np
-from numpy.testing import assert_array_almost_equal
-
-# LOCAL
-from .. import polygon
-from .test_shared import resolve_imagename
-
-GRAPH_MODE = False
-ROOT_DIR = os.path.join(os.path.dirname(__file__), 'data')
-
-
-class intersection_test:
- def __init__(self, lon_0, lat_0, proj='ortho'):
- self._lon_0 = lon_0
- self._lat_0 = lat_0
- self._proj = proj
-
- def __call__(self, func):
- @functools.wraps(func)
- def run(*args, **kwargs):
- if GRAPH_MODE:
- from mpl_toolkits.basemap import Basemap
- from matplotlib import pyplot as plt
-
- polys = func(*args, **kwargs)
-
- intersections = []
- num_permutations = math.factorial(len(polys))
- step_size = int(max(float(num_permutations) / 20.0, 1.0))
-
- areas = [x.area() for x in polys]
-
- if GRAPH_MODE:
- print("%d permutations" % num_permutations)
- for method in ('parallel', 'serial'):
- for i, permutation in enumerate(
- itertools.islice(
- itertools.permutations(polys),
- None, None, step_size)):
- filename = '%s_%s_intersection_%04d.svg' % (
- func.__name__, method, i)
- print(filename)
-
- intersection = polygon.SphericalPolygon.multi_intersection(
- permutation, method=method)
- intersections.append(intersection)
- intersection_area = intersection.area()
- if GRAPH_MODE:
- fig = plt.figure()
- m = Basemap(projection=self._proj,
- lon_0=self._lon_0,
- lat_0=self._lat_0)
- m.drawparallels(np.arange(-90., 90., 20.))
- m.drawmeridians(np.arange(0., 420., 20.))
- m.drawmapboundary(fill_color='white')
-
- intersection.draw(m, color='red', linewidth=3)
- for poly in permutation:
- poly.draw(m, color='blue', alpha=0.5)
- plt.savefig(filename)
- fig.clear()
-
- assert np.all(intersection_area * 0.9 <= areas)
-
- lengths = np.array([len(x._points) for x in intersections])
- assert np.all(lengths == [lengths[0]])
- areas = np.array([x.area() for x in intersections])
- assert_array_almost_equal(areas, areas[0], decimal=1)
-
- return run
-
-
-@intersection_test(0, 90)
-def test1():
- import pyfits
- import os
-
- fits = pyfits.open(resolve_imagename(ROOT_DIR,'1904-66_TAN.fits'))
- header = fits[0].header
-
- poly1 = polygon.SphericalPolygon.from_wcs(
- header, 1, crval=[0, 87])
- poly2 = polygon.SphericalPolygon.from_wcs(
- header, 1, crval=[20, 89])
-
- return [poly1, poly2]
-
-
-@intersection_test(0, 90)
-def test2():
- poly1 = polygon.SphericalPolygon.from_cone(0, 60, 8, steps=16)
- poly2 = polygon.SphericalPolygon.from_cone(0, 68, 8, steps=16)
- poly3 = polygon.SphericalPolygon.from_cone(12, 66, 8, steps=16)
- return [poly1, poly2, poly3]
-
-
-@intersection_test(0, 90)
-def test3():
- import pyfits
- fits = pyfits.open(resolve_imagename(ROOT_DIR, '1904-66_TAN.fits'))
- header = fits[0].header
-
- poly1 = polygon.SphericalPolygon.from_wcs(
- header, 1, crval=[0, 87])
- poly3 = polygon.SphericalPolygon.from_wcs(
- header, 1, crval=[175, 89])
-
- return [poly1, poly3]
-
-
-def test4():
- import pyfits
- import pywcs
-
- A = pyfits.open(os.path.join(ROOT_DIR, '2chipA.fits.gz'))
- B = pyfits.open(os.path.join(ROOT_DIR, '2chipB.fits.gz'))
-
- wcs = pywcs.WCS(A[1].header, fobj=A)
- chipA1 = polygon.SphericalPolygon.from_wcs(wcs)
- wcs = pywcs.WCS(A[4].header, fobj=A)
- chipA2 = polygon.SphericalPolygon.from_wcs(wcs)
- wcs = pywcs.WCS(B[1].header, fobj=B)
- chipB1 = polygon.SphericalPolygon.from_wcs(wcs)
- wcs = pywcs.WCS(B[4].header, fobj=B)
- chipB2 = polygon.SphericalPolygon.from_wcs(wcs)
-
- Apoly = chipA1.union(chipA2)
- Bpoly = chipB1.union(chipB2)
-
- X = Apoly.intersection(Bpoly)
-
-
-def test5():
- import pyfits
- import pywcs
-
- A = pyfits.open(os.path.join(ROOT_DIR, '2chipA.fits.gz'))
- B = pyfits.open(os.path.join(ROOT_DIR, '2chipB.fits.gz'))
-
- wcs = pywcs.WCS(A[1].header, fobj=A)
- chipA1 = polygon.SphericalPolygon.from_wcs(wcs)
- wcs = pywcs.WCS(A[4].header, fobj=A)
- chipA2 = polygon.SphericalPolygon.from_wcs(wcs)
- wcs = pywcs.WCS(B[1].header, fobj=B)
- chipB1 = polygon.SphericalPolygon.from_wcs(wcs)
- wcs = pywcs.WCS(B[4].header, fobj=B)
- chipB2 = polygon.SphericalPolygon.from_wcs(wcs)
-
- Apoly = chipA1.union(chipA2)
- Bpoly = chipB1.union(chipB2)
-
- Apoly.overlap(chipB1)
-
-
-@intersection_test(0, 90)
-def test6():
- import pyfits
- fits = pyfits.open(resolve_imagename(ROOT_DIR, '1904-66_TAN.fits'))
- header = fits[0].header
-
- poly1 = polygon.SphericalPolygon.from_wcs(
- header, 1)
- poly2 = polygon.SphericalPolygon.from_wcs(
- header, 1)
-
- return [poly1, poly2]
-
-
-if __name__ == '__main__':
- if '--profile' not in sys.argv:
- GRAPH_MODE = True
- from mpl_toolkits.basemap import Basemap
- from matplotlib import pyplot as plt
-
- functions = [(k, v) for k, v in globals().items() if k.startswith('test')]
- functions.sort()
- for k, v in functions:
- v()
diff --git a/lib/test/test_shared.py b/lib/test/test_shared.py
deleted file mode 100644
index 0853d3a..0000000
--- a/lib/test/test_shared.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import os
-
-def resolve_imagename(ROOT_DIR, base_name):
- """Resolve image name for tests."""
-
- image_name = os.path.join(ROOT_DIR, base_name)
-
- # Is it zipped?
- if not os.path.exists(image_name):
- image_name = image_name.replace('.fits', '.fits.gz')
-
- return image_name
diff --git a/lib/test/test_skyline.py b/lib/test/test_skyline.py
deleted file mode 100644
index 929f0ba..0000000
--- a/lib/test/test_skyline.py
+++ /dev/null
@@ -1,225 +0,0 @@
-"""
-SkyLine tests.
-
-:Author: Pey Lian Lim
-
-:Organization: Space Telescope Science Institute
-
-Examples
---------
->>> cd path/to/project
->>> nosetests
-
-"""
-from __future__ import absolute_import
-
-from copy import copy
-from numpy.testing import assert_almost_equal
-
-from ..vector import vector_to_radec
-from ..polygon import SphericalPolygon
-from ..skyline import SkyLine
-
-from .test_util import ROOT_DIR
-from .test_shared import resolve_imagename
-
-
-#---------------------------------------#
-# Load footprints used by all the tests #
-#---------------------------------------#
-f_2chipA = resolve_imagename(ROOT_DIR, '2chipA.fits') # ACS/WFC #1
-im_2chipA = SkyLine(f_2chipA)
-f_2chipB = resolve_imagename(ROOT_DIR, '2chipB.fits') # ACS/WFC #2
-im_2chipB = SkyLine(f_2chipB)
-f_2chipC = resolve_imagename(ROOT_DIR, '2chipC.fits') # WFC3/UVIS
-im_2chipC = SkyLine(f_2chipC)
-f_66_tan = resolve_imagename(ROOT_DIR, '1904-66_TAN.fits')
-im_66_tan = SkyLine(f_66_tan, extname='primary')
-
-
-#----- SHARED FUNCTIONS -----
-
-def same_members(mem1, mem2):
- assert len(mem1) == len(mem2)
-
- for m in mem1:
- assert m in mem2
-
- for m in mem2:
- assert m in mem1
-
-def subset_members(mem_child, mem_parent):
- assert len(mem_parent) > len(mem_child)
-
- for m in mem_child:
- assert m in mem_parent
-
-def subset_polygon(p_child, p_parent):
- """Overlap not working. Do this instead until fixed."""
- assert p_parent.area() >= p_child.area()
- assert p_parent.contains_point(p_child.inside)
-
-def no_polygon(p_child, p_parent):
- """Overlap not working. Do this instead until fixed."""
- assert not p_parent.contains_point(p_child.inside)
-
-
-#----- MEMBERSHIP -----
-
-def do_member_overlap(im):
- for m in im.members:
- assert_almost_equal(m.polygon.overlap(im), 1.0)
-
-def test_membership():
- do_member_overlap(im_2chipA)
- do_member_overlap(im_2chipB)
- do_member_overlap(im_66_tan)
-
- assert len(im_2chipA.members) == 1
- assert im_2chipA.members[0].fname == f_2chipA
- assert im_2chipA.members[0].ext == (1,4)
-
-
-#----- COPY -----
-
-def test_copy():
- a_copy = copy(im_2chipA)
- assert a_copy is not im_2chipA
-
-
-#----- SPHERICAL POLYGON RELATED -----
-
-def test_sphericalpolygon():
- assert im_2chipA.contains_point(im_2chipA.inside)
-
- assert im_2chipA.intersects_poly(im_2chipB.polygon)
-
- assert im_2chipA.intersects_arc(im_2chipA.inside, im_2chipB.inside)
-
- assert im_2chipA.overlap(im_2chipB) < im_2chipA.overlap(im_2chipA)
-
- assert_almost_equal(im_2chipA.area(), im_2chipB.area())
-
- ra_A, dec_A = im_2chipA.to_radec()
- for i in xrange(len(im_2chipA.points)):
- p = im_2chipA.points[i]
- ra, dec = vector_to_radec(p[0], p[1], p[2], degrees=True)
- assert_almost_equal(ra_A[i], ra)
- assert_almost_equal(dec_A[i], dec)
-
-
-#----- WCS -----
-
-def test_wcs():
- wcs = im_2chipA.to_wcs()
- new_p = SphericalPolygon.from_wcs(wcs)
- subset_polygon(im_2chipA, new_p)
-
-
-#----- UNION -----
-
-def do_add_image(im1, im2):
- u1 = im1.add_image(im2)
- u2 = im2.add_image(im1)
-
- assert u1.same_points_as(u2)
- same_members(u1.members, u2.members)
-
- all_mems = im1.members + im2.members
- same_members(u1.members, all_mems)
-
- subset_polygon(im1, u1)
- subset_polygon(im2, u1)
-
-def test_add_image():
- # Dithered
- do_add_image(im_2chipA, im_2chipB)
-
- # Not related
- do_add_image(im_2chipA, im_66_tan)
-
-
-#----- INTERSECTION -----
-
-def do_intersect_image(im1, im2):
- i1 = im1.find_intersection(im2)
- i2 = im2.find_intersection(im1)
-
- assert i1.same_points_as(i2)
- same_members(i1.members, i2.members)
-
- if len(i1.points) > 0:
- subset_members(im1.members, i1.members)
- subset_members(im2.members, i1.members)
-
- subset_polygon(i1, im1)
- subset_polygon(i1, im2)
-
-def test_find_intersection():
- # Dithered
- do_intersect_image(im_2chipA, im_2chipB)
-
- # Not related
- do_intersect_image(im_2chipA, im_66_tan)
-
-
-#----- SKYLINE OVERLAP -----
-
-def test_max_overlap():
- max_s, max_a = im_2chipA.find_max_overlap([im_2chipB, im_2chipC, im_66_tan])
- assert max_s is im_2chipB
- assert_almost_equal(max_a, im_2chipA.intersection(im_2chipB).area())
-
- max_s, max_a = im_2chipA.find_max_overlap([im_2chipB, im_2chipA])
- assert max_s is im_2chipA
- assert_almost_equal(max_a, im_2chipA.area())
-
-def test_max_overlap_pair():
- assert SkyLine.max_overlap_pair(
- [im_2chipB, im_2chipC, im_2chipA, im_66_tan]) == (im_2chipB, im_2chipA)
-
- assert SkyLine.max_overlap_pair([im_2chipC, im_2chipA, im_66_tan]) is None
-
-
-#----- INTENDED USE CASE -----
-
-def test_science_1():
- mos, inc, exc = SkyLine.mosaic([im_2chipA, im_2chipB, im_2chipC, im_66_tan])
-
- assert inc == [f_2chipA, f_2chipB]
- assert exc == [f_2chipC, f_66_tan]
-
- subset_polygon(im_2chipA, mos)
- subset_polygon(im_2chipB, mos)
-
- no_polygon(im_2chipC, mos)
- no_polygon(im_66_tan, mos)
-
-def test_science_2():
- """Like `test_science_1` but different input order."""
- mos, inc, exc = SkyLine.mosaic([im_2chipB, im_66_tan, im_2chipC, im_2chipA])
-
- assert inc == [f_2chipB, f_2chipA]
- assert exc == [f_66_tan, f_2chipC]
-
- subset_polygon(im_2chipA, mos)
- subset_polygon(im_2chipB, mos)
-
- no_polygon(im_2chipC, mos)
- no_polygon(im_66_tan, mos)
-
-
-#----- UNSTABLE -----
-
-def DISABLED_unstable_overlap():
- i1 = im_2chipA.find_intersection(im_2chipB)
- i2 = im_2chipB.find_intersection(im_2chipA)
-
- u1 = im_2chipA.add_image(im_2chipB)
- u2 = im_2chipB.add_image(im_2chipA)
-
- # failed here before - known bug
- # failure not always the same due to hash mapping
- assert_almost_equal(i1.overlap(u1), 1.0)
- assert_almost_equal(i1.overlap(i2), 1.0)
- assert_almost_equal(u1.overlap(u2), 1.0)
diff --git a/lib/test/test_union.py b/lib/test/test_union.py
deleted file mode 100644
index 4b649f6..0000000
--- a/lib/test/test_union.py
+++ /dev/null
@@ -1,215 +0,0 @@
-from __future__ import print_function, absolute_import
-
-# STDLIB
-import functools
-import itertools
-import math
-import os
-import random
-import sys
-
-# THIRD-PARTY
-import numpy as np
-from numpy.testing import assert_array_almost_equal
-
-# LOCAL
-from .. import polygon
-from .test_shared import resolve_imagename
-
-GRAPH_MODE = False
-ROOT_DIR = os.path.join(os.path.dirname(__file__), 'data')
-
-
-class union_test:
- def __init__(self, lon_0, lat_0, proj='ortho'):
- self._lon_0 = lon_0
- self._lat_0 = lat_0
- self._proj = proj
-
- def __call__(self, func):
- @functools.wraps(func)
- def run(*args, **kwargs):
- if GRAPH_MODE:
- from mpl_toolkits.basemap import Basemap
- from matplotlib import pyplot as plt
-
- polys = func(*args, **kwargs)
-
- unions = []
- num_permutations = math.factorial(len(polys))
- step_size = int(max(float(num_permutations) / 20.0, 1.0))
- if GRAPH_MODE:
- print("%d permutations" % num_permutations)
-
- areas = [x.area() for x in polys]
-
- for i, permutation in enumerate(
- itertools.islice(
- itertools.permutations(polys),
- None, None, step_size)):
- filename = '%s_union_%04d.svg' % (
- func.__name__, i)
- print(filename)
-
- union = polygon.SphericalPolygon.multi_union(
- permutation)
- unions.append(union)
- union_area = union.area()
- print(union._points)
- print(permutation[0]._points)
-
- if GRAPH_MODE:
- fig = plt.figure()
- m = Basemap(projection=self._proj,
- lon_0=self._lon_0,
- lat_0=self._lat_0)
- m.drawmapboundary(fill_color='white')
- m.drawparallels(np.arange(-90., 90., 20.))
- m.drawmeridians(np.arange(0., 420., 20.))
-
- union.draw(m, color='red', linewidth=3)
- for poly in permutation:
- poly.draw(m, color='blue', alpha=0.5)
- plt.savefig(filename)
- fig.clear()
-
- print(union_area, areas)
- assert np.all(union_area * 1.1 >= areas)
-
- lengths = np.array([len(x._points) for x in unions])
- assert np.all(lengths == [lengths[0]])
- areas = np.array([x.area() for x in unions])
- assert_array_almost_equal(areas, areas[0], 1)
-
- return run
-
-
-@union_test(0, 90)
-def test1():
- import pyfits
- fits = pyfits.open(resolve_imagename(ROOT_DIR, '1904-66_TAN.fits'))
- header = fits[0].header
-
- poly1 = polygon.SphericalPolygon.from_wcs(
- header, 1, crval=[0, 87])
- poly2 = polygon.SphericalPolygon.from_wcs(
- header, 1, crval=[20, 89])
- poly3 = polygon.SphericalPolygon.from_wcs(
- header, 1, crval=[175, 89])
- poly4 = polygon.SphericalPolygon.from_cone(
- 90, 70, 10, steps=50)
-
- return [poly1, poly2, poly3, poly4]
-
-
-@union_test(0, 90)
-def test2():
- poly1 = polygon.SphericalPolygon.from_cone(0, 60, 7, steps=16)
- poly2 = polygon.SphericalPolygon.from_cone(0, 72, 7, steps=16)
- poly3 = polygon.SphericalPolygon.from_cone(20, 60, 7, steps=16)
- poly4 = polygon.SphericalPolygon.from_cone(20, 72, 7, steps=16)
- poly5 = polygon.SphericalPolygon.from_cone(35, 55, 7, steps=16)
- poly6 = polygon.SphericalPolygon.from_cone(60, 60, 3, steps=16)
- return [poly1, poly2, poly3, poly4, poly5, poly6]
-
-
-@union_test(0, 90)
-def test3():
- random.seed(0)
- polys = []
- for i in range(10):
- polys.append(polygon.SphericalPolygon.from_cone(
- random.randrange(-180, 180),
- random.randrange(20, 90),
- random.randrange(5, 16),
- steps=16))
- return polys
-
-
-@union_test(0, 15)
-def test4():
- random.seed(64)
- polys = []
- for i in range(10):
- polys.append(polygon.SphericalPolygon.from_cone(
- random.randrange(-30, 30),
- random.randrange(-15, 60),
- random.randrange(5, 16),
- steps=16))
- return polys
-
-
-def test5():
- import pyfits
- import pywcs
-
- A = pyfits.open(os.path.join(ROOT_DIR, '2chipA.fits.gz'))
-
- wcs = pywcs.WCS(A[1].header, fobj=A)
- chipA1 = polygon.SphericalPolygon.from_wcs(wcs)
- wcs = pywcs.WCS(A[4].header, fobj=A)
- chipA2 = polygon.SphericalPolygon.from_wcs(wcs)
-
- null_union = chipA1.union(chipA2)
-
-
-def test6():
- import pyfits
- import pywcs
-
- A = pyfits.open(os.path.join(ROOT_DIR, '2chipC.fits.gz'))
-
- wcs = pywcs.WCS(A[1].header, fobj=A)
- chipA1 = polygon.SphericalPolygon.from_wcs(wcs)
- wcs = pywcs.WCS(A[4].header, fobj=A)
- chipA2 = polygon.SphericalPolygon.from_wcs(wcs)
-
- null_union = chipA1.union(chipA2)
-
-
-@union_test(0, 90)
-def test7():
- import pyfits
- import pywcs
-
- A = pyfits.open(os.path.join(ROOT_DIR, '2chipA.fits.gz'))
-
- wcs = pywcs.WCS(A[1].header, fobj=A)
- chipA1 = polygon.SphericalPolygon.from_wcs(wcs)
- wcs = pywcs.WCS(A[4].header, fobj=A)
- chipA2 = polygon.SphericalPolygon.from_wcs(wcs)
-
- B = pyfits.open(os.path.join(ROOT_DIR, '2chipB.fits.gz'))
-
- wcs = pywcs.WCS(B[1].header, fobj=B)
- chipB1 = polygon.SphericalPolygon.from_wcs(wcs)
- wcs = pywcs.WCS(B[4].header, fobj=B)
- chipB2 = polygon.SphericalPolygon.from_wcs(wcs)
-
- return [chipA1, chipA2, chipB1, chipB2]
-
-
-@union_test(0, 90)
-def test8():
- import pyfits
- fits = pyfits.open(resolve_imagename(ROOT_DIR, '1904-66_TAN.fits'))
- header = fits[0].header
-
- poly1 = polygon.SphericalPolygon.from_wcs(
- header, 1)
- poly2 = polygon.SphericalPolygon.from_wcs(
- header, 1)
-
- return [poly1, poly2]
-
-
-if __name__ == '__main__':
- if '--profile' not in sys.argv:
- GRAPH_MODE = True
- from mpl_toolkits.basemap import Basemap
- from matplotlib import pyplot as plt
-
- functions = [(k, v) for k, v in globals().items() if k.startswith('test')]
- functions.sort()
- for k, v in functions:
- v()
diff --git a/lib/test/test_util.py b/lib/test/test_util.py
deleted file mode 100644
index 6dc0393..0000000
--- a/lib/test/test_util.py
+++ /dev/null
@@ -1,14 +0,0 @@
-import os
-
-import numpy as np
-from .. import vector
-
-ROOT_DIR = os.path.join(os.path.dirname(__file__), 'data')
-
-def get_point_set(density=25):
- points = []
- for i in np.linspace(-85, 85, density, True):
- for j in np.linspace(-180, 180, np.cos(np.deg2rad(i)) * density):
- points.append([j, i])
- points = np.asarray(points)
- return np.dstack(vector.radec_to_vector(points[:,0], points[:,1]))[0]
diff --git a/lib/vector.py b/lib/vector.py
deleted file mode 100644
index c6887c4..0000000
--- a/lib/vector.py
+++ /dev/null
@@ -1,243 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2011 Association of Universities for Research in
-# Astronomy (AURA)
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-#
-# 1. Redistributions of source code must retain the above
-# copyright notice, this list of conditions and the following
-# disclaimer.
-#
-# 2. Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following
-# disclaimer in the documentation and/or other materials
-# provided with the distribution.
-#
-# 3. The name of AURA and its representatives may not be used to
-# endorse or promote products derived from this software without
-# specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR
-# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT,
-# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
-# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
-# OF THE POSSIBILITY OF SUCH DAMAGE.
-
-"""
-The `sphere.vector` module contains the basic operations for handling
-vectors and converting them to and from other representations.
-"""
-
-from __future__ import unicode_literals
-
-# THIRD-PARTY
-import numpy as np
-
-
-__all__ = ['radec_to_vector', 'vector_to_radec', 'normalize_vector',
- 'rotate_around']
-
-
-def radec_to_vector(ra, dec, degrees=True):
- r"""
- Converts a location on the unit sphere from right-ascension and
- declination to an *x*, *y*, *z* vector.
-
- Parameters
- ----------
- ra, dec : scalars or 1-D arrays
-
- degrees : bool, optional
-
- If `True`, (default) *ra* and *dec* are in decimal degrees,
- otherwise in radians.
-
- Returns
- -------
- x, y, z : tuple of scalars or 1-D arrays of the same length
-
- Notes
- -----
- Where right-ascension is *α* and declination is *δ*:
-
- .. math::
- x = \cos\alpha \cos\delta
-
- y = \sin\alpha \cos\delta
-
- z = \sin\delta
- """
- ra = np.asanyarray(ra)
- dec = np.asanyarray(dec)
-
- if degrees:
- ra_rad = np.deg2rad(ra)
- dec_rad = np.deg2rad(dec)
- else:
- ra_rad = ra
- dec_rad = dec
-
- cos_dec = np.cos(dec_rad)
-
- return (
- np.cos(ra_rad) * cos_dec,
- np.sin(ra_rad) * cos_dec,
- np.sin(dec_rad))
-
-
-def vector_to_radec(x, y, z, degrees=True):
- r"""
- Converts a vector to right-ascension and declination.
-
- Parameters
- ----------
- x, y, z : scalars or 1-D arrays
- The input vectors
-
- degrees : bool, optional
- If `True` (default) the result is returned in decimal degrees,
- otherwise radians.
-
- Returns
- -------
- ra, dec : tuple of scalars or arrays of the same length
-
- Notes
- -----
- Where right-ascension is *α* and declination is
- *δ*:
-
- .. math::
- \alpha = \arctan2(y, x)
-
- \delta = \arctan2(z, \sqrt{x^2 + y^2})
- """
- x = np.asanyarray(x)
- y = np.asanyarray(y)
- z = np.asanyarray(z)
-
- result = (
- np.arctan2(y, x),
- np.arctan2(z, np.sqrt(x ** 2 + y ** 2)))
-
- if degrees:
- return np.rad2deg(result[0]), np.rad2deg(result[1])
- else:
- return result
-
-
-def normalize_vector(x, y, z, inplace=False):
- r"""
- Normalizes a vector so it falls on the unit sphere.
-
- *x*, *y*, *z* may be scalars or 1-D arrays
-
- Parameters
- ----------
- x, y, z : scalars or 1-D arrays of the same length
- The input vectors
-
- inplace : bool, optional
- When `True`, the original arrays will be normalized in place,
- otherwise a normalized copy is returned.
-
- Returns
- -------
- X, Y, Z : scalars of 1-D arrays of the same length
- The normalized output vectors
- """
- x = np.asanyarray(x)
- y = np.asanyarray(y)
- z = np.asanyarray(z)
-
- l = np.sqrt(x ** 2 + y ** 2 + z ** 2)
-
- if inplace:
- x /= l
- y /= l
- z /= l
- return (x, y, z)
- else:
- return (x / l, y / l, z / l)
-
-
-def rotate_around(x, y, z, u, v, w, theta, degrees=True):
- r"""
- Rotates the vector (*x*, *y*, *z*) around the arbitrary axis defined by
- vector (*u*, *v*, *w*) by *theta*.
-
- It is assumed that both (*x*, *y*, *z*) and (*u*, *v*, *w*) are
- already normalized.
-
- Parameters
- ----------
- x, y, z : doubles
- The normalized vector to rotate
-
- u, v, w : doubles
- The normalized vector to rotate around
-
- theta : double, or array of doubles
- The amount to rotate
-
- degrees : bool, optional
- When `True`, *theta* is given in degrees, otherwise radians.
-
- Returns
- -------
- X, Y, Z : doubles
- The rotated vector
- """
- if degrees:
- theta = np.deg2rad(theta)
-
- costheta = np.cos(theta)
- sintheta = np.sin(theta)
- icostheta = 1.0 - costheta
-
- det = (-u*x - v*y - w*z)
- X = (-u*det)*icostheta + x*costheta + (-w*y + v*z)*sintheta
- Y = (-v*det)*icostheta + y*costheta + ( w*x - u*z)*sintheta
- Z = (-w*det)*icostheta + z*costheta + (-v*x + u*y)*sintheta
-
- return X, Y, Z
-
-
-def equal_area_proj(x, y, z):
- """
- Transform the coordinates to a 2-dimensional plane using the
- Lambert azimuthal equal-area projection.
-
- Parameters
- ----------
- x, y, z : scalars or 1-D arrays
- The input vectors
-
- Returns
- -------
- X, Y : tuple of scalars or arrays of the same length
-
- Notes
- -----
-
- .. math::
-
- X = \sqrt{\frac{2}{1-z}}x
-
- .. math::
-
- Y = \sqrt{\frac{2}{1-z}}y
- """
- scale = np.sqrt(2.0 / (1.0 - z))
- X = scale * x
- Y = scale * y
- return X, Y