Coverage for python/lsst/sims/utils/htmModule.py : 10%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2This module implements utilities to convert between RA, Dec and indexes
3on the Hierarchical Triangular Mesh (HTM), a system of tiling the unit sphere
4with nested triangles. The HTM is described in these references
6Kunszt P., Szalay A., Thakar A. (2006) in "Mining The Sky",
7Banday A, Zaroubi S, Bartelmann M. eds.
8ESO Astrophysics Symposia
9https://www.researchgate.net/publication/226072008_The_Hierarchical_Triangular_Mesh
11Szalay A. et al. (2007)
12"Indexing the Sphere with the Hierarchical Triangular Mesh"
13arXiv:cs/0701164
14"""
16import numpy as np
17import numbers
18from lsst.sims.utils import cartesianFromSpherical, sphericalFromCartesian
20__all__ = ["Trixel", "HalfSpace", "findHtmid", "trixelFromHtmid",
21 "basic_trixels", "halfSpaceFromRaDec", "levelFromHtmid",
22 "getAllTrixels", "halfSpaceFromPoints",
23 "intersectHalfSpaces"]
26class Trixel(object):
27 """
28 A trixel is a single triangle in the Hierarchical Triangular Mesh (HTM)
29 tiling scheme. It is defined by its three corners on the unit sphere.
31 Instantiating this class directly is a bad idea. __init__() does nothing
32 to ensure that the parameters you give it are self-consistent. Instead,
33 use the trixelFromHtmid() or getAllTrixels() methods in this module
34 to instantiate trixels.
35 """
37 def __init__(self, present_htmid, present_corners):
38 """
39 Initialize the current Trixel
41 Parameters
42 ----------
43 present_htmid is the htmid of this Trixel
45 present_corners is a numpy array. Each row
46 contains the Cartesian coordinates of one of
47 this Trixel's corners.
49 WARNING
50 -------
51 No effort is made to ensure that the parameters
52 passed in are self consistent. You should probably
53 not being calling __init__() directly. Use the
54 trixelFromHtmid() or getAllTrixels() methods to
55 instantiate trixels.
56 """
57 self._corners = present_corners
58 self._htmid = present_htmid
59 self._level = (len('{0:b}'.format(self._htmid))/2)-1
60 self._cross01 = None
61 self._cross12 = None
62 self._cross20 = None
63 self._w_arr = None
64 self._bounding_circle = None
66 def __eq__(self, other):
68 tol = 1.0e-20
70 if self._htmid == other._htmid:
71 if self._level == other._level:
72 if np.allclose(self._corners, other._corners, atol=tol):
73 return True
75 return False
77 def __ne__(self, other):
78 return not (self == other)
80 @property
81 def htmid(self):
82 """
83 The unique integer identifying this trixel.
84 """
85 return self._htmid
87 def contains(self, ra, dec):
88 """
89 Returns True if the specified RA, Dec are
90 inside this trixel; False if not.
92 RA and Dec are in degrees.
93 """
94 xyz = cartesianFromSpherical(np.radians(ra), np.radians(dec))
95 return self.contains_pt(xyz)
97 @property
98 def cross01(self):
99 """
100 The cross product of the unit vectors defining
101 the zeroth and first corners of this trixel.
102 """
103 if self._cross01 is None:
104 self._cross01 = np.cross(self._corners[0], self._corners[1])
105 return self._cross01
107 @property
108 def cross12(self):
109 """
110 The cross product of the unit vectors defining
111 the first and second corners of this trixel.
112 """
113 if self._cross12 is None:
114 self._cross12 = np.cross(self._corners[1], self._corners[2])
115 return self._cross12
117 @property
118 def cross20(self):
119 """
120 The cross product of the unit vectors defining the second
121 and zeroth corners of this trixel.
122 """
123 if self._cross20 is None:
124 self._cross20 = np.cross(self._corners[2], self._corners[0])
125 return self._cross20
127 def _contains_one_pt(self, pt):
128 """
129 pt is a Cartesian point (not necessarily on the unit sphere).
131 Returns True if the point projected onto the unit sphere
132 is contained within this trixel; False if not.
134 See equation 5 of
136 Kunszt P., Szalay A., Thakar A. (2006) in "Mining The Sky",
137 Banday A, Zaroubi S, Bartelmann M. eds.
138 ESO Astrophysics Symposia
139 https://www.researchgate.net/publication/226072008_The_Hierarchical_Triangular_Mesh
140 """
141 if np.dot(self.cross01, pt) >= 0.0:
142 if np.dot(self.cross12, pt) >= 0.0:
143 if np.dot(self.cross20, pt) >= 0.0:
144 return True
146 return False
148 def _contains_many_pts(self, pts):
149 """
150 pts is an array of Cartesian points (pts[0] is the zeroth
151 point, pts[1] is the first point, etc.; not necessarily on
152 the unit sphere).
154 Returns an array of booleans denoting whether or not the
155 projection of each point onto the unit sphere is contained
156 within this trixel.
158 See equation 5 of
160 Kunszt P., Szalay A., Thakar A. (2006) in "Mining The Sky",
161 Banday A, Zaroubi S, Bartelmann M. eds.
162 ESO Astrophysics Symposia
163 https://www.researchgate.net/publication/226072008_The_Hierarchical_Triangular_Mesh
164 """
165 return ((np.dot(pts, self.cross01)>=0.0) &
166 (np.dot(pts, self.cross12)>=0.0) &
167 (np.dot(pts, self.cross20)>=0.0))
169 def contains_pt(self, pt):
170 """
171 pt is either a single Cartesian point
172 or an array of Cartesian points (pt[0]
173 is the zeroth point, pt[1] is the first
174 point, etc.).
176 Return a boolean or array of booleans
177 denoting whether this point(s) projected
178 onto the unit sphere is/are contained within
179 the current trixel.
180 """
181 if len(pt.shape) == 1:
182 return self._contains_one_pt(pt)
183 return self._contains_many_pts(pt)
185 def _create_w(self):
187 w0 = self._corners[1]+self._corners[2]
188 w0 = w0/np.sqrt(np.power(w0, 2).sum())
189 w1 = self._corners[0]+self._corners[2]
190 w1 = w1/np.sqrt(np.power(w1, 2).sum())
191 w2 = self._corners[0]+self._corners[1]
192 w2 = w2/np.sqrt(np.power(w2, 2).sum())
194 self._w_arr = [w0, w1, w2]
196 @property
197 def w_arr(self):
198 """
199 An array of vectors needed to define the child trixels
200 of this trixel. See equation (3) of
202 Kunszt P., Szalay A., Thakar A. (2006) in "Mining The Sky",
203 Banday A, Zaroubi S, Bartelmann M. eds.
204 ESO Astrophysics Symposia
205 httpd://www.researchgate.net/publication/226072008_The_Hierarchical_Triangular_Mesh
206 """
207 if self._w_arr is None:
208 self._create_w()
209 return self._w_arr
211 @property
212 def t0(self):
213 """
214 The zeroth child trixel of this trixel.
216 See Figure 2 of
218 Szalay A. et al. (2007)
219 "Indexing the Sphere with the Hierarchical Triangular Mesh"
220 arXiv:cs/0701164
221 """
222 if not hasattr(self, '_t0'):
223 self._t0 = Trixel(self.htmid << 2,
224 [self._corners[0], self.w_arr[2], self.w_arr[1]])
225 return self._t0
227 @property
228 def t1(self):
229 """
230 The first child trixel of this trixel.
232 See Figure 2 of
234 Szalay A. et al. (2007)
235 "Indexing the Sphere with the Hierarchical Triangular Mesh"
236 arXiv:cs/0701164
237 """
238 if not hasattr(self, '_t1'):
239 self._t1 = Trixel((self.htmid << 2)+1,
240 [self._corners[1], self.w_arr[0], self.w_arr[2]])
241 return self._t1
243 @property
244 def t2(self):
245 """
246 The second child trixel of this trixel.
248 See Figure 2 of
250 Szalay A. et al. (2007)
251 "Indexing the Sphere with the Hierarchical Triangular Mesh"
252 arXiv:cs/0701164
253 """
254 if not hasattr(self, '_t2'):
255 self._t2 = Trixel((self.htmid << 2)+2,
256 [self._corners[2], self.w_arr[1], self.w_arr[0]])
257 return self._t2
259 @property
260 def t3(self):
261 """
262 The third child trixel of this trixel.
264 See Figure 2 of
266 Szalay A. et al. (2007)
267 "Indexing the Sphere with the Hierarchical Triangular Mesh"
268 arXiv:cs/0701164
269 """
270 if not hasattr(self, '_t3'):
271 self._t3 = Trixel((self.htmid << 2)+3,
272 [self.w_arr[0], self.w_arr[1], self.w_arr[2]])
273 return self._t3
275 def get_children(self):
276 """
277 Return a list of all of the child trixels of this trixel.
278 """
279 return [self.t0, self.t1, self.t2, self.t3]
281 def get_child(self, dex):
282 """
283 Return a specific child trixel of this trixel.
285 dex is an integer in the range [0,3] denoting
286 which child to return
288 See Figure 1 of
290 Kunszt P., Szalay A., Thakar A. (2006) in "Mining The Sky",
291 Banday A, Zaroubi S, Bartelmann M. eds.
292 ESO Astrophysics Symposia
293 https://www.researchgate.net/publication/226072008_The_Hierarchical_Triangular_Mesh
295 for an explanation of which trixel corresponds to whic
296 index.
297 """
298 if dex == 0:
299 return self.t0
300 elif dex == 1:
301 return self.t1
302 elif dex == 2:
303 return self.t2
304 elif dex == 3:
305 return self.t3
306 else:
307 raise RuntimeError("Trixel has no %d child" % dex)
309 def get_center(self):
310 """
311 Return the RA, Dec of the center of the circle bounding
312 this trixel (RA, Dec both in degrees)
313 """
314 ra, dec = sphericalFromCartesian(self.bounding_circle[0])
315 return np.degrees(ra), np.degrees(dec)
317 def get_radius(self):
318 """
319 Return the angular radius in degrees of the circle bounding
320 this trixel.
321 """
322 return np.degrees(self.bounding_circle[2])
324 @property
325 def level(self):
326 """
327 Return the level of subdivision for this trixel. A higher
328 level means a finer subdivision of the unit sphere and smaller
329 trixels. What we refer to as 'level' is denoted by 'd' in
330 equation 2.5 of
332 Szalay A. et al. (2007)
333 "Indexing the Sphere with the Hierarchical Triangular Mesh"
334 arXiv:cs/0701164
336 For a given level == ell, there are 8*4**(ell-1) trixels in
337 the entire unit sphere.
339 The htmid values of trixels with level==ell will consist
340 of 4 + 2*(ell-1) bits
341 """
342 return self._level
344 @property
345 def corners(self):
346 """
347 A numpy array containing the unit vectors pointing to the
348 corners of this trixel. corners[0] is the zeroth corner,
349 corners[1] is the first corner, etc.
350 """
351 return self._corners
353 @property
354 def bounding_circle(self):
355 """
356 The circle on the unit sphere that bounds this trixel.
358 See equation 4.2 of
360 Szalay A. et al. (2007)
361 "Indexing the Sphere with the Hierarchical Triangular Mesh"
362 arXiv:cs/0701164
364 Returns
365 -------
366 A tuple:
367 Zeroth element is the unit vector pointing at
368 the center of the bounding circle
370 First element is the distance from the center of
371 the unit sphere to the plane of the bounding circle
372 (i.e. the dot product of the zeroth element with the
373 most distant corner of the trixel).
375 Second element is the half angular extent of the bounding circle.
376 """
377 if self._bounding_circle is None:
378 # find the unit vector pointing to the center of the trixel
379 vb = np.cross((self._corners[1]-self._corners[0]), (self._corners[2]-self._corners[1]))
380 vb = vb/np.sqrt(np.power(vb, 2).sum())
382 # find the distance from the center of the trixel
383 # to the most distant corner of the trixel
384 dd = np.dot(self.corners, vb).max()
386 if np.abs(dd) > 1.0:
387 raise RuntimeError("Bounding circle has dd %e (should be between -1 and 1)" % dd)
389 self._bounding_circle = (vb, dd, np.arccos(dd))
391 return self._bounding_circle
394# Below are defined the initial Trixels
395#
396# See equations (1) and (2) of
397#
398# Kunszt P., Szalay A., Thakar A. (2006) in "Mining The Sky",
399# Banday A, Zaroubi S, Bartelmann M. eds.
400# ESO Astrophysics Symposia
401# https://www.researchgate.net/publication/226072008_The_Hierarchical_Triangular_Mesh
403_N0_trixel = Trixel(12, [np.array([1.0, 0.0, 0.0]),
404 np.array([0.0, 0.0, 1.0]),
405 np.array([0.0, -1.0, 0.0])])
407_N1_trixel = Trixel(13, [np.array([0.0, -1.0, 0.0]),
408 np.array([0.0, 0.0, 1.0]),
409 np.array([-1.0, 0.0, 0.0])])
411_N2_trixel = Trixel(14, [np.array([-1.0, 0.0, 0.0]),
412 np.array([0.0, 0.0, 1.0]),
413 np.array([0.0, 1.0, 0.0])])
415_N3_trixel = Trixel(15, [np.array([0.0, 1.0, 0.0]),
416 np.array([0.0, 0.0, 1.0]),
417 np.array([1.0, 0.0, 0.0])])
419_S0_trixel = Trixel(8, [np.array([1.0, 0.0, 0.0]),
420 np.array([0.0, 0.0, -1.0]),
421 np.array([0.0, 1.0, 0.0])])
423_S1_trixel = Trixel(9, [np.array([0.0, 1.0, 0.0]),
424 np.array([0.0, 0.0, -1.0]),
425 np.array([-1.0, 0.0, 0.0])])
427_S2_trixel = Trixel(10, [np.array([-1.0, 0.0, 0.0]),
428 np.array([0.0, 0.0, -1.0]),
429 np.array([0.0, -1.0, 0.0])])
431_S3_trixel = Trixel(11, [np.array([0.0, -1.0, 0.0]),
432 np.array([0.0, 0.0, -1.0]),
433 np.array([1.0, 0.0, 0.0])])
435basic_trixels = {'N0': _N0_trixel,
436 'N1': _N1_trixel,
437 'N2': _N2_trixel,
438 'N3': _N3_trixel,
439 'S0': _S0_trixel,
440 'S1': _S1_trixel,
441 'S2': _S2_trixel,
442 'S3': _S3_trixel}
445def levelFromHtmid(htmid):
446 """
447 Find the level of a trixel from its htmid. The level
448 indicates how refined the triangular mesh is.
450 There are 8*4**(d-1) triangles in a mesh of level=d
452 (equation 2.5 of
453 Szalay A. et al. (2007)
454 "Indexing the Sphere with the Hierarchical Triangular Mesh"
455 arXiv:cs/0701164)
457 Note: valid htmids have 4+2n bits with a leading bit of 1
458 """
459 htmid_copy = htmid
460 i_level = 1
461 while htmid_copy > 15:
462 htmid_copy >>= 2
463 i_level += 1
465 if htmid_copy < 8:
466 raise RuntimeError('\n%d is not a valid htmid.\n' % htmid
467 + 'Valid htmids will have 4+2n bits\n'
468 + 'with a leading bit of 1\n')
470 return i_level
473def trixelFromHtmid(htmid):
474 """
475 Return the trixel corresponding to the given htmid
476 (htmid is the unique integer identifying each trixel).
478 Note: this method is not efficient for finding many
479 trixels. It recursively generates trixels and their
480 children until it finds the right htmid without
481 remembering which trixels it has already generated.
482 To generate many trixels, use the getAllTrixels()
483 method, which efficiently generates all of the trixels
484 up to a given mesh level.
486 Note: valid htmids have 4+2n bits with a leading bit of 1
487 """
488 level = levelFromHtmid(htmid)
489 base_htmid = htmid >> 2*(level-1)
491 ans = None
493 if base_htmid == 8:
494 ans = _S0_trixel
495 elif base_htmid == 9:
496 ans = _S1_trixel
497 elif base_htmid == 10:
498 ans = _S2_trixel
499 elif base_htmid == 11:
500 ans = _S3_trixel
501 elif base_htmid == 12:
502 ans = _N0_trixel
503 elif base_htmid == 13:
504 ans = _N1_trixel
505 elif base_htmid == 14:
506 ans = _N2_trixel
507 elif base_htmid == 15:
508 ans = _N3_trixel
510 if ans is None:
511 raise RuntimeError("Unable to find trixel for id %d" % htmid)
513 if level == 1:
514 return ans
516 # create an integer that is 4 bits
517 # shorter than htmid (so it excludes
518 # the bits corresponding to the base
519 # trixel),with 11 in the two leading
520 # positions
521 complement = 3
522 complement <<= 2*(level-2)
524 for ix in range(level-1):
525 # Use bitwise and to figure out what the
526 # two bits to the right of the bits
527 # corresponding to the trixel currently
528 # stored in ans are. These two bits
529 # determine which child of ans we need
530 # to return.
531 target = htmid & complement
532 target >>= 2*(level-ix-2)
534 if target >= 4:
535 raise RuntimeError("target %d" % target)
537 ans = ans.get_child(target)
538 complement >>= 2
540 return ans
543def getAllTrixels(level):
544 """
545 Return a dict of all of the trixels up to a given mesh level.
546 The dict is keyed on htmid, unique integer identifying
547 each trixel on the unit sphere. This method is efficient
548 at generating many trixels at once.
549 """
551 # find how many bits should be added to the htmids
552 # of the base trixels to get up to the desired level
553 n_bits_added = 2*(level-1)
555 # first, put the base trixels into the dict
556 start_trixels = range(8, 16)
557 trixel_dict = {}
558 for t0 in start_trixels:
559 trix0 = trixelFromHtmid(t0)
560 trixel_dict[t0] = trix0
562 ct = 0
563 for t0 in start_trixels:
564 t0 = t0 << n_bits_added
565 for dt in range(2**n_bits_added):
566 htmid = t0 + dt # htmid we are currently generating
567 ct += 1
568 if htmid in trixel_dict:
569 continue
571 parent_id = htmid >> 2 # the immediate parent of htmid
573 while parent_id not in trixel_dict:
575 # find the highest-level ancestor trixel
576 # of htmid that has already
577 # been generated and added to trixel_dict
578 for n_right in range(2, n_bits_added, 2):
579 if htmid >> n_right in trixel_dict:
580 break
582 # generate the next highest level ancestor
583 # of the current htmid
584 to_gen = htmid >> n_right
585 if to_gen in trixel_dict:
586 trix0 = trixel_dict[to_gen]
587 else:
588 trix0 = trixelFromHtmid(to_gen)
589 trixel_dict[to_gen] = trix0
591 # add the children of to_gen to trixel_dict
592 trixel_dict[to_gen << 2] = trix0.get_child(0)
593 trixel_dict[(to_gen << 2)+1] = trix0.get_child(1)
594 trixel_dict[(to_gen << 2)+2] = trix0.get_child(2)
595 trixel_dict[(to_gen << 2)+3] = trix0.get_child(3)
597 # add all of the children of parent_id to
598 # trixel_dict
599 trix0 = trixel_dict[parent_id]
600 trixel_dict[(parent_id << 2)] = trix0.get_child(0)
601 trixel_dict[(parent_id << 2)+1] = trix0.get_child(1)
602 trixel_dict[(parent_id << 2)+2] = trix0.get_child(2)
603 trixel_dict[(parent_id << 2)+3] = trix0.get_child(3)
605 return trixel_dict
608def _iterateTrixelFinder(pt, parent, max_level):
609 """
610 Method to iteratively find the htmid of the trixel containing
611 a point.
613 Parameters
614 ----------
615 pt is a Cartesian point (not necessarily on the unit sphere)
617 parent is the largest trixel currently known to contain the point
619 max_level is the level of the triangular mesh at which we want
620 to find the htmid. Higher levels correspond to finer meshes.
621 A mesh with level == ell contains 8*4**(ell-1) trixels.
623 Returns
624 -------
625 The htmid at the desired level of the trixel containing the unit sphere
626 projection of the point in pt.
627 """
628 children = parent.get_children()
629 for child in children:
630 if child.contains_pt(pt):
631 if child.level == max_level:
632 return child.htmid
633 else:
634 return _iterateTrixelFinder(pt, child, max_level)
637def _findHtmid_slow(ra, dec, max_level):
638 """
639 Find the htmid (the unique integer identifying
640 each trixel) of the trixel containing a given
641 RA, Dec pair.
643 Parameters
644 ----------
645 ra in degrees
647 dec in degrees
649 max_level is an integer denoting the mesh level
650 of the trixel you want found
652 Note: This method only works one point at a time.
653 It cannot take arrays of RA and Dec.
655 Returns
656 -------
657 An int (the htmid)
658 """
660 ra_rad = np.radians(ra)
661 dec_rad = np.radians(dec)
662 pt = cartesianFromSpherical(ra_rad, dec_rad)
664 if _S0_trixel.contains_pt(pt):
665 parent = _S0_trixel
666 elif _S1_trixel.contains_pt(pt):
667 parent = _S1_trixel
668 elif _S2_trixel.contains_pt(pt):
669 parent = _S2_trixel
670 elif _S3_trixel.contains_pt(pt):
671 parent = _S3_trixel
672 elif _N0_trixel.contains_pt(pt):
673 parent = _N0_trixel
674 elif _N1_trixel.contains_pt(pt):
675 parent = _N1_trixel
676 elif _N2_trixel.contains_pt(pt):
677 parent = _N2_trixel
678 elif _N3_trixel.contains_pt(pt):
679 parent = _N3_trixel
680 else:
681 raise RuntimeError("could not find parent Trixel")
683 return _iterateTrixelFinder(pt, parent, max_level)
686def _findHtmid_fast(ra, dec, max_level):
687 """
688 Find the htmid (the unique integer identifying
689 each trixel) of the trixels containing arrays
690 of RA, Dec pairs
692 Parameters
693 ----------
694 ra in degrees (a numpy array)
696 dec in degrees (a numpy array)
698 max_level is an integer denoting the mesh level
699 of the trixel you want found
701 Returns
702 -------
703 A numpy array of ints (the htmids)
705 Note: this method works by caching all of the trixels up to
706 a given level. Do not call it on max_level>10
707 """
709 if max_level>10:
710 raise RuntimeError("Do not call _findHtmid_fast with max_level>10; "
711 "the cache of trixels generated will be too large. "
712 "Call findHtmid or _findHtmid_slow (findHtmid will "
713 "redirect to _findHtmid_slow for large max_level).")
715 if (not hasattr(_findHtmid_fast, '_trixel_dict') or
716 _findHtmid_fast._level < max_level):
718 _findHtmid_fast._trixel_dict = getAllTrixels(max_level)
719 _findHtmid_fast._level = max_level
721 ra_rad = np.radians(ra)
722 dec_rad = np.radians(dec)
723 pt_arr = cartesianFromSpherical(ra_rad, dec_rad)
725 base_trixels = [_S0_trixel,
726 _S1_trixel,
727 _S2_trixel,
728 _S3_trixel,
729 _N0_trixel,
730 _N1_trixel,
731 _N2_trixel,
732 _N3_trixel]
734 htmid_arr = np.zeros(len(pt_arr), dtype=int)
736 parent_dict = {}
737 for parent in base_trixels:
738 is_contained = parent.contains_pt(pt_arr)
739 valid_dexes = np.where(is_contained)
740 if len(valid_dexes[0]) == 0:
741 continue
742 htmid_arr[valid_dexes] = parent.htmid
743 parent_dict[parent.htmid] = valid_dexes[0]
745 for level in range(1, max_level):
746 new_parent_dict = {}
747 for parent_htmid in parent_dict.keys():
748 considered_raw = parent_dict[parent_htmid]
750 next_htmid = parent_htmid << 2
751 children_htmid = [next_htmid, next_htmid+1,
752 next_htmid+2, next_htmid+3]
754 is_found = np.zeros(len(considered_raw), dtype=int)
755 for child in children_htmid:
756 un_found = np.where(is_found==0)[0]
757 considered = considered_raw[un_found]
758 if len(considered) == 0:
759 break
760 child_trixel = _findHtmid_fast._trixel_dict[child]
761 contains = child_trixel.contains_pt(pt_arr[considered])
762 valid = np.where(contains)
763 if len(valid[0]) == 0:
764 continue
766 valid_dexes = considered[valid]
767 is_found[un_found[valid[0]]] = 1
768 htmid_arr[valid_dexes] = child
769 new_parent_dict[child] = valid_dexes
770 parent_dict = new_parent_dict
772 return htmid_arr
774def findHtmid(ra, dec, max_level):
775 """
776 Find the htmid (the unique integer identifying
777 each trixel) of the trixel containing a given
778 RA, Dec pair.
780 Parameters
781 ----------
782 ra in degrees (either a number or a numpy array)
784 dec in degrees (either a number or a numpy array)
786 max_level is an integer denoting the mesh level
787 of the trixel you want found
789 Returns
790 -------
791 An int (the htmid) or an array of ints
792 """
793 if isinstance(ra, numbers.Number):
794 are_arrays = False
795 elif isinstance(ra, list):
796 ra = np.array(ra)
797 dec = np.array(dec)
798 are_arrays = True
799 else:
800 try:
801 assert isinstance(ra, np.ndarray)
802 assert isinstance(dec, np.ndarray)
803 except AssertionError:
804 raise RuntimeError("\nfindHtmid can handle types\n"
805 + "RA: %s" % type(ra)
806 + "Dec: %s" % type(dec)
807 + "\n")
808 are_arrays = True
810 if are_arrays:
811 if max_level <= 10 and len(ra)>100:
812 return _findHtmid_fast(ra, dec, max_level)
813 else:
814 htmid_arr = np.zeros(len(ra), dtype=int)
815 for ii in range(len(ra)):
816 htmid_arr[ii] = _findHtmid_slow(ra[ii], dec[ii], max_level)
817 return htmid_arr
819 return _findHtmid_slow(ra, dec, max_level)
821class HalfSpace(object):
822 """
823 HalfSpaces are circles on the unit sphere defined by intersecting
824 a plane with the unit sphere. They are specified by the unit vector
825 pointing to their center on the unit sphere and the distance from
826 the center of the unit sphere to the plane along that unit vector.
828 See Section 3.1 of
830 Szalay A. et al. (2007)
831 "Indexing the Sphere with the Hierarchical Triangular Mesh"
832 arXiv:cs/0701164
834 Note that the specifying distance can be negative. In this case,
835 the halfspace is defined as the larger of the two regions on the
836 unit sphere divided by the circle where the plane of the halfspace
837 intersects the unit sphere.
838 """
840 def __init__(self, vector, length):
841 """
842 Parameters
843 ----------
844 vector is the unit vector pointing to the center of
845 the halfspace on the unit sphere
847 length is the distance from the center of the unit
848 sphere to the plane defining the half space along the
849 vector. This length can be negative, in which case,
850 the halfspace is defined as the larger of the two
851 regions on the unit sphere divided by the circle
852 where the plane of the halfspace intersects the
853 unit sphere.
854 """
855 self._v = vector/np.sqrt(np.power(vector, 2).sum())
856 self._d = length
857 if np.abs(self._d) < 1.0:
858 self._phi = np.arccos(self._d) # half angular extent of the half space
859 if self._phi > np.pi:
860 raise RuntimeError("phi %e d %e" % (self._phi, self._d))
861 else:
862 if self._d < 0.0:
863 self._phi = np.pi
864 else:
865 self._phi = 0.0
867 def __eq__(self, other):
868 tol = 1.0e-10
869 if np.abs(self.dd-other.dd) > tol:
870 return False
871 if np.abs(np.dot(self.vector, other.vector)-1.0) > tol:
872 return False
873 return True
875 def __ne__(self, other):
876 return not (self == other)
878 @property
879 def vector(self):
880 """
881 The unit vector from the origin to the center of the Half Space.
882 """
883 return self._v
885 @property
886 def dd(self):
887 """
888 The distance along the Half Space's vector that defines the
889 extent of the Half Space.
890 """
891 return self._d
893 @property
894 def phi(self):
895 """
896 The angular radius of the Half Space on the surface of the sphere
897 in radians.
898 """
899 return self._phi
901 def contains_pt(self, pt, tol=None):
902 """
903 pt is a cartesian point (not necessarily on
904 the unit sphere). The method returns True if
905 the projection of that point onto the unit sphere
906 is contained in the halfspace; False otherwise.
907 """
908 norm_pt = pt/np.sqrt(np.power(pt, 2).sum())
910 dot_product = np.dot(norm_pt, self._v)
912 if tol is None:
913 if dot_product > self._d:
914 return True
915 else:
916 if dot_product > (self._d-tol):
917 return True
919 return False
921 def contains_many_pts(self, pts):
922 """
923 Parameters
924 ----------
925 pts is a numpy array in which each row is a point on the
926 unit sphere (note: must be normalized)
928 Returns
929 -------
930 numpy array of booleans indicating which of pts are contained
931 by this HalfSpace
932 """
933 dot_product = np.dot(pts, self._v)
934 return (dot_product>self._d)
936 def intersects_edge(self, pt1, pt2):
937 """
938 pt1 and pt2 are two unit vectors; the edge goes from pt1 to pt2.
939 Return True if the edge intersects this halfspace; False otherwise.
941 see equation 4.8 of
943 Szalay A. et al. (2007)
944 "Indexing the Sphere with the Hierarchical Triangular Mesh"
945 arXiv:cs/0701164
946 """
947 costheta = np.dot(pt1, pt2)
948 usq = (1-costheta)/(1+costheta) # u**2; using trig identity for tan(theta/2)
949 gamma1 = np.dot(self._v, pt1)
950 gamma2 = np.dot(self._v, pt2)
951 b = gamma1*(usq-1.0) + gamma2*(usq+1)
952 a = -usq*(gamma1+self._d)
953 c = gamma1 - self._d
955 det = b*b - 4*a*c
956 if det < 0.0:
957 return False
959 sqrt_det = np.sqrt(det)
960 pos = (-b + sqrt_det)/(2.0*a)
962 if pos >= 0.0 and pos <= 1.0:
963 return True
965 neg = (-b - sqrt_det)/(2.0*a)
966 if neg >= 0.0 and neg <= 1.0:
967 return True
969 return False
971 def intersects_circle(self, center, radius_rad):
972 """
973 Does this Half Space intersect a circle on the unit sphere
975 center is the unit vector pointing to the center of the circle
977 radius_rad is the radius of the circle in radians
979 Returns a boolean
980 """
982 dotproduct = np.dot(center, self._v)
983 if np.abs(dotproduct) < 1.0:
984 theta = np.arccos(dotproduct)
985 elif (dotproduct < 1.000000001 and dotproduct>0.0):
986 theta = 0.0
987 elif (dotproduct > -1.000000001 and dotproduct<0.0):
988 theta = np.pi
989 else:
990 raise RuntimeError("Dot product between unit vectors is %e" % dotproduct)
992 if theta > self._phi + radius_rad:
993 return False
995 return True
997 def intersects_bounding_circle(self, tx):
998 """
999 tx is a Trixel. Return True if this halfspace intersects
1000 the bounding circle of the trixel; False otherwise.
1002 See the discussion around equation 4.2 of
1004 Szalay A. et al. (2007)
1005 "Indexing the Sphere with the Hierarchical Triangular Mesh"
1006 arXiv:cs/0701164
1007 """
1008 return self.intersects_circle(tx.bounding_circle[0],
1009 tx.bounding_circle[1])
1011 def contains_trixel(self, tx):
1012 """
1013 tx is a Trixel.
1015 Return "full" if the Trixel is fully contained by
1016 this halfspace.
1018 Return "partial" if the Trixel is only partially
1019 contained by this halfspace
1021 Return "outside" if no part of the Trixel is
1022 contained by this halfspace.
1024 See section 4.1 of
1026 Szalay A. et al. (2007)
1027 "Indexing the Sphere with the Hierarchical Triangular Mesh"
1028 arXiv:cs/0701164
1029 """
1031 containment = self.contains_many_pts(tx.corners)
1033 if containment.all():
1034 return "full"
1035 elif containment.any():
1036 return "partial"
1038 if tx.contains_pt(self._v):
1039 return "partial"
1041 # check if the trixel's bounding circle intersects
1042 # the halfspace
1043 if not self.intersects_bounding_circle(tx):
1044 return "outside"
1046 # need to test that the bounding circle intersect the halfspace
1047 # boundary
1049 for edge in ((tx.corners[0], tx.corners[1]),
1050 (tx.corners[1], tx.corners[2]),
1051 (tx.corners[2], tx.corners[0])):
1053 if self.intersects_edge(edge[0], edge[1]):
1054 return "partial"
1056 return "outside"
1058 @staticmethod
1059 def merge_trixel_bounds(bounds):
1060 """
1061 Take a list of trixel bounds as returned by HalfSpace.findAllTrixels
1062 and merge any tuples that should be merged
1064 Parameters
1065 ----------
1066 bounds is a list of trixel bounds as returned by HalfSpace.findAllTrixels
1068 Returns
1069 -------
1070 A new, equivalent list of trixel bounds
1071 """
1072 list_of_mins = np.array([r[0] for r in bounds])
1073 sorted_dex = np.argsort(list_of_mins)
1074 bounds_sorted = np.array(bounds)[sorted_dex]
1075 final_output = []
1076 current_list = []
1077 current_max = -1
1078 for row in bounds_sorted:
1079 if len(current_list) == 0 or row[0] <= current_max+1:
1080 current_list.append(row[0])
1081 current_list.append(row[1])
1082 if row[1]>current_max:
1083 current_max = row[1]
1084 else:
1085 final_output.append((min(current_list), max(current_list)))
1086 current_list = [row[0], row[1]]
1087 current_max = row[1]
1089 if len(current_list) >0:
1090 final_output.append((min(current_list), max(current_list)))
1091 return final_output
1093 @staticmethod
1094 def join_trixel_bound_sets(b1, b2):
1095 """
1096 Take two sets of trixel bounds as returned by HalfSpace.findAllTrixels
1097 and return a set of trixel bounds that represents the intersection of
1098 the original sets of bounds
1099 """
1100 b1_sorted = HalfSpace.merge_trixel_bounds(b1)
1101 b2_sorted = HalfSpace.merge_trixel_bounds(b2)
1103 # maximum/minimum trixel bounds outside of which trixel ranges
1104 # will be considered invalid
1105 global_t_min = max(b1_sorted[0][0], b2_sorted[0][0])
1106 global_t_max = min(b1_sorted[-1][1], b2_sorted[-1][1])
1108 b1_keep = [r for r in b1_sorted if r[0]<=global_t_max and r[1]>=global_t_min]
1109 b2_keep = [r for r in b2_sorted if r[0]<=global_t_max and r[1]>=global_t_min]
1111 dex1 = 0
1112 dex2 = 0
1113 n_b1 = len(b1_keep)
1114 n_b2 = len(b2_keep)
1115 joint_bounds = []
1117 if n_b1==0 or n_b2==0:
1118 return joint_bounds
1120 while True:
1121 r1 = b1_keep[dex1]
1122 r2 = b2_keep[dex2]
1123 if r1[0]<=r2[0] and r1[1]>=r2[1]:
1124 # r2 is completely inside r1;
1125 # keep r2 and advance dex2
1126 joint_bounds.append(r2)
1127 dex2 += 1
1128 elif r2[0]<=r1[0] and r2[1]>=r1[1]:
1129 # r1 is completely inside r2;
1130 # keep r1 and advance dex1
1131 joint_bounds.append(r1)
1132 dex1 += 1
1133 else:
1134 # The two bounds are either disjoint, or they overlap;
1135 # find the intersection
1136 local_min = max(r1[0], r2[0])
1137 local_max = min(r1[1], r2[1])
1138 if local_min<=local_max:
1139 # if we have a valid range, keep it
1140 joint_bounds.append((local_min, local_max))
1142 # advance the bound that is lowest
1143 if r1[1] < r2[1]:
1144 dex1 += 1
1145 else:
1146 dex2 += 1
1148 # if we have finished scanning one or another of the
1149 # bounds, leave the loop
1150 if dex1 >= n_b1 or dex2 >= n_b2:
1151 break
1153 return HalfSpace.merge_trixel_bounds(joint_bounds)
1156 def findAllTrixels(self, level):
1157 """
1158 Find the HTMIDs of all of the trixels filling the half space
1160 Parameters
1161 ----------
1162 level is an integer denoting the resolution of the trixel grid
1164 Returns
1165 -------
1166 A list of tuples. Each tuple gives an inclusive range of HTMIDs
1167 corresponding to trixels within the HalfSpace
1168 """
1170 global basic_trixels
1172 active_trixels = []
1173 for trixel_name in basic_trixels:
1174 active_trixels.append(basic_trixels[trixel_name])
1176 output_prelim = []
1177 max_d_htmid = 0
1179 # Once we establish that a given trixel is completely
1180 # contained within a the HalfSpace, we will need to
1181 # convert that trixel into a (min_htmid, max_htmid) pair.
1182 # This will involve evolving up from the current level
1183 # of trixel resolution to the desired level of trixel
1184 # resolution, setting the resulting 2-bit pairs to 0 for
1185 # min_htmid and 3 for max_htmid. We can get min_htmid by
1186 # taking the base trixel's level and multiplying by an
1187 # appropriate power of 2. We can get max_htmid by adding
1188 # an integer that, in binary, is wholly comprised of 1s
1189 # to min_htmid. Here we construct that integer of 1s,
1190 # starting out at level-2, since the first trixels
1191 # to which we will need to add max_d_htmid will be
1192 # at least at level 2 (the children of the base trixels).
1193 for ii in range(level-2):
1194 max_d_htmid += 3
1195 max_d_htmid <<= 2
1197 # start iterating at level 2 because level 1 is the base trixels,
1198 # where we are already starting, and i_level reallly refers to
1199 # the level of the child trixels we are investigating
1200 for i_level in range(2, level):
1201 max_d_htmid >>= 2
1203 new_active_trixels = []
1204 for tt in active_trixels:
1205 children = tt.get_children()
1206 for child in children:
1207 is_contained = self.contains_trixel(child)
1208 if is_contained == 'partial':
1209 # need to investigate more fully
1210 new_active_trixels.append(child)
1211 elif is_contained == 'full':
1212 # all of this trixels children, and their children are contained
1213 min_htmid = child._htmid << 2*(level-i_level)
1214 max_htmid = min_htmid
1215 max_htmid += max_d_htmid
1216 output_prelim.append((min_htmid, max_htmid))
1218 ########################################
1219 # some assertions for debugging purposes
1220 # assert min_htmid<max_htmid
1221 # try:
1222 # test_trix = trixelFromHtmid(min_htmid)
1223 # assert self.contains_trixel(test_trix) != 'outside'
1224 # test_trix = trixelFromHtmid(max_htmid)
1225 # assert self.contains_trixel(test_trix) != 'outside'
1226 # except AssertionError:
1227 # print('is_contained %s' % is_contained)
1228 # print('level %d' % levelFromHtmid(tt._htmid))
1229 # raise
1231 active_trixels = new_active_trixels
1232 if len(active_trixels) == 0:
1233 break
1235 # final pass over the active_trixels to see which of their
1236 # children are inside this HalfSpace
1237 for trix in active_trixels:
1238 for child in trix.get_children():
1239 if self.contains_trixel(child) != 'outside':
1240 output_prelim.append((child._htmid, child._htmid))
1242 # sort output by htmid_min
1243 min_dex_arr = np.argsort([oo[0] for oo in output_prelim])
1244 output = []
1245 for ii in min_dex_arr:
1246 output.append(output_prelim[ii])
1248 return self.merge_trixel_bounds(output)
1251def halfSpaceFromRaDec(ra, dec, radius):
1252 """
1253 Take an RA, Dec and radius of a circular field of view and return
1254 a HalfSpace
1256 Parameters
1257 ----------
1258 ra in degrees
1260 dec in degrees
1262 radius in degrees
1264 Returns
1265 -------
1266 HalfSpace corresponding to the circular field of view
1267 """
1268 dd = np.cos(np.radians(radius))
1269 xyz = cartesianFromSpherical(np.radians(ra), np.radians(dec))
1270 return HalfSpace(xyz, dd)
1273def halfSpaceFromPoints(pt1, pt2, pt3):
1274 """
1275 Return a Half Space defined by two points on a Great Circle
1276 and a third point contained in the Half Space.
1278 Parameters
1279 ----------
1280 pt1, pt2 -- two tuples containing (RA, Dec) in degrees of
1281 the points on the Great Circle defining the Half Space
1283 pt3 -- a tuple containing (RA, Dec) in degrees of a point
1284 contained in the Half Space
1286 Returns
1287 --------
1288 A Half Space
1289 """
1291 vv1 = cartesianFromSpherical(np.radians(pt1[0]), np.radians(pt1[1]))
1292 vv2 = cartesianFromSpherical(np.radians(pt2[0]), np.radians(pt2[1]))
1293 axis = np.array([vv1[1]*vv2[2]-vv1[2]*vv2[1],
1294 vv1[2]*vv2[0]-vv1[0]*vv2[2],
1295 vv1[0]*vv2[1]-vv1[1]*vv2[0]])
1297 axis /= np.sqrt(np.sum(axis**2))
1299 vv3 = cartesianFromSpherical(np.radians(pt3[0]), np.radians(pt3[1]))
1300 if np.dot(axis, vv3)<0.0:
1301 axis *= -1.0
1303 return HalfSpace(axis, 0.0)
1305def intersectHalfSpaces(hs1, hs2):
1306 """
1307 Parameters
1308 ----------
1309 hs1, hs2 are Half Spaces
1311 Returns
1312 -------
1313 A list of the cartesian points where the Half Spaces intersect.
1315 Note: if the Half Spaces are identical, then this list will be
1316 empty.
1318 Based on section 3.5 of
1319 Szalay A. et al. (2007)
1320 "Indexing the Sphere with the Hierarchical Triangular Mesh"
1321 arXiv:cs/0701164
1322 """
1323 gamma = np.dot(hs1.vector, hs2.vector)
1324 if np.abs(1.0-np.abs(gamma))<1.0e-20:
1325 # Half Spaces are based on parallel planes that don't intersect
1326 return []
1328 denom = 1.0-gamma**2
1329 if denom<0.0:
1330 return []
1332 num = hs1.dd**2+hs2.dd**2-2.0*gamma*hs1.dd*hs2.dd
1333 if denom < num:
1334 return []
1336 uu = (hs1.dd-gamma*hs2.dd)/denom
1337 vv = (hs2.dd-gamma*hs1.dd)/denom
1339 ww = np.sqrt((1.0-num/denom)/denom)
1340 cross_product = np.array([hs1.vector[1]*hs2.vector[2]-hs1.vector[2]*hs2.vector[1],
1341 hs1.vector[2]*hs2.vector[0]-hs1.vector[0]*hs2.vector[2],
1342 hs1.vector[0]*hs2.vector[1]-hs1.vector[1]*hs2.vector[0]])
1344 pt1 = uu*hs1.vector + vv*hs2.vector + ww*cross_product
1345 pt2 = uu*hs1.vector + vv*hs2.vector - ww*cross_product
1346 if np.abs(1.0-np.dot(pt1, pt2))<1.0e-20:
1347 return pt1
1348 return np.array([pt1, pt2])