Coverage for python/lsst/dax/apdb/pixelization.py: 71%

60 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-24 02:00 -0700

1# This file is part of dax_apdb. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ["Pixelization"] 

25 

26import logging 

27from typing import Any, overload 

28 

29import lsst.sphgeom 

30 

31_LOG = logging.getLogger(__name__) 

32 

33 

34class Pixelization: 

35 """Wrapper for pixelization classes from `sphgeom` with configurable 

36 pixelization type and parameters. 

37 

38 Parameters 

39 ---------- 

40 pixelization : `str` 

41 Name of a pixelization type, one of ""htm", "q3c", "mq3c", or 

42 "healpix". 

43 pix_level : `int` 

44 Pixelization level. 

45 pix_max_ranges : `int` 

46 Maximum number of ranges returned from `envelope()` method. 

47 """ 

48 

49 def __init__(self, pixelization: str, pix_level: int, pix_max_ranges: int): 

50 self._pix_max_ranges = pix_max_ranges 

51 self._is_healpix = False 

52 

53 self.pixelator: lsst.sphgeom.Pixelization 

54 if pixelization == "htm": 

55 self.pixelator = lsst.sphgeom.HtmPixelization(pix_level) 

56 elif pixelization == "q3c": 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true

57 self.pixelator = lsst.sphgeom.Q3cPixelization(pix_level) 

58 elif pixelization == "mq3c": 58 ↛ 60line 58 didn't jump to line 60 because the condition on line 58 was always true

59 self.pixelator = lsst.sphgeom.Mq3cPixelization(pix_level) 

60 elif pixelization == "healpix": 

61 # Healpix does not support maxRanges. 

62 self._pix_max_ranges = 0 

63 self._is_healpix = True 

64 self.pixelator = lsst.sphgeom.HealpixPixelization(pix_level) # type: ignore[attr-defined] 

65 else: 

66 raise ValueError(f"unknown pixelization: {pixelization}") 

67 

68 def pixels(self, region: lsst.sphgeom.Region) -> list[int]: 

69 """Compute set of the pixel indices for given region. 

70 

71 Parameters 

72 ---------- 

73 region : `lsst.sphgeom.Region` 

74 """ 

75 # We want finest set of pixels, so ask as many pixel as reasonable, but 

76 # healpix does not support non-zero maxRanges. 

77 ranges = self.pixelator.envelope(region, 0 if self._is_healpix else 1_000_000) 

78 indices = [] 

79 for lower, upper in ranges: 

80 indices += list(range(lower, upper)) 

81 return indices 

82 

83 def circle_pixels(self, ra: float, dec: float, pad_arcsec: float) -> list[int]: 

84 """Make a list of spatial partitions that a small circle touches. 

85 

86 Parameters 

87 ---------- 

88 ra, dec : `float` 

89 Center of a circle, degrees. 

90 pad_arcsec : `float` 

91 Radius of a circle in arcseconds. 

92 

93 Returns 

94 ------- 

95 pixels : `list` [`int`] 

96 All pixels that envelop the circle. 

97 """ 

98 lon_lat = lsst.sphgeom.LonLat.fromDegrees(ra, dec) 

99 center = lsst.sphgeom.UnitVector3d(lon_lat) 

100 region = lsst.sphgeom.Circle(center, lsst.sphgeom.Angle.fromDegrees(pad_arcsec / 3600.0)) 

101 return self.pixels(region) 

102 

103 @overload 

104 def pixel(self, direction: lsst.sphgeom.UnitVector3d, /) -> int: ... 104 ↛ exitline 104 didn't return from function 'pixel' because

105 

106 @overload 

107 def pixel(self, ra: float, dec: float, /) -> int: ... 107 ↛ exitline 107 didn't return from function 'pixel' because

108 

109 def pixel(self, *args: Any) -> int: 

110 """Compute the index of the pixel for given direction. 

111 

112 Parameters 

113 ---------- 

114 args 

115 The method can take either a single `lsst.sphgeom.UnitVector3d` or 

116 a pair of floating point numbers (or values convertible to floats) 

117 representing RA and Dec in degrees. 

118 

119 Returns 

120 ------- 

121 pixel : `int` 

122 Pixel index. 

123 """ 

124 match args: 

125 case (lsst.sphgeom.UnitVector3d() as direction,): 

126 pass 

127 case (ra, dec): 127 ↛ 134line 127 didn't jump to line 134 because the pattern on line 127 always matched

128 try: 

129 direction = lsst.sphgeom.UnitVector3d( 

130 lsst.sphgeom.LonLat.fromDegrees(float(ra), float(dec)) 

131 ) 

132 except (TypeError, ValueError) as exc: 

133 raise TypeError(f"Unexpected arguments: {args}") from exc 

134 case _: 

135 raise TypeError(f"Unexpected arguments: {args}") 

136 index = self.pixelator.index(direction) 

137 return index 

138 

139 def region(self, pixel: int) -> lsst.sphgeom.Region: 

140 """Return region corresponding to a pixel index. 

141 

142 Parameters 

143 ---------- 

144 pixel : `int` 

145 Pixel index. 

146 

147 Returns 

148 ------- 

149 region : `lsst.sphgeom.Region` 

150 Region for a given pixel index. 

151 """ 

152 region = self.pixelator.pixel(pixel) 

153 return region 

154 

155 def envelope(self, region: lsst.sphgeom.Region) -> list[tuple[int, int]]: 

156 """Generate a set of HTM indices covering specified region. 

157 

158 Parameters 

159 ---------- 

160 region: `sphgeom.Region` 

161 Region that needs to be indexed. 

162 

163 Returns 

164 ------- 

165 ranges : `list` of `tuple` 

166 Sequence of ranges, range is a tuple (minHtmID, maxHtmID). 

167 """ 

168 _LOG.debug("region: %s", region) 

169 indices = self.pixelator.envelope(region, self._pix_max_ranges) 

170 

171 if _LOG.isEnabledFor(logging.DEBUG): 171 ↛ 179line 171 didn't jump to line 179 because the condition on line 171 was always true

172 for irange in indices.ranges(): 

173 _LOG.debug( 

174 "range: %s %s", 

175 self.pixelator.toString(irange[0]), 

176 self.pixelator.toString(irange[1]), 

177 ) 

178 

179 return indices.ranges()