Hide keyboard shortcuts

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 

5 

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 

10 

11Szalay A. et al. (2007) 

12"Indexing the Sphere with the Hierarchical Triangular Mesh" 

13arXiv:cs/0701164 

14""" 

15 

16import numpy as np 

17import numbers 

18from lsst.sims.utils import cartesianFromSpherical, sphericalFromCartesian 

19 

20__all__ = ["Trixel", "HalfSpace", "findHtmid", "trixelFromHtmid", 

21 "basic_trixels", "halfSpaceFromRaDec", "levelFromHtmid", 

22 "getAllTrixels", "halfSpaceFromPoints", 

23 "intersectHalfSpaces"] 

24 

25 

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. 

30 

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 """ 

36 

37 def __init__(self, present_htmid, present_corners): 

38 """ 

39 Initialize the current Trixel 

40 

41 Parameters 

42 ---------- 

43 present_htmid is the htmid of this Trixel 

44 

45 present_corners is a numpy array. Each row 

46 contains the Cartesian coordinates of one of 

47 this Trixel's corners. 

48 

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 

65 

66 def __eq__(self, other): 

67 

68 tol = 1.0e-20 

69 

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 

74 

75 return False 

76 

77 def __ne__(self, other): 

78 return not (self == other) 

79 

80 @property 

81 def htmid(self): 

82 """ 

83 The unique integer identifying this trixel. 

84 """ 

85 return self._htmid 

86 

87 def contains(self, ra, dec): 

88 """ 

89 Returns True if the specified RA, Dec are 

90 inside this trixel; False if not. 

91 

92 RA and Dec are in degrees. 

93 """ 

94 xyz = cartesianFromSpherical(np.radians(ra), np.radians(dec)) 

95 return self.contains_pt(xyz) 

96 

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 

106 

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 

116 

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 

126 

127 def _contains_one_pt(self, pt): 

128 """ 

129 pt is a Cartesian point (not necessarily on the unit sphere). 

130 

131 Returns True if the point projected onto the unit sphere 

132 is contained within this trixel; False if not. 

133 

134 See equation 5 of 

135 

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 

145 

146 return False 

147 

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). 

153 

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. 

157 

158 See equation 5 of 

159 

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)) 

168 

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.). 

175 

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) 

184 

185 def _create_w(self): 

186 

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()) 

193 

194 self._w_arr = [w0, w1, w2] 

195 

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 

201 

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 

210 

211 @property 

212 def t0(self): 

213 """ 

214 The zeroth child trixel of this trixel. 

215 

216 See Figure 2 of 

217 

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 

226 

227 @property 

228 def t1(self): 

229 """ 

230 The first child trixel of this trixel. 

231 

232 See Figure 2 of 

233 

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 

242 

243 @property 

244 def t2(self): 

245 """ 

246 The second child trixel of this trixel. 

247 

248 See Figure 2 of 

249 

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 

258 

259 @property 

260 def t3(self): 

261 """ 

262 The third child trixel of this trixel. 

263 

264 See Figure 2 of 

265 

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 

274 

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] 

280 

281 def get_child(self, dex): 

282 """ 

283 Return a specific child trixel of this trixel. 

284 

285 dex is an integer in the range [0,3] denoting 

286 which child to return 

287 

288 See Figure 1 of 

289 

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 

294 

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) 

308 

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) 

316 

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]) 

323 

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 

331 

332 Szalay A. et al. (2007) 

333 "Indexing the Sphere with the Hierarchical Triangular Mesh" 

334 arXiv:cs/0701164 

335 

336 For a given level == ell, there are 8*4**(ell-1) trixels in 

337 the entire unit sphere. 

338 

339 The htmid values of trixels with level==ell will consist 

340 of 4 + 2*(ell-1) bits 

341 """ 

342 return self._level 

343 

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 

352 

353 @property 

354 def bounding_circle(self): 

355 """ 

356 The circle on the unit sphere that bounds this trixel. 

357 

358 See equation 4.2 of 

359 

360 Szalay A. et al. (2007) 

361 "Indexing the Sphere with the Hierarchical Triangular Mesh" 

362 arXiv:cs/0701164 

363 

364 Returns 

365 ------- 

366 A tuple: 

367 Zeroth element is the unit vector pointing at 

368 the center of the bounding circle 

369 

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). 

374 

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()) 

381 

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() 

385 

386 if np.abs(dd) > 1.0: 

387 raise RuntimeError("Bounding circle has dd %e (should be between -1 and 1)" % dd) 

388 

389 self._bounding_circle = (vb, dd, np.arccos(dd)) 

390 

391 return self._bounding_circle 

392 

393 

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 

402 

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])]) 

406 

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])]) 

410 

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])]) 

414 

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])]) 

418 

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])]) 

422 

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])]) 

426 

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])]) 

430 

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])]) 

434 

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} 

443 

444 

445def levelFromHtmid(htmid): 

446 """ 

447 Find the level of a trixel from its htmid. The level 

448 indicates how refined the triangular mesh is. 

449 

450 There are 8*4**(d-1) triangles in a mesh of level=d 

451 

452 (equation 2.5 of 

453 Szalay A. et al. (2007) 

454 "Indexing the Sphere with the Hierarchical Triangular Mesh" 

455 arXiv:cs/0701164) 

456 

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 

464 

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') 

469 

470 return i_level 

471 

472 

473def trixelFromHtmid(htmid): 

474 """ 

475 Return the trixel corresponding to the given htmid 

476 (htmid is the unique integer identifying each trixel). 

477 

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. 

485 

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) 

490 

491 ans = None 

492 

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 

509 

510 if ans is None: 

511 raise RuntimeError("Unable to find trixel for id %d" % htmid) 

512 

513 if level == 1: 

514 return ans 

515 

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) 

523 

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) 

533 

534 if target >= 4: 

535 raise RuntimeError("target %d" % target) 

536 

537 ans = ans.get_child(target) 

538 complement >>= 2 

539 

540 return ans 

541 

542 

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 """ 

550 

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) 

554 

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 

561 

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 

570 

571 parent_id = htmid >> 2 # the immediate parent of htmid 

572 

573 while parent_id not in trixel_dict: 

574 

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 

581 

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 

590 

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) 

596 

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) 

604 

605 return trixel_dict 

606 

607 

608def _iterateTrixelFinder(pt, parent, max_level): 

609 """ 

610 Method to iteratively find the htmid of the trixel containing 

611 a point. 

612 

613 Parameters 

614 ---------- 

615 pt is a Cartesian point (not necessarily on the unit sphere) 

616 

617 parent is the largest trixel currently known to contain the point 

618 

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. 

622 

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) 

635 

636 

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. 

642 

643 Parameters 

644 ---------- 

645 ra in degrees 

646 

647 dec in degrees 

648 

649 max_level is an integer denoting the mesh level 

650 of the trixel you want found 

651 

652 Note: This method only works one point at a time. 

653 It cannot take arrays of RA and Dec. 

654 

655 Returns 

656 ------- 

657 An int (the htmid) 

658 """ 

659 

660 ra_rad = np.radians(ra) 

661 dec_rad = np.radians(dec) 

662 pt = cartesianFromSpherical(ra_rad, dec_rad) 

663 

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") 

682 

683 return _iterateTrixelFinder(pt, parent, max_level) 

684 

685 

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 

691 

692 Parameters 

693 ---------- 

694 ra in degrees (a numpy array) 

695 

696 dec in degrees (a numpy array) 

697 

698 max_level is an integer denoting the mesh level 

699 of the trixel you want found 

700 

701 Returns 

702 ------- 

703 A numpy array of ints (the htmids) 

704 

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 """ 

708 

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).") 

714 

715 if (not hasattr(_findHtmid_fast, '_trixel_dict') or 

716 _findHtmid_fast._level < max_level): 

717 

718 _findHtmid_fast._trixel_dict = getAllTrixels(max_level) 

719 _findHtmid_fast._level = max_level 

720 

721 ra_rad = np.radians(ra) 

722 dec_rad = np.radians(dec) 

723 pt_arr = cartesianFromSpherical(ra_rad, dec_rad) 

724 

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] 

733 

734 htmid_arr = np.zeros(len(pt_arr), dtype=int) 

735 

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] 

744 

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] 

749 

750 next_htmid = parent_htmid << 2 

751 children_htmid = [next_htmid, next_htmid+1, 

752 next_htmid+2, next_htmid+3] 

753 

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 

765 

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 

771 

772 return htmid_arr 

773 

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. 

779 

780 Parameters 

781 ---------- 

782 ra in degrees (either a number or a numpy array) 

783 

784 dec in degrees (either a number or a numpy array) 

785 

786 max_level is an integer denoting the mesh level 

787 of the trixel you want found 

788 

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 

809 

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 

818 

819 return _findHtmid_slow(ra, dec, max_level) 

820 

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. 

827 

828 See Section 3.1 of 

829 

830 Szalay A. et al. (2007) 

831 "Indexing the Sphere with the Hierarchical Triangular Mesh" 

832 arXiv:cs/0701164 

833 

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 """ 

839 

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 

846 

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 

866 

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 

874 

875 def __ne__(self, other): 

876 return not (self == other) 

877 

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 

884 

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 

892 

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 

900 

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()) 

909 

910 dot_product = np.dot(norm_pt, self._v) 

911 

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 

918 

919 return False 

920 

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) 

927 

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) 

935 

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. 

940 

941 see equation 4.8 of 

942 

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 

954 

955 det = b*b - 4*a*c 

956 if det < 0.0: 

957 return False 

958 

959 sqrt_det = np.sqrt(det) 

960 pos = (-b + sqrt_det)/(2.0*a) 

961 

962 if pos >= 0.0 and pos <= 1.0: 

963 return True 

964 

965 neg = (-b - sqrt_det)/(2.0*a) 

966 if neg >= 0.0 and neg <= 1.0: 

967 return True 

968 

969 return False 

970 

971 def intersects_circle(self, center, radius_rad): 

972 """ 

973 Does this Half Space intersect a circle on the unit sphere 

974 

975 center is the unit vector pointing to the center of the circle 

976 

977 radius_rad is the radius of the circle in radians 

978 

979 Returns a boolean 

980 """ 

981 

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) 

991 

992 if theta > self._phi + radius_rad: 

993 return False 

994 

995 return True 

996 

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. 

1001 

1002 See the discussion around equation 4.2 of 

1003 

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]) 

1010 

1011 def contains_trixel(self, tx): 

1012 """ 

1013 tx is a Trixel. 

1014 

1015 Return "full" if the Trixel is fully contained by 

1016 this halfspace. 

1017 

1018 Return "partial" if the Trixel is only partially 

1019 contained by this halfspace 

1020 

1021 Return "outside" if no part of the Trixel is 

1022 contained by this halfspace. 

1023 

1024 See section 4.1 of 

1025 

1026 Szalay A. et al. (2007) 

1027 "Indexing the Sphere with the Hierarchical Triangular Mesh" 

1028 arXiv:cs/0701164 

1029 """ 

1030 

1031 containment = self.contains_many_pts(tx.corners) 

1032 

1033 if containment.all(): 

1034 return "full" 

1035 elif containment.any(): 

1036 return "partial" 

1037 

1038 if tx.contains_pt(self._v): 

1039 return "partial" 

1040 

1041 # check if the trixel's bounding circle intersects 

1042 # the halfspace 

1043 if not self.intersects_bounding_circle(tx): 

1044 return "outside" 

1045 

1046 # need to test that the bounding circle intersect the halfspace 

1047 # boundary 

1048 

1049 for edge in ((tx.corners[0], tx.corners[1]), 

1050 (tx.corners[1], tx.corners[2]), 

1051 (tx.corners[2], tx.corners[0])): 

1052 

1053 if self.intersects_edge(edge[0], edge[1]): 

1054 return "partial" 

1055 

1056 return "outside" 

1057 

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 

1063 

1064 Parameters 

1065 ---------- 

1066 bounds is a list of trixel bounds as returned by HalfSpace.findAllTrixels 

1067 

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] 

1088 

1089 if len(current_list) >0: 

1090 final_output.append((min(current_list), max(current_list))) 

1091 return final_output 

1092 

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) 

1102 

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]) 

1107 

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] 

1110 

1111 dex1 = 0 

1112 dex2 = 0 

1113 n_b1 = len(b1_keep) 

1114 n_b2 = len(b2_keep) 

1115 joint_bounds = [] 

1116 

1117 if n_b1==0 or n_b2==0: 

1118 return joint_bounds 

1119 

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)) 

1141 

1142 # advance the bound that is lowest 

1143 if r1[1] < r2[1]: 

1144 dex1 += 1 

1145 else: 

1146 dex2 += 1 

1147 

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 

1152 

1153 return HalfSpace.merge_trixel_bounds(joint_bounds) 

1154 

1155 

1156 def findAllTrixels(self, level): 

1157 """ 

1158 Find the HTMIDs of all of the trixels filling the half space 

1159 

1160 Parameters 

1161 ---------- 

1162 level is an integer denoting the resolution of the trixel grid 

1163 

1164 Returns 

1165 ------- 

1166 A list of tuples. Each tuple gives an inclusive range of HTMIDs 

1167 corresponding to trixels within the HalfSpace 

1168 """ 

1169 

1170 global basic_trixels 

1171 

1172 active_trixels = [] 

1173 for trixel_name in basic_trixels: 

1174 active_trixels.append(basic_trixels[trixel_name]) 

1175 

1176 output_prelim = [] 

1177 max_d_htmid = 0 

1178 

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 

1196 

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 

1202 

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)) 

1217 

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 

1230 

1231 active_trixels = new_active_trixels 

1232 if len(active_trixels) == 0: 

1233 break 

1234 

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)) 

1241 

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]) 

1247 

1248 return self.merge_trixel_bounds(output) 

1249 

1250 

1251def halfSpaceFromRaDec(ra, dec, radius): 

1252 """ 

1253 Take an RA, Dec and radius of a circular field of view and return 

1254 a HalfSpace 

1255 

1256 Parameters 

1257 ---------- 

1258 ra in degrees 

1259 

1260 dec in degrees 

1261 

1262 radius in degrees 

1263 

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) 

1271 

1272 

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. 

1277 

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 

1282 

1283 pt3 -- a tuple containing (RA, Dec) in degrees of a point 

1284 contained in the Half Space 

1285 

1286 Returns 

1287 -------- 

1288 A Half Space 

1289 """ 

1290 

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]]) 

1296 

1297 axis /= np.sqrt(np.sum(axis**2)) 

1298 

1299 vv3 = cartesianFromSpherical(np.radians(pt3[0]), np.radians(pt3[1])) 

1300 if np.dot(axis, vv3)<0.0: 

1301 axis *= -1.0 

1302 

1303 return HalfSpace(axis, 0.0) 

1304 

1305def intersectHalfSpaces(hs1, hs2): 

1306 """ 

1307 Parameters 

1308 ---------- 

1309 hs1, hs2 are Half Spaces 

1310 

1311 Returns 

1312 ------- 

1313 A list of the cartesian points where the Half Spaces intersect. 

1314 

1315 Note: if the Half Spaces are identical, then this list will be 

1316 empty. 

1317 

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 [] 

1327 

1328 denom = 1.0-gamma**2 

1329 if denom<0.0: 

1330 return [] 

1331 

1332 num = hs1.dd**2+hs2.dd**2-2.0*gamma*hs1.dd*hs2.dd 

1333 if denom < num: 

1334 return [] 

1335 

1336 uu = (hs1.dd-gamma*hs2.dd)/denom 

1337 vv = (hs2.dd-gamma*hs1.dd)/denom 

1338 

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]]) 

1343 

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])