summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/graph.py78
-rw-r--r--lib/great_circle_arc.py8
-rw-r--r--lib/polygon.py81
-rw-r--r--lib/test/test_intersection.py2
-rw-r--r--lib/test/test_union.py81
-rw-r--r--lib/vector.py34
6 files changed, 175 insertions, 109 deletions
diff --git a/lib/graph.py b/lib/graph.py
index 25fc80c..97c12b4 100644
--- a/lib/graph.py
+++ b/lib/graph.py
@@ -166,6 +166,7 @@ class Graph:
self._nodes = set()
self._edges = set()
self._source_polygons = set()
+ self._start_node = None
self.add_polygons(polygons)
@@ -204,6 +205,8 @@ class Graph:
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):
# Don't create self-pointing edges
if not np.array_equal(points[i], nodeA._point):
@@ -330,7 +333,7 @@ class Graph:
self.remove_edge(edge)
return [edgeA, edgeB]
- def _sanity_check(self, node_is_2=False):
+ 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
@@ -370,12 +373,7 @@ class Graph:
from mpl_toolkits.basemap import Basemap
from matplotlib import pyplot as plt
fig = plt.figure()
- m = Basemap(projection="ortho",
- lon_0=lon_0,
- lat_0=lat_0)
- m.drawparallels(np.arange(-90., 90., 20.))
- m.drawmeridians(np.arange(0., 420., 20.))
- m.drawmapboundary(fill_color='white')
+ m = Basemap()
counts = {}
for node in self._nodes:
@@ -383,11 +381,21 @@ class Graph:
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]
@@ -395,8 +403,11 @@ class Graph:
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(title)
+ plt.title("%s, %d v, %d e" % (
+ title, len(self._nodes), len(self._edges)))
plt.legend()
plt.show()
@@ -414,13 +425,13 @@ class Graph:
will be included in the output.
"""
self._remove_cut_lines()
- self._sanity_check()
+ self._sanity_check("union - remove cut lines")
self._find_all_intersections()
- self._sanity_check()
- self._remove_interior_nodes()
- self._sanity_check()
+ 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(True)
+ self._sanity_check("union - remove 3ary edges", True)
return self._trace()
def intersection(self):
@@ -437,13 +448,13 @@ class Graph:
will be included in the output.
"""
self._remove_cut_lines()
- self._sanity_check()
+ self._sanity_check("intersection - remove cut lines")
self._find_all_intersections()
- self._sanity_check()
+ self._sanity_check("intersection - find all intersections")
self._remove_exterior_edges()
- self._sanity_check()
+ self._sanity_check("intersection - remove exterior edges")
self._remove_3ary_edges(large_first=True)
- self._sanity_check(True)
+ self._sanity_check("intersection - remove 3ary edges", True)
return self._trace()
def _remove_cut_lines(self):
@@ -595,29 +606,24 @@ class Graph:
(new_ends, ends[:j], ends[j+1:]))
break
- def _remove_interior_nodes(self):
+ 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
- # node._count is the number of polygons that the node is
- # inside, other than the polygon that it came from
- for node in self._nodes:
- node._count = 0
+ for edge in self._edges:
+ edge._count = 0
for polygon in polygons:
- if polygon in node._source_polygons:
- continue
- if polygon.contains_point(node._point):
- node._count += 1
+ if (not polygon in edge._source_polygons and
+ polygon.intersects_arc(
+ edge._nodes[0]._point, edge._nodes[1]._point)):
+ edge._count += 1
- for node in list(self._nodes):
- # Nodes with a count >= 1 are completely contained in the
- # outer polygon, so they should be removed. What's left
- # are only outer nodes.
- if node._count >= 1:
- self.remove_node(node)
+ for edge in list(self._edges):
+ if edge._count >= 1:
+ self.remove_edge(edge)
def _remove_exterior_edges(self):
"""
@@ -673,7 +679,13 @@ class Graph:
seen_nodes = set()
while len(edges):
polygon = []
- edge = edges.pop()
+ # 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?
diff --git a/lib/great_circle_arc.py b/lib/great_circle_arc.py
index 682f9d9..f70d16e 100644
--- a/lib/great_circle_arc.py
+++ b/lib/great_circle_arc.py
@@ -218,6 +218,7 @@ def length(A, B, degrees=True):
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)
@@ -350,7 +351,10 @@ def interpolate(A, B, steps=50):
t = np.linspace(0.0, 1.0, steps, endpoint=True).reshape((steps, 1))
omega = length(A, B, degrees=False)
- sin_omega = np.sin(omega)
- offsets = np.sin(t * omega) / sin_omega
+ 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
index 6241416..b7ea5d1 100644
--- a/lib/polygon.py
+++ b/lib/polygon.py
@@ -204,6 +204,7 @@ class SphericalPolygon(object):
# 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))
@@ -382,29 +383,50 @@ class SphericalPolygon(object):
that are larger than half of the sphere. Therefore, the
area will always be less than 2π.
- The area is computed based on the sum of the radian angles:
+ 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::
- S = \sum^n_{i=0} \theta_i - (n - 2) \pi
+ 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.array(0.0)
- A = self._points[:-1]
- B = np.roll(A, 1, 0)
- C = np.roll(B, 1, 0)
+ # Rotate polygon so that center of polygon is at north pole
+ centroid = np.mean(self._points, axis=0)
+ points = self._points - (centroid + np.array([0, 0, 1]))
- angles = great_circle_arc.angle(A, B, C, degrees=False)
-
- midpoints = great_circle_arc.midpoint(A, C)
- interior = [self.contains_point(x) for x in midpoints]
- angles = np.where(interior, angles, 2.*np.pi - angles)
+ 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 = vector.equal_area_proj(interp[:, 0],
+ interp[:, 1],
+ interp[:, 2])
+ X.extend(x)
+ Y.extend(y)
- sum = np.sum(angles)
- area = sum - float(len(angles) - 2) * np.pi
+ X = np.array(X)
+ Y = np.array(Y)
- return area
+ return np.abs(np.sum(X[:-1] * Y[1:] - X[1:] * Y[:-1]) / 2.0)
def union(self, other):
"""
@@ -445,7 +467,7 @@ class SphericalPolygon(object):
return self.__class__(polygon, self._inside)
@classmethod
- def multi_union(cls, polygons, method='parallel'):
+ def multi_union(cls, polygons):
"""
Return a new `SphericalPolygon` that is the union of all of the
polygons in *polygons*.
@@ -454,21 +476,6 @@ class SphericalPolygon(object):
----------
polygons : sequence of `SphericalPolygon`
- method : 'parallel' or 'serial', optional
- Specifies the method that is used to perform the unions:
-
- - 'parallel' (default): A graph is built using all of
- the polygons, and the union operation is computed on
- the entire thing globally.
-
- - 'serial': The polygon is built in steps by adding one
- polygon at a time and unioning 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
@@ -483,17 +490,9 @@ class SphericalPolygon(object):
import graph
- if method.lower() == 'parallel':
- g = graph.Graph(polygons)
- polygon = g.union()
- return cls(polygon, polygons[0]._inside)
- elif method.lower() == 'serial':
- result = copy(polygons[0])
- for polygon in polygons[1:]:
- result = result.union(polygon)
- return result
- else:
- raise ValueError("method must be 'parallel' or 'serial'")
+ g = graph.Graph(polygons)
+ polygon = g.union()
+ return cls(polygon, polygons[0]._inside)
@staticmethod
def _find_new_inside(points, polygons):
@@ -670,6 +669,8 @@ class SphericalPolygon(object):
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],
diff --git a/lib/test/test_intersection.py b/lib/test/test_intersection.py
index adb79f0..12b3ce7 100644
--- a/lib/test/test_intersection.py
+++ b/lib/test/test_intersection.py
@@ -70,7 +70,7 @@ class intersection_test:
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])
+ assert_array_almost_equal(areas, areas[0], decimal=3)
return run
diff --git a/lib/test/test_union.py b/lib/test/test_union.py
index c963b76..d29eb25 100644
--- a/lib/test/test_union.py
+++ b/lib/test/test_union.py
@@ -36,36 +36,35 @@ class union_test:
step_size = int(max(float(num_permutations) / 20.0, 1.0))
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_union_%04d.svg' % (
- func.__name__, method, i)
- print(filename)
-
- union = polygon.SphericalPolygon.multi_union(
- permutation, method=method)
- unions.append(union)
- areas = [x.area() for x in permutation]
- union_area = union.area()
- assert np.all(union_area >= areas)
-
- 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()
+ 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)
+ areas = [x.area() for x in permutation]
+ union_area = union.area()
+ assert np.all(union_area >= areas)
+
+ 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()
lengths = np.array([len(x._points) for x in unions])
assert np.all(lengths == [lengths[0]])
@@ -158,6 +157,28 @@ def test6():
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]
+
+
if __name__ == '__main__':
if '--profile' not in sys.argv:
GRAPH_MODE = True
diff --git a/lib/vector.py b/lib/vector.py
index 7323c92..5d26161 100644
--- a/lib/vector.py
+++ b/lib/vector.py
@@ -198,9 +198,6 @@ def rotate_around(x, y, z, u, v, w, theta, degrees=True):
if degrees:
theta = np.deg2rad(theta)
- u2 = u**2
- v2 = v**2
- w2 = w**2
costheta = np.cos(theta)
sintheta = np.sin(theta)
icostheta = 1.0 - costheta
@@ -211,3 +208,34 @@ def rotate_around(x, y, z, u, v, w, theta, degrees=True):
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