LSSTApplications  12.1-5-gbdcc3ab,15.0+10,15.0+19,15.0-1-g19261fa+10,15.0-1-g60afb23+19,15.0-1-g615e0bb+11,15.0-1-g6668b0b+6,15.0-1-g788a293+19,15.0-1-ga91101e+19,15.0-1-gae1598d+9,15.0-1-gd076f1f+17,15.0-1-gdf18595+3,15.0-1-gf4f1c34+9,15.0-10-g113cadf7+2,15.0-11-g5674e3b,15.0-2-g100d730+12,15.0-2-g20c4630+8,15.0-2-g35685a8+15,15.0-2-g5dfaa72+8,15.0-2-gf38729e+14,15.0-24-g02ed2a30c+2,15.0-3-g11fe1a0+3,15.0-3-g130a88a+2,15.0-3-g707930d+1,15.0-3-g9103c06+9,15.0-3-ga03b4ca+26,15.0-3-gaec6799+6,15.0-4-g32c2b40+2,15.0-4-g535e784+3,15.0-4-g654b129+17,15.0-5-g23e394c+7,15.0-5-g54bfcd9+2,15.0-5-gb31927c,15.0-6-g4418537+2,15.0-7-g0c26201,15.0-7-g6bb3a066+2,15.0-9-g5661f8f+4,w.2018.18
LSSTDataManagementBasePackage
geometry.py
Go to the documentation of this file.
1 from __future__ import division
2 from builtins import map
3 from builtins import range
4 from builtins import object
5 #
6 # LSST Data Management System
7 # Copyright 2008, 2009, 2010 LSST Corporation.
8 #
9 # This product includes software developed by the
10 # LSST Project (http://www.lsst.org/).
11 #
12 # This program is free software: you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation, either version 3 of the License, or
15 # (at your option) any later version.
16 #
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the LSST License Statement and
23 # the GNU General Public License along with this program. If not,
24 # see <http://www.lsstcorp.org/LegalNotices/>.
25 #
26 
27 import math
28 import numbers
29 import operator
30 
31 
32 # Unit conversion factors
33 ARCSEC_PER_DEG = 3600.0
34 DEG_PER_ARCSEC = 1.0 / 3600.0
35 
36 # Angle comparison slack
37 ANGLE_EPSILON = 0.001 * DEG_PER_ARCSEC # 1 milli-arcsec in degrees
38 
39 # Used in pole proximity tests
40 POLE_EPSILON = 1.0 * DEG_PER_ARCSEC # 1 arcsec in degrees
41 
42 # Dot product of 2 cartesian unit vectors must be < COS_MAX,
43 # or they are considered degenerate.
44 COS_MAX = 1.0 - 1.0e-15
45 
46 # Square norm of the cross product of 2 cartesian unit vectors must be
47 # >= CROSS_N2MIN, or the edge joining them is considered degenerate
48 CROSS_N2MIN = 2e-15
49 
50 # Dot product of a unit plane normal and a cartesian unit vector must be
51 # > SIN_MIN, or the vector is considered to be on the plane
52 SIN_MIN = math.sqrt(CROSS_N2MIN)
53 
54 # Constants related to floating point arithmetic
55 INF = float('inf')
56 NEG_INF = float('-inf')
57 try:
58  # python 2.6+
59  from sys import float_info
60  MAX_FLOAT = float_info.max
61  MIN_FLOAT = float_info.min
62  EPSILON = float_info.epsilon
63 except ImportError:
64  # assume python float is an IEEE754 double
65  MAX_FLOAT = 1.7976931348623157e+308
66  MIN_FLOAT = 2.2250738585072014e-308
67  EPSILON = 2.2204460492503131e-16
68 
69 if hasattr(math, 'isinf'):
70  # python 2.6+
71  isinf = math.isinf
72 else:
73  def isinf(x):
74  return x == INF or x == NEG_INF
75 
76 # -- Utility methods ----
77 
78 
79 def dot(v1, v2):
80  """Returns the dot product of cartesian 3-vectors v1 and v2.
81  """
82  return v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]
83 
84 
85 def cross(v1, v2):
86  """Returns the cross product of cartesian 3-vectors v1 and v2.
87  """
88  return (v1[1] * v2[2] - v1[2] * v2[1],
89  v1[2] * v2[0] - v1[0] * v2[2],
90  v1[0] * v2[1] - v1[1] * v2[0])
91 
92 
93 def invScale(v, s):
94  """Returns a copy of the cartesian 3-vector v scaled by 1 / s.
95  """
96  return (v[0] / s, v[1] / s, v[2] / s)
97 
98 
99 def normalize(v):
100  """Returns a normalized copy of the cartesian 3-vector v.
101  """
102  n = math.sqrt(dot(v, v))
103  if n == 0.0:
104  raise RuntimeError('Cannot normalize a 3-vector with 0 magnitude')
105  return (v[0] / n, v[1] / n, v[2] / n)
106 
107 
108 def sphericalCoords(*args):
109  """Returns spherical coordinates in degrees for the input coordinates,
110  which can be spherical or 3D cartesian. The 2 (spherical) or 3
111  (cartesian 3-vector) inputs can be passed either individually
112  or as a tuple/list, and can be of any type convertible to a float.
113  """
114  if len(args) == 1:
115  args = args[0]
116  if len(args) == 2:
117  t = (float(args[0]), float(args[1]))
118  if t[1] < -90.0 or t[1] > 90.0:
119  raise RuntimeError('Latitude angle is out of bounds')
120  return t
121  elif len(args) == 3:
122  x = float(args[0])
123  y = float(args[1])
124  z = float(args[2])
125  d2 = x * x + y * y
126  if d2 == 0.0:
127  theta = 0.0
128  else:
129  theta = math.degrees(math.atan2(y, x))
130  if theta < 0.0:
131  theta += 360.0
132  if z == 0.0:
133  phi = 0.0
134  else:
135  phi = math.degrees(math.atan2(z, math.sqrt(d2)))
136  return (theta, phi)
137  raise TypeError('Expecting 2 or 3 coordinates, or a tuple/list ' +
138  'containing 2 or 3 coordinates')
139 
140 
142  """Returns a unit cartesian 3-vector corresponding to the input
143  coordinates, which can be spherical or 3D cartesian. The 2 (spherical)
144  or 3 (cartesian 3-vector) input coordinates can either be passed
145  individually or as a tuple/list, and can be of any type convertible
146  to a float.
147  """
148  if len(args) == 1:
149  args = args[0]
150  if len(args) == 2:
151  theta = math.radians(float(args[0]))
152  phi = math.radians(float(args[1]))
153  cosPhi = math.cos(phi)
154  return (math.cos(theta) * cosPhi,
155  math.sin(theta) * cosPhi,
156  math.sin(phi))
157  elif len(args) == 3:
158  x = float(args[0])
159  y = float(args[1])
160  z = float(args[2])
161  n = math.sqrt(x * x + y * y + z * z)
162  if n == 0.0:
163  raise RuntimeError('Cannot normalize a 3-vector with 0 magnitude')
164  return (x / n, y / n, z / n)
165  raise TypeError('Expecting 2 or 3 coordinates, or a tuple/list ' +
166  'containing 2 or 3 coordinates')
167 
168 
170  """Returns the angular separation in degrees between points p1 and p2,
171  which must both be specified in spherical coordinates. The implementation
172  uses the halversine distance formula.
173  """
174  sdt = math.sin(math.radians(p1[0] - p2[0]) * 0.5)
175  sdp = math.sin(math.radians(p1[1] - p2[1]) * 0.5)
176  cc = math.cos(math.radians(p1[1])) * math.cos(math.radians(p2[1]))
177  s = math.sqrt(sdp * sdp + cc * sdt * sdt)
178  if s > 1.0:
179  return 180.0
180  else:
181  return 2.0 * math.degrees(math.asin(s))
182 
183 
184 def clampPhi(phi):
185  """Clamps the input latitude angle phi to [-90.0, 90.0] deg.
186  """
187  if phi < -90.0:
188  return -90.0
189  elif phi >= 90.0:
190  return 90.0
191  return phi
192 
193 
194 def reduceTheta(theta):
195  """Range reduces the given longitude angle to lie in the range
196  [0.0, 360.0).
197  """
198  theta = math.fmod(theta, 360.0)
199  if theta < 0.0:
200  return theta + 360.0
201  else:
202  return theta
203 
204 
205 def northEast(v):
206  """Returns unit N,E basis vectors for a point v, which must be a
207  cartesian 3-vector.
208  """
209  north = (-v[0] * v[2],
210  -v[1] * v[2],
211  v[0] * v[0] + v[1] * v[1])
212  if north == (0.0, 0.0, 0.0):
213  # pick an arbitrary orthogonal basis with z = 0
214  north = (-1.0, 0.0, 0.0)
215  east = (0.0, 1.0, 0.0)
216  else:
217  north = normalize(north)
218  east = normalize(cross(north, v))
219  return north, east
220 
221 
222 def alpha(r, centerPhi, phi):
223  """Returns the longitude angle alpha of the intersections (alpha, phi),
224  (-alpha, phi) of the circle centered on (0, centerPhi) with radius r and
225  the plane z = sin(phi). If there is no intersection, None is returned.
226  """
227  if phi < centerPhi - r or phi > centerPhi + r:
228  return None
229  a = abs(centerPhi - phi)
230  if a <= r - (90.0 - phi) or a <= r - (90.0 + phi):
231  return None
232  p = math.radians(phi)
233  cp = math.radians(centerPhi)
234  x = math.cos(math.radians(r)) - math.sin(cp) * math.sin(cp)
235  u = math.cos(cp) * math.cos(p)
236  y = math.sqrt(abs(u * u - x * x))
237  return math.degrees(abs(math.atan2(y, x)))
238 
239 
240 def maxAlpha(r, centerPhi):
241  """Computes alpha, the extent in longitude angle [-alpha, alpha] of the
242  circle with radius r and center (0, centerPhi) on the unit sphere.
243  Both r and centerPhi are assumed to be in units of degrees.
244  centerPhi is clamped to lie in the range [-90,90] and r must
245  lie in the range [0, 90].
246  """
247  assert r >= 0.0 and r <= 90.0
248  if r == 0.0:
249  return 0.0
250  centerPhi = clampPhi(centerPhi)
251  if abs(centerPhi) + r > 90.0 - POLE_EPSILON:
252  return 180.0
253  r = math.radians(r)
254  c = math.radians(centerPhi)
255  y = math.sin(r)
256  x = math.sqrt(abs(math.cos(c - r) * math.cos(c + r)))
257  return math.degrees(abs(math.atan(y / x)))
258 
259 
261  """Returns the angular separation in degrees between points v1 and v2,
262  which must both be specified as cartesian 3-vectors.
263  """
264  cs = dot(v1, v2)
265  n = cross(v1, v2)
266  ss = math.sqrt(dot(n, n))
267  if cs == 0.0 and ss == 0.0:
268  return 0.0
269  return math.degrees(math.atan2(ss, cs))
270 
271 
272 def minEdgeSep(p, n, v1, v2):
273  """Returns the minimum angular separation in degrees between p
274  and points on the great circle edge with plane normal n and
275  vertices v1, v2. All inputs must be unit cartesian 3-vectors.
276  """
277  p1 = cross(n, v1)
278  p2 = cross(n, v2)
279  if dot(p1, p) >= 0.0 and dot(p2, p) <= 0.0:
280  return abs(90.0 - cartesianAngularSep(p, n))
281  else:
282  return min(cartesianAngularSep(p, v1), cartesianAngularSep(p, v2))
283 
284 
285 def minPhiEdgeSep(p, phi, minTheta, maxTheta):
286  """Returns the minimum angular separation in degrees between p
287  and points on the small circle edge with constant latitude angle
288  phi and vertices (minTheta, phi), (maxTheta, phi). p must be in
289  spherical coordinates.
290  """
291  if minTheta > maxTheta:
292  if p[0] >= minTheta or p[0] <= maxTheta:
293  return abs(p[1] - phi)
294  else:
295  if p[0] >= minTheta and p[0] <= maxTheta:
296  return abs(p[1] - phi)
297  return min(sphericalAngularSep(p, (minTheta, phi)),
298  sphericalAngularSep(p, (maxTheta, phi)))
299 
300 
301 def minThetaEdgeSep(p, theta, minPhi, maxPhi):
302  """Returns the minimum angular separation in degrees between p
303  and points on the great circle edge with constant longitude angle
304  theta and vertices (theta, minPhi), (theta, maxPhi). p must be a
305  unit cartesian 3-vector.
306  """
307  v1 = cartesianUnitVector(theta, minPhi)
308  v2 = cartesianUnitVector(theta, maxPhi)
309  n = cross(v1, v2)
310  d2 = dot(n, n)
311  if d2 == 0.0:
312  return min(cartesianAngularSep(p, v1), cartesianAngularSep(p, v2))
313  return minEdgeSep(p, invScale(n, math.sqrt(d2)), v1, v2)
314 
315 
316 def centroid(vertices):
317  """Computes the centroid of a set of vertices (which must be passed in
318  as a list of cartesian unit vectors) on the unit sphere.
319  """
320  x, y, z = 0.0, 0.0, 0.0
321  # Note: could use more accurate summation routines
322  for v in vertices:
323  x += v[0]
324  y += v[1]
325  z += v[2]
326  return normalize((x, y, z))
327 
328 
329 def between(p, n, v1, v2):
330  """Tests whether p lies on the shortest great circle arc from cartesian
331  unit vectors v1 to v2, assuming that p is a unit vector on the plane
332  defined by the origin, v1 and v2.
333  """
334  p1 = cross(n, v1)
335  p2 = cross(n, v2)
336  return dot(p1, p) >= 0.0 and dot(p2, p) <= 0.0
337 
338 
339 def segments(phiMin, phiMax, width):
340  """Computes the number of segments to divide the given latitude angle
341  range [phiMin, phiMax] (degrees) into. Two points within the range
342  separated by at least one segment are guaranteed to have angular
343  separation of at least width degrees.
344  """
345  p = max(abs(phiMin), abs(phiMax))
346  if p > 90.0 - 1.0 * DEG_PER_ARCSEC:
347  return 1
348  if width >= 180.0:
349  return 1
350  elif width < 1.0 * DEG_PER_ARCSEC:
351  width = 1.0 * DEG_PER_ARCSEC
352  p = math.radians(p)
353  cw = math.cos(math.radians(width))
354  sp = math.sin(p)
355  cp = math.cos(p)
356  x = cw - sp * sp
357  u = cp * cp
358  y = math.sqrt(abs(u * u - x * x))
359  return int(math.floor((2.0 * math.pi) / abs(math.atan2(y, x))))
360 
361 
362 # -- Regions on the unit sphere ----
363 
364 # TODO: make this an ABC once python 2.6 becomes the default
366  """Base class for regions on the unit sphere.
367  """
368  pass
369 
370 
371 class SphericalBox(SphericalRegion):
372  """A spherical coordinate space bounding box.
373 
374  This is similar to a bounding box in cartesian space in that
375  it is specified by a pair of points; however, a spherical box may
376  correspond to the entire unit-sphere, a spherical cap, a lune or
377  the traditional rectangle. Additionally, spherical boxes can span
378  the 0/360 degree longitude angle discontinuity.
379 
380  Note that points falling exactly on spherical box edges are
381  considered to be inside (contained by) the box.
382  """
383 
384  def __init__(self, *args):
385  """
386  Creates a new spherical box. If no arguments are supplied, then
387  an empty box is created. If the arguments consist of a single
388  SphericalRegion, then a copy of its bounding box is created.
389  Otherwise, the arguments must consist of a pair of 2 (spherical)
390  or 3 (cartesian 3-vector) element coordinate tuples/lists that
391  specify the minimum/maximum longitude/latitude angles for the box.
392  Latitude angles must be within [-90, 90] degrees, and the minimum
393  latitude angle must be less than or equal to the maximum. If both
394  minimum and maximum longitude angles lie in the range [0.0, 360.0],
395  then the maximum can be less than the minimum. For example, a box
396  with min/max longitude angles of 350/10 deg spans the longitude angle
397  ranges [350, 360) and [0, 10]. Otherwise, the minimum must be less
398  than or equal to the maximum, though values can be arbitrary. If
399  the two are are separated by 360 degrees or more, then the box
400  spans longitude angles [0, 360). Otherwise, both values are range
401  reduced. For example, a spherical box with min/max longitude angles
402  specified as 350/370 deg spans longitude angle ranges [350, 360) and
403  [0, 10].
404  """
405  if len(args) == 0:
406  self.setEmpty()
407  return
408  elif len(args) == 1:
409  if isinstance(args[0], SphericalRegion):
410  bbox = args[0].getBoundingBox()
411  self.min = tuple(bbox.getMin())
412  self.max = tuple(bbox.getMax())
413  return
414  args = args[0]
415  if len(args) == 2:
416  self.min = sphericalCoords(args[0])
417  self.max = sphericalCoords(args[1])
418  else:
419  raise TypeError('Expecting a spherical region, 2 points, '
420  'or a tuple/list containing 2 points')
421  if self.min[1] > self.max[1]:
422  raise RuntimeError(
423  'Latitude angle minimum is greater than maximum')
424  if (self.max[0] < self.min[0] and
425  (self.max[0] < 0.0 or self.min[0] > 360.0)):
426  raise RuntimeError(
427  'Longitude angle minimum is greater than maximum')
428  # Range-reduce longitude angles
429  if self.max[0] - self.min[0] >= 360.0:
430  self.min = (0.0, self.min[1])
431  self.max = (360.0, self.max[1])
432  else:
433  self.min = (reduceTheta(self.min[0]), self.min[1])
434  self.max = (reduceTheta(self.max[0]), self.max[1])
435 
436  def wraps(self):
437  """Returns True if this spherical box wraps across the 0/360
438  degree longitude angle discontinuity.
439  """
440  return self.min[0] > self.max[0]
441 
442  def getBoundingBox(self):
443  """Returns a bounding box for this spherical region.
444  """
445  return self
446 
447  def getMin(self):
448  """Returns the minimum longitude and latitude angles of this
449  spherical box as a 2-tuple of floats (in units of degrees).
450  """
451  return self.min
452 
453  def getMax(self):
454  """Returns the maximum longitude and latitude angles of this
455  spherical box as a 2-tuple of floats (in units of degrees).
456  """
457  return self.max
458 
459  def getThetaExtent(self):
460  """Returns the extent in longitude angle of this box.
461  """
462  if self.wraps():
463  return 360.0 - self.min[0] + self.max[0]
464  else:
465  return self.max[0] - self.min[0]
466 
467  def getCenter(self):
468  """Returns an 2-tuple of floats corresponding to the longitude/latitude
469  angles (in degrees) of the center of this spherical box.
470  """
471  centerTheta = 0.5 * (self.min[0] + self.max[0])
472  centerPhi = 0.5 * (self.min[1] + self.max[1])
473  if self.wraps():
474  if centerTheta >= 180.0:
475  centerTheta -= 180.0
476  else:
477  centerTheta += 180.0
478  return (centerTheta, centerPhi)
479 
480  def isEmpty(self):
481  """Returns True if this spherical box contains no points.
482  """
483  return self.min[1] > self.max[1]
484 
485  def isFull(self):
486  """Returns True if this spherical box contains every point
487  on the unit sphere.
488  """
489  return self.min == (0.0, -90.0) and self.max == (360.0, 90.0)
490 
491  def containsPoint(self, p):
492  """Returns True if this spherical box contains the given point,
493  which must be specified in spherical coordinates.
494  """
495  if p[1] < self.min[1] or p[1] > self.max[1]:
496  return False
497  theta = reduceTheta(p[0])
498  if self.wraps():
499  return theta >= self.min[0] or theta <= self.max[0]
500  else:
501  return theta >= self.min[0] and theta <= self.max[0]
502 
503  def contains(self, pointOrRegion):
504  """Returns True if this spherical box completely contains the given
505  point or spherical region. Note that the implementation is
506  conservative where ellipses are concerned: False may be returned
507  for an ellipse that is actually completely contained in this box.
508  """
509  if self.isEmpty():
510  return False
511  if isinstance(pointOrRegion, SphericalRegion):
512  b = pointOrRegion.getBoundingBox()
513  if b.isEmpty():
514  return False
515  if b.min[1] < self.min[1] or b.max[1] > self.max[1]:
516  return False
517  if self.wraps():
518  if b.wraps():
519  return b.min[0] >= self.min[0] and b.max[0] <= self.max[0]
520  else:
521  return b.min[0] >= self.min[0] or b.max[0] <= self.max[0]
522  else:
523  if b.wraps():
524  return self.min[0] == 0.0 and self.max[0] == 360.0
525  else:
526  return b.min[0] >= self.min[0] and b.max[0] <= self.max[0]
527  else:
528  return self.containsPoint(sphericalCoords(pointOrRegion))
529 
530  def intersects(self, pointOrRegion):
531  """Returns True if this spherical box intersects the given point
532  or spherical region. Note that the implementation is conservative:
533  True may be returned for a region that does not actually intersect
534  this box.
535  """
536  if self.isEmpty():
537  return False
538  if isinstance(pointOrRegion, SphericalBox):
539  b = pointOrRegion
540  if b.isEmpty():
541  return False
542  if b.min[1] > self.max[1] or b.max[1] < self.min[1]:
543  return False
544  if self.wraps():
545  if b.wraps():
546  return True
547  else:
548  return b.min[0] <= self.max[0] or b.max[0] >= self.min[0]
549  else:
550  if b.wraps():
551  return self.min[0] <= b.max[0] or self.max[0] >= b.min[0]
552  else:
553  return self.min[0] <= b.max[0] and self.max[0] >= b.min[0]
554  elif isinstance(pointOrRegion, SphericalRegion):
555  return pointOrRegion.intersects(self)
556  else:
557  return self.containsPoint(sphericalCoords(pointOrRegion))
558 
559  def extend(self, pointOrRegion):
560  """Extends this box to the smallest spherical box S containing
561  the union of this box with the specified point or spherical region.
562  """
563  if self == pointOrRegion:
564  return self
565  if isinstance(pointOrRegion, SphericalRegion):
566  b = pointOrRegion.getBoundingBox()
567  if b.isEmpty():
568  return self
569  elif self.isEmpty():
570  self.min = tuple(b.min)
571  self.max = tuple(b.max)
572  minPhi = min(self.min[1], b.min[1])
573  maxPhi = max(self.max[1], b.max[1])
574  minTheta = self.min[0]
575  maxTheta = self.max[0]
576  if self.wraps():
577  if b.wraps():
578  minMinRa = min(self.min[0], b.min[0])
579  maxMaxRa = max(self.max[0], b.max[0])
580  if maxMaxRa >= minMinRa:
581  minTheta = 0.0
582  maxTheta = 360.0
583  else:
584  minTheta = minMinRa
585  maxTheta = maxMaxRa
586  else:
587  if b.min[0] <= self.max[0] and b.max[0] >= self.min[0]:
588  minTheta = 0.0
589  maxTheta = 360.0
590  elif b.min[0] - self.max[0] > self.min[0] - b.max[0]:
591  minTheta = b.min[0]
592  else:
593  maxTheta = b.max[0]
594  else:
595  if b.wraps():
596  if self.min[0] <= b.max[0] and self.max[0] >= b.min[0]:
597  minTheta = 0.0
598  maxTheta = 360.0
599  elif self.min[0] - b.max[0] > b.min[0] - self.max[0]:
600  maxTheta = b.max[0]
601  else:
602  minTheta = b.min[0]
603  else:
604  if b.min[0] > self.max[0]:
605  if (360.0 - b.min[0] + self.max[0] <
606  b.max[0] - self.min[0]):
607  minTheta = b.min[0]
608  else:
609  maxTheta = b.max[0]
610  elif self.min[0] > b.max[0]:
611  if (360.0 - self.min[0] + b.max[0] <
612  self.max[0] - b.min[0]):
613  maxTheta = b.max[0]
614  else:
615  minTheta = b.min[0]
616  else:
617  minTheta = min(self.min[0], b.min[0])
618  maxTheta = max(self.max[0], b.max[0])
619  self.min = (minTheta, minPhi)
620  self.max = (maxTheta, maxPhi)
621  else:
622  p = sphericalCoords(pointOrRegion)
623  theta, phi = reduceTheta(p[0]), p[1]
624  if self.containsPoint(p):
625  return self
626  elif self.isEmpty():
627  self.min = (theta, phi)
628  self.max = (theta, phi)
629  else:
630  minPhi = min(self.min[1], phi)
631  maxPhi = max(self.max[1], phi)
632  minTheta = self.min[0]
633  maxTheta = self.max[0]
634  if self.wraps():
635  if self.min[0] - theta > theta - self.max[0]:
636  maxTheta = theta
637  else:
638  minTheta = theta
639  elif theta < self.min[0]:
640  if self.min[0] - theta <= 360.0 - self.max[0] + theta:
641  minTheta = theta
642  else:
643  maxTheta = theta
644  elif theta - self.max[0] <= 360.0 - theta + self.min[0]:
645  maxTheta = theta
646  else:
647  minTheta = theta
648  self.min = (minTheta, minPhi)
649  self.max = (maxTheta, maxPhi)
650  return self
651 
652  def shrink(self, box):
653  """Shrinks this box to the smallest spherical box containing
654  the intersection of this box and the specified one.
655  """
656  b = box
657  if not isinstance(b, SphericalBox):
658  raise TypeError('Expecting a SphericalBox object')
659  if self == b or self.isEmpty():
660  return self
661  elif b.isEmpty():
662  return self.setEmpty()
663  minPhi = max(self.min[1], b.min[1])
664  maxPhi = min(self.max[1], b.max[1])
665  minTheta = self.min[0]
666  maxTheta = self.max[0]
667  if self.wraps():
668  if b.wraps():
669  minTheta = max(minTheta, b.min[0])
670  maxTheta = min(maxTheta, b.max[0])
671  else:
672  if b.max[0] >= minTheta:
673  if b.min[0] <= maxTheta:
674  if b.max[0] - b.min[0] <= 360.0 - minTheta + maxTheta:
675  minTheta = b.min[0]
676  maxTheta = b.max[0]
677  else:
678  minTheta = max(minTheta, b.min[0])
679  maxTheta = b.max[0]
680  elif b.min[0] <= maxTheta:
681  minTheta = b.min[0]
682  maxTheta = min(maxTheta, b.max[0])
683  else:
684  minPhi = 90.0
685  maxPhi = -90.0
686  else:
687  if b.wraps():
688  if maxTheta >= b.min[0]:
689  if minTheta <= b.max[0]:
690  if maxTheta - minTheta > 360.0 - b.min[0] + b.max[0]:
691  minTheta = b.min[0]
692  maxTheta = b.max[0]
693  else:
694  minTheta = max(minTheta, b.min[0])
695  elif minTheta <= b.max[0]:
696  maxTheta = b.max[0]
697  else:
698  minPhi = 90.0
699  maxPhi = -90.0
700  elif minTheta > b.max[0] or maxTheta < b.min[0]:
701  minPhi = 90.0
702  maxPhi = -90.0
703  else:
704  minTheta = max(minTheta, b.min[0])
705  maxTheta = min(maxTheta, b.max[0])
706  self.min = (minTheta, minPhi)
707  self.max = (maxTheta, maxPhi)
708  return self
709 
710  def setEmpty(self):
711  """Empties this spherical box.
712  """
713  self.min = (0.0, 90.0)
714  self.max = (0.0, -90.0)
715  return self
716 
717  def setFull(self):
718  """Expands this spherical box to fill the unit sphere.
719  """
720  self.min = (0.0, -90.0)
721  self.max = (360.0, 90.0)
722  return self
723 
724  def __repr__(self):
725  """Returns a string representation of this spherical box.
726  """
727  if self.isEmpty():
728  return ''.join([self.__class__.__name__, '(', ')'])
729  return ''.join([self.__class__.__name__, '(',
730  repr(self.min), ', ', repr(self.max), ')'])
731 
732  def __eq__(self, other):
733  if isinstance(other, SphericalBox):
734  if self.isEmpty() and other.isEmpty():
735  return True
736  return self.min == other.min and self.max == other.max
737  return False
738 
739  def __hash__(self):
740  return hash((self.min, self.max))
741 
742  @staticmethod
743  def edge(v1, v2, n):
744  """Returns a spherical bounding box for the great circle edge
745  connecting v1 to v2 with plane normal n. All arguments must be
746  cartesian unit vectors.
747  """
748  theta1, phi1 = sphericalCoords(v1)
749  theta2, phi2 = sphericalCoords(v2)
750  # Compute latitude angle range of the edge
751  minPhi = min(phi1, phi2)
752  maxPhi = max(phi1, phi2)
753  d = n[0] * n[0] + n[1] * n[1]
754  if abs(d) > MIN_FLOAT:
755  # compute the 2 (antipodal) latitude angle extrema of n
756  if abs(n[2]) <= SIN_MIN:
757  ex = (0.0, 0.0, -1.0)
758  else:
759  ex = (n[0] * n[2] / d, n[1] * n[2] / d, -d)
760  # check whether either extremum is inside the edge
761  if between(ex, n, v1, v2):
762  minPhi = min(minPhi, sphericalCoords(ex)[1])
763  ex = (-ex[0], -ex[1], -ex[2])
764  if between(ex, n, v1, v2):
765  maxPhi = max(maxPhi, sphericalCoords(ex)[1])
766  # Compute longitude angle range of the edge
767  if abs(n[2]) <= SIN_MIN:
768  # great circle is very close to a pole
769  d = min(abs(theta1 - theta2), abs(360.0 - theta1 + theta2))
770  if d >= 90.0 and d <= 270.0:
771  # d is closer to 180 than 0/360: edge crosses over a pole
772  minTheta = 0.0
773  maxTheta = 360.0
774  else:
775  # theta1 and theta2 are nearly identical
776  minTheta = min(theta1, theta2)
777  maxTheta = max(theta1, theta2)
778  if maxTheta - minTheta > 180.0:
779  # min/max on opposite sides of 0/360
780  # longitude angle discontinuity
781  tmp = maxTheta
782  maxTheta = minTheta
783  minTheta = tmp
784  elif n[2] > 0.0:
785  minTheta = theta1
786  maxTheta = theta2
787  else:
788  minTheta = theta2
789  maxTheta = theta1
790  # return results
791  return SphericalBox((minTheta, minPhi), (maxTheta, maxPhi))
792 
793 
795  """A circle on the unit sphere. Points falling exactly on the
796  circle are considered to be inside (contained by) the circle.
797  """
798 
799  def __init__(self, center, radius):
800  """Creates a new spherical circle with the given center and radius.
801  """
802  self.center = sphericalCoords(center)
803  self.radius = float(radius)
804  self.boundingBox = None
805  if self.radius < 0.0 or self.radius > 180.0:
806  raise RuntimeError(
807  'Circle radius is negative or greater than 180 deg')
808  self.center = (reduceTheta(self.center[0]), self.center[1])
809 
810  def getBoundingBox(self):
811  """Returns a bounding box for this spherical circle.
812  """
813  if self.boundingBox is None:
814  if self.isEmpty():
815  self.boundingBox = SphericalBox()
816  elif self.isFull():
817  self.boundingBox = SphericalBox()
818  self.boundingBox.setFull()
819  else:
820  alpha = maxAlpha(self.radius, self.center[1])
821  minPhi = clampPhi(self.center[1] - self.radius)
822  maxPhi = clampPhi(self.center[1] + self.radius)
823  if alpha > 180.0 - ANGLE_EPSILON:
824  minTheta = 0.0
825  maxTheta = 360.0
826  else:
827  minTheta = self.center[0] - alpha
828  maxTheta = self.center[0] + alpha
829  self.boundingBox = SphericalBox((minTheta, minPhi),
830  (maxTheta, maxPhi))
831  return self.boundingBox
832 
833  def getBoundingCircle(self):
834  return self
835 
836  def getCenter(self):
837  """Returns an (ra, dec) 2-tuple of floats corresponding to the
838  center of this circle.
839  """
840  return self.center
841 
842  def getRadius(self):
843  """Returns the radius (degrees) of this circle.
844  """
845  return self.radius
846 
847  def isEmpty(self):
848  """Returns True if this circle contains no points.
849  """
850  return self.radius < 0.0
851 
852  def isFull(self):
853  """Returns True if this spherical box contains every point
854  on the unit sphere.
855  """
856  return self.radius >= 180.0
857 
858  def contains(self, pointOrRegion):
859  """Returns True if the specified point or spherical region is
860  completely contained in this circle. Note that the implementation
861  is conservative where ellipses are concerned: False may be returned
862  for an ellipse that is actually completely contained in this circle.
863  """
864  if self.isEmpty():
865  return False
866  pr = pointOrRegion
867  c = self.center
868  r = self.radius
869  if isinstance(pr, SphericalBox):
870  if pr.isEmpty():
871  return False
872  minp = pr.getMin()
873  maxp = pr.getMax()
874  if (sphericalAngularSep(c, minp) > r or
875  sphericalAngularSep(c, maxp) > r or
876  sphericalAngularSep(c, (minp[0], maxp[1])) > r or
877  sphericalAngularSep(c, (maxp[0], minp[1])) > r):
878  return False
879  a = alpha(r, c[1], minp[1])
880  if a is not None:
881  if (pr.containsPoint((c[0] + a, minp[1])) or
882  pr.containsPoint((c[0] - a, minp[1]))):
883  return False
884  a = alpha(r, c[1], maxp[1])
885  if a is not None:
886  if (pr.containsPoint((c[0] + a, maxp[1])) or
887  pr.containsPoint((c[0] - a, maxp[1]))):
888  return False
889  return True
890  elif isinstance(pr, SphericalCircle):
891  if pr.isEmpty():
892  return False
893  return sphericalAngularSep(c, pr.center) <= r - pr.radius
894  elif isinstance(pr, SphericalEllipse):
895  bc = pr.getBoundingCircle()
896  return sphericalAngularSep(c, bc.center) <= r - bc.radius
897  elif isinstance(pr, SphericalConvexPolygon):
898  p = cartesianUnitVector(c)
899  for v in pr.getVertices():
900  if cartesianAngularSep(p, v) > r:
901  return False
902  return True
903  else:
904  return sphericalAngularSep(c, sphericalCoords(pr)) <= r
905 
906  def intersects(self, pointOrRegion):
907  """Returns True if the given point or spherical region intersects
908  this circle. Note that the implementation is conservative where
909  ellipses are concerned: True may be returned for an ellipse that
910  is actually disjoint from this circle.
911  """
912  if self.isEmpty():
913  return False
914  pr = pointOrRegion
915  c = self.center
916  r = self.radius
917  if isinstance(pr, SphericalBox):
918  if pr.isEmpty():
919  return False
920  elif pr.containsPoint(c):
921  return True
922  minp = pr.getMin()
923  maxp = pr.getMax()
924  if (minPhiEdgeSep(c, minp[1], minp[0], maxp[0]) <= r or
925  minPhiEdgeSep(c, maxp[1], minp[0], maxp[0]) <= r):
926  return True
927  p = cartesianUnitVector(c)
928  return (minThetaEdgeSep(p, minp[0], minp[1], maxp[1]) <= r or
929  minThetaEdgeSep(p, maxp[0], minp[1], maxp[1]) <= r)
930  elif isinstance(pr, SphericalCircle):
931  if pr.isEmpty():
932  return False
933  return sphericalAngularSep(c, pr.center) <= r + pr.radius
934  elif isinstance(pr, SphericalEllipse):
935  bc = pr.getBoundingCircle()
936  return sphericalAngularSep(c, bc.center) <= r + bc.radius
937  elif isinstance(pr, SphericalConvexPolygon):
938  return pr.intersects(self)
939  else:
940  return sphericalAngularSep(c, sphericalCoords(pr)) <= r
941 
942  def __repr__(self):
943  """Returns a string representation of this circle.
944  """
945  return ''.join([self.__class__.__name__, '(', repr(self.center),
946  ', ', repr(self.radius), ')'])
947 
948  def __eq__(self, other):
949  if isinstance(other, SphericalCircle):
950  if self.isEmpty() and other.isEmpty():
951  return True
952  if self.radius == other.radius:
953  if self.center[1] == other.center[1]:
954  if abs(self.center[1]) == 90.0:
955  return True
956  return self.center[0] == other.center[0]
957  return False
958 
959  def __hash__(self):
960  return hash((self.center, self.radius))
961 
963  """An ellipse on the unit sphere. This is a standard 2D cartesian
964  ellipse defined on the plane tangent to the unit sphere at the ellipse
965  center and then orthographically projected onto the surface of the
966  unit sphere.
967  """
968 
969  def __init__(self, center,
970  semiMajorAxisLength, semiMinorAxisLength, majorAxisAngle):
971  self.center = sphericalCoords(center)
972  self.semiMajorAxisLength = float(semiMajorAxisLength)
973  self.semiMinorAxisLength = float(semiMinorAxisLength)
974  a = math.fmod(float(majorAxisAngle), 180.0)
975  if a < 0.0:
976  a += 180.0
977  self.majorAxisAngle = a
978  self.boundingCircle = None
979  self.innerCircle = None
980  if self.semiMinorAxisLength < 0.0:
981  raise RuntimeError('Negative semi-minor axis length')
983  raise RuntimeError(
984  'Semi-major axis length is less than semi-minor axis length')
985  # large spherical ellipses don't make much sense
986  if self.semiMajorAxisLength > 10.0 * ARCSEC_PER_DEG:
987  raise RuntimeError(
988  'Semi-major axis length must be less than or equal to 10 deg')
989  self.center = (reduceTheta(self.center[0]), self.center[1])
990 
991  def getBoundingBox(self):
992  """Returns a bounding box for this spherical ellipse. Note that at
993  present this is conservative: a bounding box for the circle C with
994  radius equal to the semi-major axis length of this ellipse is returned.
995  """
996  return self.getBoundingCircle().getBoundingBox()
997 
998  def getBoundingCircle(self):
999  """Returns a bounding circle for this spherical ellipse. This is
1000  a circle with the same center as this ellipse and with radius
1001  equal to the arcsine of the semi-major axis length.
1002  """
1003  if self.boundingCircle is None:
1004  r = math.degrees(math.asin(math.radians(
1005  DEG_PER_ARCSEC * self.semiMajorAxisLength)))
1006  self.boundingCircle = SphericalCircle(self.center, r)
1007  return self.boundingCircle
1008 
1009  def getInnerCircle(self):
1010  """Returns the circle of maximum radius having the same center as
1011  this ellipse and which is completely contained in the ellipse.
1012  """
1013  if self.innerCircle is None:
1014  r = math.degrees(math.asin(math.radians(
1015  DEG_PER_ARCSEC * self.semiMinorAxisLength)))
1016  self.innerCircle = SphericalCircle(self.center, r)
1017  return self.innerCircle
1018 
1019  def getCenter(self):
1020  """Returns an (ra, dec) 2-tuple of floats corresponding to the center
1021  of this ellipse.
1022  """
1023  return self.center
1024 
1026  """Return the major axis angle (east of north, in degrees) for this
1027  ellipse.
1028  """
1029  return self.majorAxisAngle
1030 
1032  """Returns the semi-major axis length of this ellipse. Units
1033  are in arcsec since ellipses are typically small.
1034  """
1035  return self.semiMajorAxisLength
1036 
1038  """Returns the semi-minor axis length of this ellipse. Units
1039  are in arcsec since ellipses are typically small.
1040  """
1041  return self.semiMinorAxisLength
1042 
1043  def _containsPoint(self, v):
1044  theta = math.radians(self.center[0])
1045  phi = math.radians(self.center[1])
1046  ang = math.radians(self.majorAxisAngle)
1047  sinTheta = math.sin(theta)
1048  cosTheta = math.cos(theta)
1049  sinPhi = math.sin(phi)
1050  cosPhi = math.cos(phi)
1051  sinAng = math.sin(ang)
1052  cosAng = math.cos(ang)
1053  # get coords of input point in (N,E) basis
1054  n = cosPhi * v[2] - sinPhi * (sinTheta * v[1] + cosTheta * v[0])
1055  e = cosTheta * v[1] - sinTheta * v[0]
1056  # rotate by negated major axis angle
1057  x = sinAng * e + cosAng * n
1058  y = cosAng * e - sinAng * n
1059  # scale by inverse of semi-axis-lengths
1060  x /= math.radians(self.semiMajorAxisLength * DEG_PER_ARCSEC)
1061  y /= math.radians(self.semiMinorAxisLength * DEG_PER_ARCSEC)
1062  # Apply point in circle test for the unit circle centered at the origin
1063  return x * x + y * y <= 1.0
1064 
1065  def contains(self, pointOrRegion):
1066  """Returns True if the specified point or spherical region is
1067  completely contained in this ellipse. The implementation is
1068  conservative in the sense that False may be returned for a region
1069  that really is contained by this ellipse.
1070  """
1071  if isinstance(pointOrRegion, (tuple, list)):
1072  v = cartesianUnitVector(pointOrRegion)
1073  return self._containsPoint(v)
1074  else:
1075  return self.getInnerCircle().contains(pointOrRegion)
1076 
1077  def intersects(self, pointOrRegion):
1078  """Returns True if the specified point or spherical region intersects
1079  this ellipse. The implementation is conservative in the sense that
1080  True may be returned for a region that does not intersect this
1081  ellipse.
1082  """
1083  if isinstance(pointOrRegion, (tuple, list)):
1084  v = cartesianUnitVector(pointOrRegion)
1085  return self._containsPoint(v)
1086  else:
1087  return self.getBoundingCircle().intersects(pointOrRegion)
1088 
1089  def __repr__(self):
1090  """Returns a string representation of this ellipse.
1091  """
1092  return ''.join([
1093  self.__class__.__name__, '(', repr(self.center), ', ',
1094  repr(self.semiMajorAxisLength), ', ',
1095  repr(self.semiMinorAxisLength), ', ',
1096  repr(self.majorAxisAngle), ')'])
1097 
1098  def __eq__(self, other):
1099  if isinstance(other, SphericalEllipse):
1100  return (self.center == other.center and
1101  self.semiMajorAxisLength == other.semiMajorAxisLength and
1102  self.semiMinorAxisLength == other.semiMinorAxisLength and
1103  self.majorAxisAngle == other.majorAxisAngle)
1104  return False
1105 
1106  def __hash__(self):
1107  return hash((self.center, self.semiMajorAxisLength, self.semiMinorAxisLength, self.majorAxisAngle))
1108 
1109 
1111  """A convex polygon on the unit sphere with great circle edges. Points
1112  falling exactly on polygon edges are considered to be inside (contained
1113  by) the polygon.
1114  """
1115 
1116  def __init__(self, *args):
1117  """Creates a new polygon. Arguments must be either:
1118 
1119  1. a SphericalConvexPolygon
1120  2. a list of vertices
1121  3. a list of vertices and a list of corresponding edges
1122 
1123  In the first case, a copy is constructed. In the second case,
1124  the argument must be a sequence of 3 element tuples/lists
1125  (unit cartesian 3-vectors) - a copy of the vertices is stored
1126  and polygon edges are computed. In the last case, copies of the
1127  input vertex and edge lists are stored.
1128 
1129  Vertices must be hemispherical and in counter-clockwise order when
1130  viewed from outside the unit sphere in a right handed coordinate
1131  system. They must also form a convex polygon.
1132 
1133  When edges are specified, the i-th edge must correspond to the plane
1134  equation of great circle connecting vertices (i - 1, i), that is,
1135  the edge should be a unit cartesian 3-vector parallel to v[i-1] ^ v[i]
1136  (where ^ denotes the vector cross product).
1137 
1138  Note that these conditions are NOT verified for performance reasons.
1139  Operations on SphericalConvexPolygon objects constructed with inputs
1140  not satisfying these conditions are undefined. Use the convex()
1141  function to check for convexity and ordering of the vertices. Or,
1142  use the convexHull() function to create a SphericalConvexPolygon
1143  from an arbitrary list of hemispherical input vertices.
1144  """
1145  self.boundingBox = None
1146  self.boundingCircle = None
1147  if len(args) == 0:
1148  raise RuntimeError('Expecting at least one argument')
1149  elif len(args) == 1:
1150  if isinstance(args[0], SphericalConvexPolygon):
1151  self.vertices = tuple(args[0].vertices)
1152  self.edges = tuple(args[0].edges)
1153  else:
1154  self.vertices = tuple(args[0])
1155  self._computeEdges()
1156  elif len(args) == 2:
1157  self.vertices = tuple(args[0])
1158  self.edges = tuple(args[1])
1159  if len(self.edges) != len(self.vertices):
1160  raise RuntimeError(
1161  'number of edges does not match number of vertices')
1162  else:
1163  self.vertices = tuple(args)
1164  self._computeEdges()
1165  if len(self.vertices) < 3:
1166  raise RuntimeError(
1167  'spherical polygon must contain at least 3 vertices')
1168 
1169  def _computeEdges(self):
1170  """Computes edge plane normals from vertices.
1171  """
1172  v = self.vertices
1173  n = len(v)
1174  edges = []
1175  for i in range(n):
1176  edges.append(normalize(cross(v[i - 1], v[i])))
1177  self.edges = tuple(edges)
1178 
1179  def getVertices(self):
1180  """Returns the list of polygon vertices.
1181  """
1182  return self.vertices
1183 
1184  def getEdges(self):
1185  """Returns the list of polygon edges. The i-th edge is the plane
1186  equation for the great circle edge formed by vertices i-1 and i.
1187  """
1188  return self.edges
1189 
1191  """Returns a bounding circle (not necessarily minimal) for this
1192  spherical convex polygon.
1193  """
1194  if self.boundingCircle is None:
1195  center = centroid(self.vertices)
1196  radius = 0.0
1197  for v in self.vertices:
1198  radius = max(radius, cartesianAngularSep(center, v))
1199  self.boundingCircle = SphericalCircle(center, radius)
1200  return self.boundingCircle
1201 
1202  def getBoundingBox(self):
1203  """Returns a bounding box for this spherical convex polygon.
1204  """
1205  if self.boundingBox is None:
1206  self.boundingBox = SphericalBox()
1207  for i in range(0, len(self.vertices)):
1208  self.boundingBox.extend(SphericalBox.edge(
1209  self.vertices[i - 1], self.vertices[i], self.edges[i]))
1210  return self.boundingBox
1211 
1212  def getZRange(self):
1213  """Returns the z coordinate range spanned by this spherical
1214  convex polygon.
1215  """
1216  bbox = self.getBoundingBox()
1217  return (math.sin(math.radians(bbox.getMin()[1])),
1218  math.sin(math.radians(bbox.getMax()[1])))
1219 
1220  def clip(self, plane):
1221  """Returns the polygon corresponding to the intersection of this
1222  polygon with the positive half space defined by the given plane.
1223  The plane must be specified with a cartesian unit vector (its
1224  normal) and always passes through the origin.
1225 
1226  Clipping is performed using the Sutherland-Hodgman algorithm,
1227  adapted for spherical polygons.
1228  """
1229  vertices, edges, classification = [], [], []
1230  vin, vout = False, False
1231  for i in range(len(self.vertices)):
1232  d = dot(plane, self.vertices[i])
1233  if d > SIN_MIN:
1234  vin = True
1235  elif d < -SIN_MIN:
1236  vout = True
1237  else:
1238  d = 0.0
1239  classification.append(d)
1240  if not vin and not vout:
1241  # polygon and clipping plane are coplanar
1242  return None
1243  if not vout:
1244  return self
1245  elif not vin:
1246  return None
1247  v0 = self.vertices[-1]
1248  d0 = classification[-1]
1249  for i in range(len(self.vertices)):
1250  v1 = self.vertices[i]
1251  d1 = classification[i]
1252  if d0 > 0.0:
1253  if d1 >= 0.0:
1254  # positive to positive, positive to zero: no intersection,
1255  # add current input vertex to output polygon
1256  vertices.append(v1)
1257  edges.append(self.edges[i])
1258  else:
1259  # positive to negative: add intersection point to polygon
1260  f = d0 / (d0 - d1)
1261  v = normalize((v0[0] + (v1[0] - v0[0]) * f,
1262  v0[1] + (v1[1] - v0[1]) * f,
1263  v0[2] + (v1[2] - v0[2]) * f))
1264  vertices.append(v)
1265  edges.append(self.edges[i])
1266  elif d0 == 0.0:
1267  if d1 >= 0.0:
1268  # zero to positive: no intersection, add current input
1269  # vertex to output polygon.
1270  vertices.append(v1)
1271  edges.append(self.edges[i])
1272  # zero to zero: coplanar edge - since the polygon has vertices
1273  # on both sides of plane, this implies concavity or a
1274  # duplicate vertex. Under the convexity assumption, this
1275  # must be caused by a near duplicate vertex, so skip the
1276  # vertex.
1277  #
1278  # zero to negative: no intersection, skip the vertex
1279  else:
1280  if d1 > 0.0:
1281  # negative to positive: add intersection point to output
1282  # polygon followed by the current input vertex
1283  f = d0 / (d0 - d1)
1284  v = normalize((v0[0] + (v1[0] - v0[0]) * f,
1285  v0[1] + (v1[1] - v0[1]) * f,
1286  v0[2] + (v1[2] - v0[2]) * f))
1287  vertices.append(v)
1288  edges.append(tuple(plane))
1289  vertices.append(v1)
1290  edges.append(self.edges[i])
1291  elif d1 == 0.0:
1292  # negative to zero: add current input vertex to output
1293  # polygon
1294  vertices.append(v1)
1295  edges.append(tuple(plane))
1296  # negative to negative: no intersection, skip vertex
1297  v0 = v1
1298  d0 = d1
1299  return SphericalConvexPolygon(vertices, edges)
1300 
1301  def intersect(self, poly):
1302  """Returns the intersection of poly with this polygon, or
1303  None if the intersection does not exist.
1304  """
1305  if not isinstance(poly, SphericalConvexPolygon):
1306  raise TypeError('Expecting a SphericalConvexPolygon object')
1307  p = self
1308  for edge in poly.getEdges():
1309  p = p.clip(edge)
1310  if p is None:
1311  break
1312  return p
1313 
1314  def area(self):
1315  """Returns the area of this spherical convex polygon.
1316  """
1317  numVerts = len(self.vertices)
1318  angleSum = 0.0
1319  for i in range(numVerts):
1320  tmp = cross(self.edges[i - 1], self.edges[i])
1321  sina = math.sqrt(dot(tmp, tmp))
1322  cosa = -dot(self.edges[i - 1], self.edges[i])
1323  a = math.atan2(sina, cosa)
1324  angleSum += a
1325  return angleSum - (numVerts - 2) * math.pi
1326 
1327  def containsPoint(self, v):
1328  for e in self.edges:
1329  if dot(v, e) < 0.0:
1330  return False
1331  return True
1332 
1333  def contains(self, pointOrRegion):
1334  """Returns True if the specified point or spherical region is
1335  completely contained in this polygon. Note that the implementation
1336  is conservative where ellipses are concerned: False may be returned
1337  for an ellipse that is actually completely contained by this polygon.
1338  """
1339  pr = pointOrRegion
1340  if isinstance(pr, SphericalConvexPolygon):
1341  for v in pr.getVertices():
1342  if not self.containsPoint(v):
1343  return False
1344  return True
1345  elif isinstance(pr, SphericalEllipse):
1346  return self.contains(pr.getBoundingCircle())
1347  elif isinstance(pr, SphericalCircle):
1348  cv = cartesianUnitVector(pr.getCenter())
1349  if not self.containsPoint(cv):
1350  return False
1351  else:
1352  minSep = INF
1353  for i in range(len(self.vertices)):
1354  s = minEdgeSep(cv, self.edges[i],
1355  self.vertices[i - 1], self.vertices[i])
1356  minSep = min(minSep, s)
1357  return minSep >= pr.getRadius()
1358  elif isinstance(pr, SphericalBox):
1359  # check that all box vertices are inside the polygon
1360  bMin, bMax = pr.getMin(), pr.getMax()
1361  bzMin = math.sin(math.radians(bMin[1]))
1362  bzMax = math.sin(math.radians(bMax[1]))
1363  verts = map(cartesianUnitVector,
1364  (bMin, bMax, (bMin[0], bMax[1]), (bMax[0], bMin[1])))
1365  for v in verts:
1366  if not self.containsPoint(v):
1367  return False
1368  # check that intersections of box small circles with polygon
1369  # edges either don't exist or fall outside the box.
1370  for i in range(len(self.vertices)):
1371  v0 = self.vertices[i - 1]
1372  v1 = self.vertices[i]
1373  e = self.edges[i]
1374  d = e[0] * e[0] + e[1] * e[1]
1375  if abs(e[2]) >= COS_MAX or d < MIN_FLOAT:
1376  # polygon edge is approximately described by z = +/-1.
1377  # box vertices are inside the polygon, so they
1378  # cannot intersect the edge.
1379  continue
1380  D = d - bzMin * bzMin
1381  if D >= 0.0:
1382  # polygon edge intersects z = bzMin
1383  D = math.sqrt(D)
1384  xr = -e[0] * e[2] * bzMin
1385  yr = -e[1] * e[2] * bzMin
1386  i1 = ((xr + e[1] * D) / d, (yr - e[0] * D) / d, bzMin)
1387  i2 = ((xr - e[1] * D) / d, (yr + e[0] * D) / d, bzMin)
1388  if (pr.containsPoint(sphericalCoords(i1)) or
1389  pr.containsPoint(sphericalCoords(i2))):
1390  return False
1391  D = d - bzMax * bzMax
1392  if D >= 0.0:
1393  # polygon edge intersects z = bzMax
1394  D = math.sqrt(D)
1395  xr = -e[0] * e[2] * bzMax
1396  yr = -e[1] * e[2] * bzMax
1397  i1 = ((xr + e[1] * D) / d, (yr - e[0] * D) / d, bzMax)
1398  i2 = ((xr - e[1] * D) / d, (yr + e[0] * D) / d, bzMax)
1399  if (pr.containsPoint(sphericalCoords(i1)) or
1400  pr.containsPoint(sphericalCoords(i2))):
1401  return False
1402  return True
1403  else:
1404  return self.containsPoint(cartesianUnitVector(pr))
1405 
1406  def intersects(self, pointOrRegion):
1407  """Returns True if the given point or spherical region intersects
1408  this polygon. Note that the implementation is conservative where
1409  ellipses are concerned: True may be returned for an ellipse that
1410  is actually disjoint from this polygon.
1411  """
1412  pr = pointOrRegion
1413  if isinstance(pr, SphericalConvexPolygon):
1414  return self.intersect(pr) is not None
1415  elif isinstance(pr, SphericalEllipse):
1416  return self.intersects(pr.getBoundingCircle())
1417  elif isinstance(pr, SphericalCircle):
1418  cv = cartesianUnitVector(pr.getCenter())
1419  if self.containsPoint(cv):
1420  return True
1421  else:
1422  minSep = INF
1423  for i in range(len(self.vertices)):
1424  s = minEdgeSep(cv, self.edges[i],
1425  self.vertices[i - 1], self.vertices[i])
1426  minSep = min(minSep, s)
1427  return minSep <= pr.getRadius()
1428  elif isinstance(pr, SphericalBox):
1429  minTheta = math.radians(pr.getMin()[0])
1430  maxTheta = math.radians(pr.getMax()[0])
1431  bzMin = math.sin(math.radians(pr.getMin()[1]))
1432  bzMax = math.sin(math.radians(pr.getMax()[1]))
1433  p = self.clip((-math.sin(minTheta), math.cos(minTheta), 0.0))
1434  if pr.getThetaExtent() > 180.0:
1435  if p is not None:
1436  zMin, zMax = p.getZRange()
1437  if zMin <= bzMax and zMax >= bzMin:
1438  return True
1439  p = self.clip((math.sin(maxTheta), -math.cos(maxTheta), 0.0))
1440  else:
1441  if p is not None:
1442  p = p.clip((math.sin(maxTheta), -math.cos(maxTheta), 0.0))
1443  if p is None:
1444  return False
1445  zMin, zMax = p.getZRange()
1446  return zMin <= bzMax and zMax >= bzMin
1447  else:
1448  return self.containsPoint(cartesianUnitVector(pr))
1449 
1450  def __repr__(self):
1451  """Returns a string representation of this polygon.
1452  """
1453  return ''.join([self.__class__.__name__, '(',
1454  repr(map(sphericalCoords, self.vertices)), ')'])
1455 
1456  def __eq__(self, other):
1457  if not isinstance(other, SphericalConvexPolygon):
1458  return False
1459  n = len(self.vertices)
1460  if n != len(other.vertices):
1461  return False
1462  # Test for equality up to cyclic permutation of vertices/edges
1463  try:
1464  offset = other.vertices.index(self.vertices[0])
1465  except ValueError:
1466  return False
1467  for i in range(0, n):
1468  j = offset + i
1469  if j >= n:
1470  j -= n
1471  if (self.vertices[i] != self.vertices[j] or
1472  self.edges[i] != self.edges[j]):
1473  return False
1474  return True
1475 
1476  def __hash__(self):
1477  return hash((self.vertices, self.edges))
1478 
1479 # -- Finding the median element of an array in linear time ----
1480 #
1481 # This is a necessary primitive for Megiddo's linear time
1482 # 2d linear programming algorithm.
1483 
1484 def _partition(array, left, right, i):
1485  """Partitions an array around the pivot value at index i,
1486  returning the new index of the pivot value.
1487  """
1488  pivot = array[i]
1489  array[i] = array[right - 1]
1490  j = left
1491  for k in range(left, right - 1):
1492  if array[k] < pivot:
1493  tmp = array[j]
1494  array[j] = array[k]
1495  array[k] = tmp
1496  j += 1
1497  array[right - 1] = array[j]
1498  array[j] = pivot
1499  return j
1500 
1501 
1502 def _medianOfN(array, i, n):
1503  """Finds the median of n consecutive elements in an array starting
1504  at index i (efficient for small n). The index of the median element
1505  is returned.
1506  """
1507  if n == 1:
1508  return i
1509  k = n >> 1
1510  e1 = i + k + 1
1511  e2 = i + n
1512  for j in range(i, e1):
1513  minIndex = j
1514  minValue = array[j]
1515  for s in range(j + 1, e2):
1516  if array[s] < minValue:
1517  minIndex = s
1518  minValue = array[s]
1519  tmp = array[j]
1520  array[j] = array[minIndex]
1521  array[minIndex] = tmp
1522  return i + k
1523 
1524 
1525 def _medianOfMedians(array, left, right):
1526  """Returns the "median of medians" for an array. This primitive is used
1527  for pivot selection in the median finding algorithm.
1528  """
1529  while True:
1530  if right - left <= 5:
1531  return _medianOfN(array, left, right - left)
1532  i = left
1533  j = left
1534  while i + 4 < right:
1535  k = _medianOfN(array, i, 5)
1536  tmp = array[j]
1537  array[j] = array[k]
1538  array[k] = tmp
1539  j += 1
1540  i += 5
1541  right = j
1542 
1543 
1544 def median(array):
1545  """Finds the median element of the given array in linear time.
1546  """
1547  left = 0
1548  right = len(array)
1549  if right == 0:
1550  return None
1551  k = right >> 1
1552  while True:
1553  i = _medianOfMedians(array, left, right)
1554  i = _partition(array, left, right, i)
1555  if k == i:
1556  return array[k]
1557  elif k < i:
1558  right = i
1559  else:
1560  left = i + 1
1561 
1562 
1563 # -- Testing whether a set of points is hemispherical ----
1564 #
1565 # This test is used by the convexity test and convex hull algorithm. It is
1566 # implemented using Megiddo's algorithm for linear programming in R2, see:
1567 #
1568 # Megiddo, N. 1982. Linear-time algorithms for linear programming in R3 and related problems.
1569 # In Proceedings of the 23rd Annual Symposium on Foundations of Computer Science (November 03 - 05, 1982).
1570 # SFCS. IEEE Computer Society, Washington, DC, 329-338. DOI=
1571 # http://dx.doi.org/10.1109/SFCS.1982.74
1572 
1573 def _prune(constraints, xmin, xmax, op):
1574  """Removes redundant constraints and reports intersection points
1575  of non-redundant pairs. The constraint list is modified in place.
1576  """
1577  intersections = []
1578  i = 0
1579  while i < len(constraints) - 1:
1580  a1, b1 = constraints[i]
1581  a2, b2 = constraints[i + 1]
1582  # if constraints are near parallel or intersect to the left or right
1583  # of the feasible x range, remove the higher/lower constraint
1584  da = a1 - a2
1585  if abs(da) < MIN_FLOAT / EPSILON:
1586  xi = INF
1587  else:
1588  xi = (b2 - b1) / da
1589  xierr = 2.0 * EPSILON * abs(xi)
1590  if isinf(xi):
1591  if op(b1, b2):
1592  constraints[i + 1] = constraints[-1]
1593  else:
1594  constraints[i] = constraints[-1]
1595  constraints.pop()
1596  else:
1597  if xi + xierr <= xmin:
1598  if op(a1, a2):
1599  constraints[i + 1] = constraints[-1]
1600  else:
1601  constraints[i] = constraints[-1]
1602  constraints.pop()
1603  elif xi - xierr >= xmax:
1604  if op(a1, a2):
1605  constraints[i] = constraints[-1]
1606  else:
1607  constraints[i + 1] = constraints[-1]
1608  constraints.pop()
1609  else:
1610  # save intersection for later
1611  intersections.append((xi, xierr))
1612  i += 2
1613  return intersections
1614 
1615 
1616 def _vrange(x, xerr, a, b):
1617  p = a * (x - xerr)
1618  v = p + b
1619  verr = EPSILON * abs(p) + EPSILON * abs(v)
1620  vmin = v - verr
1621  vmax = v + verr
1622  p = a * (x + xerr)
1623  v = p + b
1624  verr = EPSILON * abs(p) + EPSILON * abs(v)
1625  vmin = min(vmin, v - verr)
1626  vmax = max(vmax, v + verr)
1627  return vmin, vmax
1628 
1629 
1630 def _gh(constraints, x, xerr, op):
1631  a, b = constraints[0]
1632  amin, amax = a, a
1633  vmin, vmax = _vrange(x, xerr, a, b)
1634  for i in range(1, len(constraints)):
1635  a, b = constraints[i]
1636  vimin, vimax = _vrange(x, xerr, a, b)
1637  if vimax < vmin or vimin > vmax:
1638  if op(vimin, vmin):
1639  amin = a
1640  amax = a
1641  vmin = vimin
1642  vmax = vimax
1643  else:
1644  amin = min(amin, a)
1645  amax = max(amax, a)
1646  return vmin, vmax, amin, amax
1647 
1648 
1649 def _feasible2D(points, z):
1650  I1 = []
1651  I2 = []
1652  xmin = NEG_INF
1653  xmax = INF
1654  # transform each constraint of the form x*v[0] + y*v[1] + z*v[2] > 0
1655  # into y op a*x + b or x op c, where op is < or >
1656  for v in points:
1657  if abs(v[1]) <= MIN_FLOAT:
1658  if abs(v[0]) <= MIN_FLOAT:
1659  if z * v[2] <= 0.0:
1660  # inequalities trivially lack a solution
1661  return False
1662  # current inequality is trivially true, skip it
1663  else:
1664  xlim = - z * v[2] / v[0]
1665  if v[0] > 0.0:
1666  xmin = max(xmin, xlim)
1667  else:
1668  xmax = min(xmax, xlim)
1669  if xmax <= xmin:
1670  # inequalities trivially lack a solution
1671  return False
1672  else:
1673  # finite since |z|, |v[i]| <= 1 and 1/MIN_FLOAT is finite
1674  coeffs = (v[0] / v[1], - z * v[2] / v[1])
1675  if v[1] > 0.0:
1676  I1.append(coeffs)
1677  else:
1678  I2.append(coeffs)
1679  # At this point (xmin, xmax) is non-empty - if either I1 or I2 is empty
1680  # then a solution trivially exists
1681  if len(I1) == 0 or len(I2) == 0:
1682  return True
1683  # Check for a feasible solution to the inequalities I1 of the form
1684  # form y > a*x + b, I2 of the form y < a*x + b, x > xmin and x < xmax
1685  numConstraints = 0
1686  while True:
1687  intersections = _prune(I1, xmin, xmax, operator.gt)
1688  intersections.extend(_prune(I2, xmin, xmax, operator.lt))
1689  if len(intersections) == 0:
1690  # I1 and I2 each contain exactly one constraint
1691  a1, b1 = I1[0]
1692  a2, b2 = I2[0]
1693  if a1 == a2:
1694  xi = INF
1695  else:
1696  xi = (b2 - b1) / (a1 - a2)
1697  if isinf(xi):
1698  return b1 < b2
1699  return (xi > xmin or a1 < a2) and (xi < xmax or a1 > a2)
1700  elif numConstraints == len(I1) + len(I2):
1701  # No constraints were pruned during search interval refinement,
1702  # and g was not conclusively less than h. Conservatively return
1703  # False to avoid an infinite loop.
1704  return False
1705  numConstraints = len(I1) + len(I2)
1706  x, xerr = median(intersections)
1707  # If g(x) < h(x), x is a feasible solution. Otherwise, refine the
1708  # search interval by examining the one-sided derivates of g/h.
1709  gmin, gmax, sg, Sg = _gh(I1, x, xerr, operator.gt)
1710  hmin, hmax, sh, Sh = _gh(I2, x, xerr, operator.lt)
1711  if gmax <= hmin:
1712  return True
1713  elif sg > Sh:
1714  xmax = x + xerr
1715  elif Sg < sh:
1716  xmin = x - xerr
1717  else:
1718  return False
1719 
1720 
1721 def _feasible1D(points, y):
1722  xmin = NEG_INF
1723  xmax = INF
1724  for v in points:
1725  if abs(v[0]) <= MIN_FLOAT:
1726  if y * v[1] <= 0.0:
1727  return False
1728  # inequality is trivially true, skip it
1729  else:
1730  xlim = - y * v[1] / v[0]
1731  if v[0] > 0.0:
1732  xmin = max(xmin, xlim)
1733  else:
1734  xmax = min(xmax, xlim)
1735  if xmax <= xmin:
1736  return False
1737  return True
1738 
1739 
1740 def hemispherical(points):
1741  """Tests whether a set of points is hemispherical, i.e. whether a plane
1742  exists such that all points are strictly on one side of the plane. The
1743  algorithm used is Megiddo's algorithm for linear programming in R2 and
1744  has run-time O(n), where n is the number of points. Points must be passed
1745  in as a list of cartesian unit vectors.
1746  """
1747  # Check whether the set of linear equations
1748  # x * v[0] + y * v[1] + z * v[2] > 0.0 (for v in points)
1749  # has a solution (x, y, z). If (x,y,z) is a solution (is feasible),
1750  # so is C*(x,y,z), C > 0. Therefore we can fix z to 1, -1 and
1751  # perform 2D feasibility tests.
1752  if _feasible2D(points, 1.0):
1753  return True
1754  if _feasible2D(points, -1.0):
1755  return True
1756  # At this point a feasible solution must have z = 0. Fix y to 1, -1 and
1757  # perform 1D feasibility tests.
1758  if _feasible1D(points, 1.0):
1759  return True
1760  if _feasible1D(points, -1.0):
1761  return True
1762  # At this point a feasible solution must have y = z = 0. If all v[0]
1763  # are non-zero and have the same sign, then there is a feasible solution.
1764  havePos = False
1765  haveNeg = False
1766  for v in points:
1767  if v[0] > 0.0:
1768  if haveNeg:
1769  return False
1770  havePos = True
1771  elif v[0] < 0.0:
1772  if havePos:
1773  return False
1774  haveNeg = True
1775  else:
1776  return False
1777  return True
1778 
1779 
1780 # -- Convexity test and convex hull algorithm ----
1781 
1782 def convex(vertices):
1783  """Tests whether an ordered list of vertices (which must be specified
1784  as cartesian unit vectors) form a spherical convex polygon and determines
1785  their winding order. Returns a 2-tuple as follows:
1786 
1787  (True, True): The vertex list forms a spherical convex polygon and is in
1788  counter-clockwise order when viewed from outside the unit
1789  sphere in a right handed coordinate system.
1790  (True, False): The vertex list forms a spherical convex polygon and is in
1791  in clockwise order.
1792  (False, msg): The vertex list does not form a spherical convex polygon -
1793  msg is a string describing why the test failed.
1794 
1795  The algorithm completes in O(n) time, where n is the number of
1796  input vertices.
1797  """
1798  if len(vertices) < 3:
1799  return (False, '3 or more vertices must be specified')
1800  if not hemispherical(vertices):
1801  return (False, 'vertices are not hemispherical')
1802  center = centroid(vertices)
1803  windingAngle = 0.0
1804  clockwise = False
1805  counterClockwise = False
1806  p1 = cross(center, vertices[-1])
1807  n2 = dot(p1, p1)
1808  if abs(n2) < CROSS_N2MIN:
1809  return (False, 'centroid of vertices is too close to a vertex')
1810  for i in range(len(vertices)):
1811  beg = vertices[i - 2]
1812  mid = vertices[i - 1]
1813  end = vertices[i]
1814  plane = cross(mid, end)
1815  n2 = dot(plane, plane)
1816  if dot(mid, end) >= COS_MAX or n2 < CROSS_N2MIN:
1817  return (False, 'vertex list contains [near-]duplicates')
1818  plane = invScale(plane, math.sqrt(n2))
1819  d = dot(plane, beg)
1820  if d > SIN_MIN:
1821  if clockwise:
1822  return (False, 'vertices wind around centroid in both ' +
1823  'clockwise and counter-clockwise order')
1824  counterClockwise = True
1825  elif d < -SIN_MIN:
1826  if counterClockwise:
1827  return (False, 'vertices wind around centroid in both ' +
1828  'clockwise and counter-clockwise order')
1829  clockwise = True
1830  # center must be inside polygon formed by vertices if they form a
1831  # convex polygon - an equivalent check is to test that polygon
1832  # vertices always wind around center in the same direction.
1833  d = dot(plane, center)
1834  if d < SIN_MIN and counterClockwise or d > -SIN_MIN and clockwise:
1835  return (False, 'centroid of vertices is not conclusively ' +
1836  'inside all edges')
1837  # sum up winding angle for edge (mid, end)
1838  p2 = cross(center, end)
1839  n2 = dot(p2, p2)
1840  if abs(n2) < CROSS_N2MIN:
1841  return (False, 'centroid of vertices is too close to a vertex')
1842  windingAngle += cartesianAngularSep(p1, p2)
1843  p1 = p2
1844  # For convex polygons, the closest multiple of 360 to
1845  # windingAngle is 1.
1846  m = math.floor(windingAngle / 360.0)
1847  if m == 0.0 and windingAngle > 180.0 or m == 1.0 and windingAngle < 540.0:
1848  return (True, counterClockwise)
1849  return (False, 'vertices do not completely wind around centroid, or ' +
1850  'wind around it multiple times')
1851 
1852 
1853 def convexHull(points):
1854  """Computes the convex hull (a spherical convex polygon) of an unordered
1855  list of points on the unit sphere, which must be passed in as cartesian
1856  unit vectors. The algorithm takes O(n log n) time, where n is the number
1857  of points.
1858  """
1859  if len(points) < 3:
1860  return None
1861  if not hemispherical(points):
1862  return None
1863  center = centroid(points)
1864  maxSep = 0.0
1865  extremum = 0
1866  # Vertex furthest from the center is on the hull
1867  for i in range(len(points)):
1868  sep = cartesianAngularSep(points[i], center)
1869  if sep > maxSep:
1870  extremum = i
1871  maxSep = sep
1872  anchor = points[extremum]
1873  refPlane = cross(center, anchor)
1874  n2 = dot(refPlane, refPlane)
1875  if n2 < CROSS_N2MIN:
1876  # vertex and center are too close
1877  return None
1878  refPlane = invScale(refPlane, math.sqrt(n2))
1879  # Order points by winding angle from the first (extreme) vertex
1880  av = [(0.0, anchor)]
1881  for i in range(0, len(points)):
1882  if i == extremum:
1883  continue
1884  v = points[i]
1885  plane = cross(center, v)
1886  n2 = dot(plane, plane)
1887  if n2 >= CROSS_N2MIN:
1888  plane = invScale(plane, math.sqrt(n2))
1889  p = cross(refPlane, plane)
1890  sa = math.sqrt(dot(p, p))
1891  if dot(p, center) < 0.0:
1892  sa = -sa
1893  angle = math.atan2(sa, dot(refPlane, plane))
1894  if angle < 0.0:
1895  angle += 2.0 * math.pi
1896  av.append((angle, v))
1897  if len(av) < 3:
1898  return None
1899  av.sort(key=lambda t: t[0]) # stable, so av[0] still contains anchor
1900  # Loop over vertices using a Graham scan adapted for spherical geometry.
1901  # Chan's algorithm would be an improvement, but seems likely to be slower
1902  # for small vertex counts (the expected case).
1903  verts = [anchor]
1904  edges = [None]
1905  edge = None
1906  i = 1
1907  while i < len(av):
1908  v = av[i][1]
1909  p = cross(anchor, v)
1910  n2 = dot(p, p)
1911  if dot(anchor, v) < COS_MAX and n2 >= CROSS_N2MIN:
1912  if edge is None:
1913  # Compute first edge
1914  edge = invScale(p, math.sqrt(n2))
1915  verts.append(v)
1916  edges.append(edge)
1917  anchor = v
1918  else:
1919  d = dot(v, edge)
1920  if d > SIN_MIN:
1921  # v is inside the edge defined by the last
1922  # 2 vertices on the hull
1923  edge = invScale(p, math.sqrt(n2))
1924  verts.append(v)
1925  edges.append(edge)
1926  anchor = v
1927  elif d < -SIN_MIN:
1928  # backtrack - the most recently added hull vertex
1929  # is not actually on the hull.
1930  verts.pop()
1931  edges.pop()
1932  anchor = verts[-1]
1933  edge = edges[-1]
1934  # reprocess v to decide whether to add it to the hull
1935  # or whether further backtracking is necessary
1936  continue
1937  # v is coplanar with edge, skip it
1938  i += 1
1939  # Handle backtracking necessary for last edge
1940  if len(verts) < 3:
1941  return None
1942  v = verts[0]
1943  while True:
1944  p = cross(anchor, v)
1945  n2 = dot(p, p)
1946  if dot(anchor, v) < COS_MAX and n2 >= CROSS_N2MIN:
1947  if dot(v, edge) > SIN_MIN:
1948  edges[0] = invScale(p, math.sqrt(n2))
1949  break
1950  verts.pop()
1951  edges.pop()
1952  anchor = verts[-1]
1953  edge = edges[-1]
1954  if len(verts) < 3:
1955  return None
1956  return SphericalConvexPolygon(verts, edges)
1957 
1958 
1959 # -- Generic unit sphere partitioning scheme base class ----
1960 
1961 # TODO: make this an ABC once python 2.6 becomes the default
1963  """Base class for partitioning schemes.
1964  """
1965  pass
1966 
1967 
1968 # -- Utilities for the spherical box partitioning scheme ----
1969 
1970 class _SubList(object):
1971  """Class that maintains a sub-list of a backing list in
1972  insertion order. Elements can be deleted in O(1) time.
1973  """
1974 
1975  def __init__(self, backingList):
1976  self.backingList = backingList
1977  self.active = []
1978  self.head = -1
1979  self.tail = -1
1980  self.freeTail = -1
1981  self.len = 0
1982 
1983  def append(self, i):
1984  if self.freeTail == -1:
1985  j = len(self.active)
1986  self.active.append([i, self.tail, -1])
1987  else:
1988  j = self.freeTail
1989  self.freeTail = self.active[self.freeTail][2]
1990  self.active[j] = [i, self.tail, -1]
1991  if self.tail != -1:
1992  self.active[self.tail][2] = j
1993  if self.head == -1:
1994  self.head = j
1995  self.tail = j
1996  self.len += 1
1997 
1998  def filter(self, predicate):
1999  """Removes all elements E in the sub-list for which predicate(E)
2000  evaluates to True.
2001  """
2002  h = self.head
2003  while h != -1:
2004  t = self.active[h]
2005  if predicate(self.backingList[t[0]]):
2006  # Remove element h
2007  self.backingList[t[0]] = None
2008  self.len -= 1
2009  prev = t[1]
2010  next = t[2]
2011  if next != -1:
2012  self.active[next][1] = prev
2013  else:
2014  self.tail = prev
2015  if prev != -1:
2016  self.active[prev][2] = next
2017  else:
2018  self.head = next
2019  t[2] = self.freeTail
2020  self.freeTail = h
2021  h = next
2022  else:
2023  h = t[2]
2024 
2025  def __len__(self):
2026  return self.len
2027 
2028  def __iter__(self):
2029  """Returns an iterator over all elements in the sub-list.
2030  """
2031  h = self.head
2032  while h != -1:
2033  t = self.active[h]
2034  yield self.backingList[t[0]]
2035  h = t[2]
2036 
2037 
2038 # -- Spherical box partitioning scheme ----
2039 
2041  """A simple partitioning scheme that breaks the unit sphere into fixed
2042  height latitude angle stripes. These are in turn broken up into fixed
2043  width longitude angle chunks (each stripe has a variable number of chunks
2044  to account for distortion at the poles). Chunks are in turn broken up
2045  into fixed height sub-stripes, and each sub-stripe is then divided into
2046  fixed width sub-chunks. Again, the number of sub-chunks per sub-stripe is
2047  variable to account for polar distortion.
2048  """
2049 
2050  def __init__(self, numStripes, numSubStripesPerStripe):
2051  if (not isinstance(numStripes, numbers.Integral) or
2052  not isinstance(numSubStripesPerStripe, numbers.Integral)):
2053  raise TypeError('Number of stripes and sub-stripes per stripe ' +
2054  'must be integers')
2055  if numStripes < 1 or numSubStripesPerStripe < 1:
2056  raise RuntimeError('Number of stripes and sub-stripes per ' +
2057  'stripe must be positive')
2058  self.numStripes = numStripes
2059  self.numSSPerStripe = numSubStripesPerStripe
2060  self.numSubStripes = numStripes * numSubStripesPerStripe
2061  h = 180.0 / numStripes
2062  hs = 180.0 / self.numSubStripes
2063  self.stripeHeight = h
2065  self.numChunks = [segments(i * h - 90.0, (i + 1) * h - 90.0, h)
2066  for i in range(numStripes)]
2067  self.numSCPerChunk = []
2068  self.subChunkWidth = []
2069  for i in range(self.numSubStripes):
2070  nc = self.numChunks[i//numSubStripesPerStripe]
2071  n = segments(i * hs - 90.0, (i + 1) * hs - 90.0, hs)//nc
2072  self.numSCPerChunk.append(n)
2073  w = 360.0 / (n * nc)
2074  self.subChunkWidth.append(w)
2076 
2077  def getSubStripe(self, phi):
2078  """Returns the sub-stripe number of the sub-stripe containing points
2079  with the given latitude angle.
2080  """
2081  assert phi >= -90.0 and phi <= 90.0
2082  ss = int(math.floor((phi + 90.0) / self.subStripeHeight))
2083  if ss >= self.numSubStripes:
2084  ss = self.numSubStripes - 1
2085  return ss
2086 
2087  def getStripe(self, phi):
2088  """Returns the stripe number of the stripe containing all points
2089  with the given latitude angle.
2090  """
2091  ss = self.getSubStripe(phi)
2092  return ss//self.numSSPerStripe
2093 
2094  def getSubChunk(self, subStripe, theta):
2095  """Returns the sub-chunk number of the sub-chunk containing all points
2096  in the given sub-stripe with the given longitude angle.
2097  """
2098  assert subStripe >= 0 and theta >= 0.0 and theta <= 360.0
2099  sc = int(math.floor(theta / self.subChunkWidth[subStripe]))
2100  nsc = (self.numSCPerChunk[subStripe] *
2101  self.numChunks[subStripe//self.numSSPerStripe])
2102  if sc >= nsc:
2103  sc = nsc - 1
2104  return sc
2105 
2106  def getChunk(self, stripe, theta):
2107  """Returns the chunk number of the chunk containing all points
2108  in the given stripe with the given longitude angle.
2109  """
2110  assert stripe >= 0 and theta >= 0.0 and theta <= 360.0
2111  ss = stripe * self.numSSPerStripe
2112  sc = self.getSubChunk(ss, theta)
2113  return sc//self.numSCPerChunk[ss]
2114 
2115  def _getChunkBoundingBox(self, stripe, chunk):
2116  assert stripe >= 0 and stripe < self.numStripes
2117  assert chunk >= 0 and chunk < self.numChunks[stripe]
2118  ss = stripe * self.numSSPerStripe
2119  minPhi = ss * self.subStripeHeight - 90.0
2120  maxPhi = (ss + self.numSSPerStripe) * self.subStripeHeight - 90.0
2121  # Why all the epsilons? Because chunk bounding boxes are defined
2122  # as the union of all sub-chunk bounding boxes: minTheta and maxTheta
2123  # are not guaranteed to be identical across all the sub-stripes in
2124  # the chunk. Furthermore, the computation of the boundaries
2125  # themselves is inexact - points very slightly outside of them
2126  # could still be assigned to the corresponding chunk.
2127  if maxPhi >= 90.0 - ANGLE_EPSILON:
2128  maxPhi = 90.0
2129  else:
2130  maxPhi += ANGLE_EPSILON
2131  if minPhi > -90.0 + ANGLE_EPSILON:
2132  minPhi -= ANGLE_EPSILON
2133  nscpc = self.numSCPerChunk[ss]
2134  sc = chunk * nscpc
2135  scw = self.subChunkWidth[ss]
2136  minTheta = max(0.0, sc * scw - ANGLE_EPSILON)
2137  maxTheta = min((sc + nscpc) * scw + ANGLE_EPSILON, 360.0)
2138  # avoid range reduction in SphericalBox constructor
2139  box = SphericalBox()
2140  box.min = (minTheta, minPhi)
2141  box.max = (maxTheta, maxPhi)
2142  return box
2143 
2144  def getChunkBoundingBox(self, chunkId):
2145  """Returns a SphericalBox bounding the given chunk. Note that
2146  this is a bounding box only - not an exact representation! In
2147  particular, for a point p and a chunk C with bounding box B,
2148  B.contains(p) == True does not imply that p is assigned to C.
2149  However, for all points p assigned to C, B.contains(p) is True.
2150  """
2151  s = chunkId / (self.numStripes * 2)
2152  c = chunkId - s * self.numStripes * 2
2153  return self._getChunkBoundingBox(s, c)
2154 
2155  def _getSubChunkBoundingBox(self, subStripe, subChunk):
2156  assert subStripe >= 0
2157  nsc = (self.numSCPerChunk[subStripe] *
2158  self.numChunks[subStripe//self.numSSPerStripe])
2159  assert subChunk >= 0 and subChunk < nsc
2160  scw = self.subChunkWidth[subStripe]
2161  minPhi = subStripe * self.subStripeHeight - 90.0
2162  maxPhi = (subStripe + 1) * self.subStripeHeight - 90.0
2163  # Why all the epsilons? Because the computation of the boundaries
2164  # themselves is inexact - points very slightly outside of them
2165  # could still be assigned to the corresponding chunk.
2166  if maxPhi >= 90.0 - ANGLE_EPSILON:
2167  maxPhi = 90.0
2168  else:
2169  maxPhi += ANGLE_EPSILON
2170  if minPhi > -90.0 + ANGLE_EPSILON:
2171  minPhi -= ANGLE_EPSILON
2172  minTheta = max(0.0, subChunk * scw - ANGLE_EPSILON)
2173  maxTheta = min((subChunk + 1) * scw + ANGLE_EPSILON, 360.0)
2174  # avoid range reduction in SphericalBox constructor
2175  box = SphericalBox()
2176  box.min = (minTheta, minPhi)
2177  box.max = (maxTheta, maxPhi)
2178  return box
2179 
2180  def getSubChunkBoundingBox(self, chunkId, subChunkId):
2181  """Returns a SphericalBox bounding the given sub-chunk. Note that
2182  this is a bounding box only - not an exact representation! In
2183  particular, for a point p and a sub-chunk SC with bounding box B,
2184  B.contains(p) == True does not imply that p is assigned to SC.
2185  However, for all points p assigned to SC, B.contains(p) is True.
2186  """
2187  s = chunkId//(self.numStripes * 2)
2188  c = chunkId - s * self.numStripes * 2
2189  ssc = subChunkId//self.maxSCPerChunk
2190  scc = subChunkId - ssc * self.maxSCPerChunk
2191  ss = s * self.numSSPerStripe + ssc
2192  sc = c * self.numSCPerChunk[ss] + scc
2193  return self._getSubChunkBoundingBox(ss, sc)
2194 
2195  def getChunkId(self, stripe, chunk):
2196  """Returns the chunk ID of the chunk with the given
2197  stripe/chunk numbers.
2198  """
2199  return stripe * self.numStripes * 2 + chunk
2200 
2201  def getSubChunkId(self, subStripe, subChunk):
2202  """Returns the sub-chunk ID of the sub-chunk with the given
2203  sub-stripe/sub-chunk numbers.
2204  """
2205  ss = (subStripe % self.numSSPerStripe) * self.maxSCPerChunk
2206  sc = (subChunk % self.numSCPerChunk[subStripe])
2207  return ss + sc
2208 
2209  def _allSubChunks(self, stripe, withEmptySet):
2210  assert stripe >= 0 and stripe < self.numStripes
2211  emptySet = set()
2212  for i in range(self.numSSPerStripe):
2213  nsc = self.numSCPerChunk[stripe * self.numSSPerStripe + i]
2214  scId = i * self.maxSCPerChunk
2215  if withEmptySet:
2216  for j in range(nsc):
2217  yield (scId + j, emptySet)
2218  else:
2219  for j in range(nsc):
2220  yield scId + j
2221 
2222  def _processChunk(self, minS, minC, c, cOverlap):
2223  ss = minS * self.numSSPerStripe
2224  while minC < c and len(cOverlap) > 0:
2225  bbox = self._getChunkBoundingBox(minS, minC)
2226  if any(br[1].contains(bbox) for br in cOverlap):
2227  # if a constraint contains the whole chunk,
2228  # yield a fast sub-chunk iterator
2229  yield (self.getChunkId(minS, minC),
2230  self._allSubChunks(minS, True))
2231  else:
2232  yield (self.getChunkId(minS, minC),
2233  self._subIntersect(minS, minC, list(cOverlap)))
2234  # Finished processing minC - advance and remove any regions
2235  # no longer intersecting the current chunk
2236  minC += 1
2237  theta = (minC * self.numSCPerChunk[ss]) * self.subChunkWidth[ss]
2238  if theta >= 360.0 - ANGLE_EPSILON:
2239  theta = 360.0 + ANGLE_EPSILON
2240  else:
2241  theta -= ANGLE_EPSILON
2242  cOverlap.filter(lambda br: br[0].getMax()[0] < theta)
2243 
2244  def _processStripe(self, minS, s, sOverlap):
2245  while minS < s and len(sOverlap) > 0:
2246  # Sort stripe regions by minimum bounding box longitude angle
2247  sRegions = list(sOverlap)
2248  sRegions.sort(key=lambda x: x[0].getMin()[0])
2249  minC = self.getChunk(minS, max(0.0,
2250  sRegions[0][0].getMin()[0] - ANGLE_EPSILON))
2251  cOverlap = _SubList(sRegions)
2252  cOverlap.append(0)
2253  for j in range(1, len(sRegions)):
2254  c = self.getChunk(minS, max(0.0,
2255  sRegions[j][0].getMin()[0] - ANGLE_EPSILON))
2256  if c == minC:
2257  cOverlap.append(j)
2258  continue
2259  # All regions overlapping minC have been accumulated
2260  for x in self._processChunk(minS, minC, c, cOverlap):
2261  yield x
2262  minC = c
2263  cOverlap.append(j)
2264  maxC = self.numChunks[minS]
2265  for x in self._processChunk(minS, minC, maxC, cOverlap):
2266  yield x
2267  # Finished processing minS - remove any regions not
2268  # intersecting stripes greater than minS and advance
2269  minS += 1
2270  phi = (minS * self.numSSPerStripe) * self.subStripeHeight - 90.0
2271  if phi >= 90.0 - ANGLE_EPSILON:
2272  phi = 90.0 + ANGLE_EPSILON
2273  else:
2274  phi -= ANGLE_EPSILON
2275  sOverlap.filter(lambda br: br[0].getMax()[1] < phi)
2276 
2277  def intersect(self, *args):
2278  """Computes the intersection of a spherical box partitioning of the
2279  unit sphere and one or more SphericalRegions. Results are
2280  returned as an iterator over (chunkId, SubIterator) tuples. These
2281  correspond to all chunks overlapping at least one input region,
2282  and contain sub-iterators over all sub-chunks intersecting at least
2283  one input region. The sub-iterators return (subChunkId, Regions)
2284  tuples, where Regions is a set of the regions that intersect with
2285  (but do not completely contain) a particular sub-chunk. Note that
2286  Regions can be an empty set - this means that the sub-chunk
2287  is completely contained in at least one of the input regions.
2288  """
2289  if len(args) == 0:
2290  return
2291  elif len(args) == 1 and not isinstance(args[0], SphericalRegion):
2292  # accept arbitrary sequences of SphericalRegion objects
2293  args = args[0]
2294  if not all(isinstance(r, SphericalRegion) for r in args):
2295  raise TypeError(
2296  'Input must consist of one or more SphericalRegion objects')
2297  # Construct list of (bounding box, region) tuples
2298  regions = []
2299  for r in args:
2300  b = r.getBoundingBox()
2301  if b.wraps():
2302  # Split boxes that wrap
2303  bMin = (0.0, b.getMin()[1])
2304  bMax = (360.0, b.getMax()[1])
2305  regions.append((SphericalBox(bMin, b.getMax()), r))
2306  # Cannot use SphericalBox constructor: 360.0 would get
2307  # range reduced to 0!
2308  b2 = SphericalBox()
2309  b2.min = b.getMin()
2310  b2.max = bMax
2311  regions.append((b2, r))
2312  else:
2313  regions.append((b, r))
2314  # Sort regions by minimum bounding box latitude angle
2315  regions.sort(key=lambda x: x[0].getMin()[1])
2316  minS = self.getStripe(
2317  max(-90.0, regions[0][0].getMin()[1] - ANGLE_EPSILON))
2318  sOverlap = _SubList(regions)
2319  sOverlap.append(0)
2320  # Loop over regions
2321  for i in range(1, len(regions)):
2322  s = self.getStripe(
2323  max(-90.0, regions[i][0].getMin()[1] - ANGLE_EPSILON))
2324  if s == minS:
2325  sOverlap.append(i)
2326  continue
2327  # All regions overlapping minS have been accumulated
2328  for x in self._processStripe(minS, s, sOverlap):
2329  yield x
2330  minS = s
2331  sOverlap.append(i)
2332  for x in self._processStripe(minS, self.numStripes, sOverlap):
2333  yield x
2334 
2335  def _processSubChunk(self, minSS, minSC, sc, scOverlap):
2336  while minSC < sc and len(scOverlap) > 0:
2337  partial = None
2338  bbox = self._getSubChunkBoundingBox(minSS, minSC)
2339  for br in scOverlap:
2340  if br[1].contains(bbox):
2341  partial = set()
2342  break
2343  elif br[1].intersects(bbox):
2344  if partial is None:
2345  partial = set()
2346  partial.add(br[1])
2347  if partial is not None:
2348  yield (self.getSubChunkId(minSS, minSC), partial)
2349  # Finished processing minSC - remove any regions not
2350  # intersecting sub-chunks > minSC and advance
2351  minSC += 1
2352  theta = minSC * self.subChunkWidth[minSS]
2353  if theta >= 360.0 - ANGLE_EPSILON:
2354  theta = 360.0 + ANGLE_EPSILON
2355  else:
2356  theta -= ANGLE_EPSILON
2357  scOverlap.filter(lambda br: br[0].getMax()[0] < theta)
2358 
2359  def _processSubStripe(self, minSS, ss, chunk, ssOverlap):
2360  while minSS < ss and len(ssOverlap) > 0:
2361  firstSC = chunk * self.numSCPerChunk[minSS]
2362  ssRegions = list(ssOverlap)
2363  # Sort sub-stripe regions by minimum bounding box longitude
2364  ssRegions.sort(key=lambda x: x[0].getMin()[0])
2365  minSC = max(firstSC, self.getSubChunk(minSS, max(0.0,
2366  ssRegions[0][0].getMin()[0] - ANGLE_EPSILON)))
2367  scOverlap = _SubList(ssRegions)
2368  scOverlap.append(0)
2369  for j in range(1, len(ssRegions)):
2370  sc = max(firstSC, self.getSubChunk(minSS, max(0.0,
2371  ssRegions[j][0].getMin()[0] - ANGLE_EPSILON)))
2372  if sc == minSC:
2373  scOverlap.append(j)
2374  continue
2375  # All regions overlapping minSC have been accumulated
2376  for x in self._processSubChunk(minSS, minSC, sc, scOverlap):
2377  yield x
2378  minSC = sc
2379  scOverlap.append(j)
2380  maxSC = firstSC + self.numSCPerChunk[minSS]
2381  for x in self._processSubChunk(minSS, minSC, maxSC, scOverlap):
2382  yield x
2383  # Finished processing minSS - remove any regions not
2384  # intersecting stripes greater than minSS and advance
2385  minSS += 1
2386  phi = minSS * self.subStripeHeight - 90.0
2387  if phi >= 90.0 - ANGLE_EPSILON:
2388  phi = 90.0 + ANGLE_EPSILON
2389  else:
2390  phi -= ANGLE_EPSILON
2391  ssOverlap.filter(lambda br: br[0].getMax()[1] < phi)
2392 
2393  def _subIntersect(self, stripe, chunk, regions):
2394  """Returns an iterator over (subChunkId, Regions) tuples, where
2395  Regions is a set of all regions that intersect with (but do not
2396  completely contain) a particular sub-chunk.
2397  """
2398  # Sort regions by minimum bounding box latitude angle
2399  regions.sort(key=lambda x: x[0].getMin()[1])
2400  firstSS = stripe * self.numSSPerStripe
2401  minSS = max(firstSS, self.getSubStripe(max(-90.0,
2402  regions[0][0].getMin()[1] - ANGLE_EPSILON)))
2403  ssOverlap = _SubList(regions)
2404  ssOverlap.append(0)
2405  # Loop over regions
2406  for i in range(1, len(regions)):
2407  ss = max(firstSS, self.getSubStripe(max(-90.0,
2408  regions[i][0].getMin()[1] - ANGLE_EPSILON)))
2409  if ss == minSS:
2410  ssOverlap.append(i)
2411  continue
2412  for x in self._processSubStripe(minSS, ss, chunk, ssOverlap):
2413  yield x
2414  minSS = ss
2415  ssOverlap.append(i)
2416  maxSS = firstSS + self.numSSPerStripe
2417  for x in self._processSubStripe(minSS, maxSS, chunk, ssOverlap):
2418  yield x
2419 
2420  def __iter__(self):
2421  """Returns an iterator over (chunkId, SubIterator) tuples - one for
2422  each chunk in the partition map. Each SubIterator is an iterator over
2423  subChunkIds for the corresponding chunk.
2424  """
2425  for s in range(self.numStripes):
2426  for c in range(self.numChunks[s]):
2427  yield (self.getChunkId(s, c), self._allSubChunks(s, False))
2428 
2429  def __eq__(self, other):
2430  if isinstance(other, SphericalBoxPartitionMap):
2431  return (self.numStripes == other.numStripes and
2432  self.numSSPerStripe == other.numSSPerStripe)
2433  return False
2434 
2435  def __repr__(self):
2436  return ''.join([self.__class__.__name__, '(', repr(self.numStripes),
2437  ', ', repr(self.numSSPerStripe), ')'])
def filter(self, predicate)
Definition: geometry.py:1998
def _subIntersect(self, stripe, chunk, regions)
Definition: geometry.py:2393
def normalize(v)
Definition: geometry.py:99
def dot(v1, v2)
Definition: geometry.py:79
def _processSubChunk(self, minSS, minSC, sc, scOverlap)
Definition: geometry.py:2335
Angle abs(Angle const &a)
Definition: Angle.h:106
bool all(CoordinateExpr< N > const &expr)
Return true if all elements are true.
def hemispherical(points)
Definition: geometry.py:1740
bool contains(VertexIterator const begin, VertexIterator const end, UnitVector3d const &v)
def _processSubStripe(self, minSS, ss, chunk, ssOverlap)
Definition: geometry.py:2359
def _getChunkBoundingBox(self, stripe, chunk)
Definition: geometry.py:2115
def maxAlpha(r, centerPhi)
Definition: geometry.py:240
def intersects(self, pointOrRegion)
Definition: geometry.py:906
def _processStripe(self, minS, s, sOverlap)
Definition: geometry.py:2244
def contains(self, pointOrRegion)
Definition: geometry.py:503
def getSubChunkBoundingBox(self, chunkId, subChunkId)
Definition: geometry.py:2180
def invScale(v, s)
Definition: geometry.py:93
def minThetaEdgeSep(p, theta, minPhi, maxPhi)
Definition: geometry.py:301
int min
Definition: Coord.cc:82
def __init__(self, center, radius)
Definition: geometry.py:799
def __init__(self, center, semiMajorAxisLength, semiMinorAxisLength, majorAxisAngle)
Definition: geometry.py:970
def minPhiEdgeSep(p, phi, minTheta, maxTheta)
Definition: geometry.py:285
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
def centroid(vertices)
Definition: geometry.py:316
daf::base::PropertySet * set
Definition: fits.cc:818
def contains(self, pointOrRegion)
Definition: geometry.py:1333
def cross(v1, v2)
Definition: geometry.py:85
def _allSubChunks(self, stripe, withEmptySet)
Definition: geometry.py:2209
def median(array)
Definition: geometry.py:1544
bool any(CoordinateExpr< N > const &expr)
Return true if any elements are true.
def sphericalCoords(args)
Definition: geometry.py:108
def getSubChunkId(self, subStripe, subChunk)
Definition: geometry.py:2201
def extend(self, pointOrRegion)
Definition: geometry.py:559
def __init__(self, numStripes, numSubStripesPerStripe)
Definition: geometry.py:2050
def intersects(self, pointOrRegion)
Definition: geometry.py:1077
def contains(self, pointOrRegion)
Definition: geometry.py:1065
int max
Definition: BoundedField.cc:99
def contains(self, pointOrRegion)
Definition: geometry.py:858
def getChunkId(self, stripe, chunk)
Definition: geometry.py:2195
def between(p, n, v1, v2)
Definition: geometry.py:329
def intersects(self, pointOrRegion)
Definition: geometry.py:1406
def clampPhi(phi)
Definition: geometry.py:184
def convexHull(points)
Definition: geometry.py:1853
def minEdgeSep(p, n, v1, v2)
Definition: geometry.py:272
def alpha(r, centerPhi, phi)
Definition: geometry.py:222
def _getSubChunkBoundingBox(self, subStripe, subChunk)
Definition: geometry.py:2155
def intersects(self, pointOrRegion)
Definition: geometry.py:530
def _processChunk(self, minS, minC, c, cOverlap)
Definition: geometry.py:2222
def reduceTheta(theta)
Definition: geometry.py:194
def sphericalAngularSep(p1, p2)
Definition: geometry.py:169
def convex(vertices)
Definition: geometry.py:1782
def __init__(self, backingList)
Definition: geometry.py:1975
def cartesianUnitVector(args)
Definition: geometry.py:141
def segments(phiMin, phiMax, width)
Definition: geometry.py:339
daf::base::PropertyList * list
Definition: fits.cc:819
def cartesianAngularSep(v1, v2)
Definition: geometry.py:260
def getSubChunk(self, subStripe, theta)
Definition: geometry.py:2094