Coverage for tests / test_HealpixPixelization.py: 19%
117 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-26 08:41 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-26 08:41 +0000
1# This file is part of sphgeom.
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 software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
28import pickle
29import unittest
31import hpgeom as hpg
32import numpy as np
34try:
35 import yaml
36except ImportError:
37 yaml = None
39from lsst.sphgeom import Angle, Box, Circle, ConvexPolygon, Ellipse, HealpixPixelization, LonLat, UnitVector3d
42class HealpixPixelizationTestCase(unittest.TestCase):
43 """Test HEALPix pixelization."""
45 def test_construction(self):
46 """Test construction of a HealpixPixelization."""
47 with self.assertRaises(ValueError):
48 HealpixPixelization(-1)
49 h1 = HealpixPixelization(5)
50 self.assertEqual(h1.level, 5)
51 self.assertEqual(h1.getLevel(), 5)
52 self.assertEqual(h1.nside, 32)
53 h2 = HealpixPixelization(6)
54 h3 = HealpixPixelization(h2.level)
55 self.assertNotEqual(h1, h2)
56 self.assertEqual(h2, h3)
58 def test_indexing(self):
59 """Test indexing of HealpixPixelization."""
60 h = HealpixPixelization(5)
61 vec = UnitVector3d(1, 1, 1)
62 lonlat = LonLat(vec)
63 pix = hpg.angle_to_pixel(h.nside, lonlat.getLon().asDegrees(), lonlat.getLat().asDegrees())
64 self.assertEqual(h.index(UnitVector3d(1, 1, 1)), pix)
66 def test_pixel(self):
67 """Test pixel polygon of HealpixPixelization."""
68 h = HealpixPixelization(5)
69 pix_poly = h.pixel(10)
70 self.assertIsInstance(pix_poly, ConvexPolygon)
72 def test_envelope(self):
73 """Test envelope method of HealpixPixelization."""
74 # Make the hardest intersection: a region that _just_
75 # touches a healpix pixel.
76 h = HealpixPixelization(5)
77 pix = hpg.angle_to_pixel(h.nside, 50.0, 20.0)
79 corners_ra, corners_dec = hpg.boundaries(h.nside, pix, step=1)
81 # Take the southernmost corner...
82 smost = np.argmin(corners_dec)
84 # Choose challenging comparison box corners:
85 ra_range = np.array([corners_ra[smost] - 0.5, corners_ra[smost] + 0.5])
86 dec_range = np.array([corners_dec[smost] - 0.5, corners_dec[smost] + 1e-8])
88 # Test the box region
89 box = Box(
90 point1=LonLat.fromDegrees(ra_range[0], dec_range[0]),
91 point2=LonLat.fromDegrees(ra_range[1], dec_range[1]),
92 )
93 # These pixels have been checked to completely overlap the region
94 self._check_envelope(h, box, [98, 99, 104, 105])
96 # Try a polygon region:
97 poly = ConvexPolygon(
98 [
99 UnitVector3d(LonLat.fromDegrees(ra_range[0], dec_range[0])),
100 UnitVector3d(LonLat.fromDegrees(ra_range[1], dec_range[0])),
101 UnitVector3d(LonLat.fromDegrees(ra_range[1], dec_range[1])),
102 UnitVector3d(LonLat.fromDegrees(ra_range[0], dec_range[1])),
103 UnitVector3d(LonLat.fromDegrees(ra_range[0], dec_range[0])),
104 ]
105 )
106 self._check_envelope(h, poly, [98, 99, 104, 105])
108 # Try a circle region
109 circle = Circle(
110 center=UnitVector3d(
111 LonLat.fromDegrees((ra_range[0] + ra_range[1]) / 2.0, (dec_range[0] + dec_range[1]) / 2.0)
112 ),
113 angle=Angle.fromDegrees((dec_range[1] - dec_range[0]) / 2.0),
114 )
115 self._check_envelope(h, circle, [98, 99, 104, 105])
117 # Try a circle region with zero-radius (representing a point)
118 pt_circle = Circle(
119 center=UnitVector3d(
120 LonLat.fromDegrees((ra_range[0] + ra_range[1]) / 2.0, (dec_range[0] + dec_range[1]) / 2.0)
121 ),
122 )
123 self._check_envelope(h, pt_circle, [98])
125 # Try an ellipse region
126 circle = Ellipse(
127 center=UnitVector3d(
128 LonLat.fromDegrees((ra_range[0] + ra_range[1]) / 2.0, (dec_range[0] + dec_range[1]) / 2.0)
129 ),
130 alpha=Angle.fromDegrees(1.5 * (dec_range[1] - dec_range[0]) / 2.0),
131 beta=Angle.fromDegrees(0.5 * (dec_range[1] - dec_range[0]) / 2.0),
132 orientation=Angle.fromDegrees(45.0),
133 )
134 self._check_envelope(h, circle, [98, 99, 104, 105])
136 def test_envelope_missing_neighbors(self):
137 """Test envelope, with a pixel with missing neighbors.
139 Testing DM-47043.
140 """
141 h = HealpixPixelization(0)
142 h_highres = HealpixPixelization(4)
144 self.assertEqual(h._nside_highres, h_highres._nside)
146 # Find a pixel with a missing neighbor at high res.
147 n = hpg.neighbors(h_highres._nside, np.arange(hpg.nside_to_npixel(h_highres._nside)))
148 ind = np.argmin(n)
149 self.assertEqual(n.ravel()[ind], -1)
151 # This is the pixel with a bad (-1) neighbor.
152 badpix = ind // 8
153 ra, dec = hpg.pixel_to_angle(h_highres._nside, badpix)
155 box = Box(
156 point1=LonLat.fromDegrees(ra - 0.1, dec - 0.1),
157 point2=LonLat.fromDegrees(ra + 0.1, dec + 0.1),
158 )
160 self._check_envelope(h, box, [0, 1, 5])
162 def test_envelope_small(self):
163 """Test envelope, DM-54274."""
164 h = HealpixPixelization(3)
166 # Test the box region
167 box = Box(
168 point1=LonLat.fromDegrees(0.6, 0.6),
169 point2=LonLat.fromDegrees(0.61, 0.61),
170 )
171 self._check_envelope(h, box, [282, 304])
173 # Try a polygon region:
174 poly = ConvexPolygon(
175 [
176 UnitVector3d(-0.9879246204440798, -0.08691162558237045, 0.12826267445773423),
177 UnitVector3d(-0.9885269066487583, -0.08474209761828523, 0.1250333224492182),
178 UnitVector3d(-0.9885167542515381, -0.08795173158697882, 0.12287847441621431),
179 UnitVector3d(-0.9879142847819917, -0.09012140316514382, 0.1261090742779096),
180 ],
181 )
183 self._check_envelope(h, poly, [433])
185 # Try a circle region
186 circle = Circle(
187 center=UnitVector3d(LonLat.fromDegrees(0.6, 0.6)),
188 angle=Angle.fromDegrees(0.01),
189 )
190 self._check_envelope(h, circle, [282, 304])
192 # Try an ellipse region
193 circle = Ellipse(
194 center=UnitVector3d(LonLat.fromDegrees(0.6, 0.6)),
195 alpha=Angle.fromDegrees(0.02),
196 beta=Angle.fromDegrees(0.005),
197 orientation=Angle.fromDegrees(45.0),
198 )
199 self._check_envelope(h, circle, [282, 304])
201 def _check_envelope(self, pixelization, region, check_pixels):
202 """Check the envelope from a region.
204 Parameters
205 ----------
206 pixelization : `lsst.sphgeom.HealpixPixelization`
207 region : `lsst.sphgeom.Region`
208 check_pixels : `list` [`int`]
209 """
210 pixel_range = pixelization.envelope(region)
212 pixels = []
213 for r in pixel_range.ranges():
214 pixels.extend(range(r[0], r[1]))
216 self.assertEqual(pixels, check_pixels)
218 def test_interior(self):
219 """Test interior method of HealpixPixelization."""
220 h = HealpixPixelization(5)
221 pix = hpg.angle_to_pixel(h.nside, 50.0, 20.0)
223 corners_ra, corners_dec = hpg.boundaries(h.nside, pix, step=1)
225 ra_range = np.array([corners_ra.min() - 1.0, corners_ra.max() + 1.0])
226 dec_range = np.array([corners_dec.min() - 1.0, corners_dec.max() + 1.0])
228 # Test the box region
229 box = Box(
230 point1=LonLat.fromDegrees(ra_range[0], dec_range[0]),
231 point2=LonLat.fromDegrees(ra_range[1], dec_range[1]),
232 )
233 # These pixels have been checked to completely overlap the region
234 self._check_interior(h, box, [pix])
236 # Try a polygon region:
237 poly = ConvexPolygon(
238 [
239 UnitVector3d(LonLat.fromDegrees(ra_range[0], dec_range[0])),
240 UnitVector3d(LonLat.fromDegrees(ra_range[1], dec_range[0])),
241 UnitVector3d(LonLat.fromDegrees(ra_range[1], dec_range[1])),
242 UnitVector3d(LonLat.fromDegrees(ra_range[0], dec_range[1])),
243 UnitVector3d(LonLat.fromDegrees(ra_range[0], dec_range[0])),
244 ]
245 )
246 self._check_interior(h, poly, [pix])
248 # Try a circle region
249 circle = Circle(
250 center=UnitVector3d(
251 LonLat.fromDegrees((ra_range[0] + ra_range[1]) / 2.0, (dec_range[0] + dec_range[1]) / 2.0)
252 ),
253 angle=Angle.fromDegrees(2.5),
254 )
255 self._check_interior(h, circle, [pix])
257 # Try an ellipse region
258 ellipse = Ellipse(
259 center=UnitVector3d(
260 LonLat.fromDegrees((ra_range[0] + ra_range[1]) / 2.0, (dec_range[0] + dec_range[1]) / 2.0)
261 ),
262 alpha=Angle.fromDegrees(1.5),
263 beta=Angle.fromDegrees(2.5),
264 orientation=Angle.fromDegrees(45.0),
265 )
266 self._check_interior(h, ellipse, [pix])
268 def test_interior_nearly_degenerate_polygon(self):
269 """Check interior pixels with nearly degenerate polygon.
271 See DM-53933.
272 """
273 poly = ConvexPolygon(
274 [
275 UnitVector3d(-0.016144196513206758, -0.9235039286924489, -0.3832490816799893),
276 UnitVector3d(-0.01988138785117867, -0.9228560783513854, -0.38463149775728545),
277 UnitVector3d(-0.022348863683228713, -0.9209177038528237, -0.3891158066983548),
278 UnitVector3d(-0.024841110338799998, -0.918545770328335, -0.3945333788782152),
279 UnitVector3d(-0.026637426968850634, -0.9164294757774608, -0.39930873194901123),
280 UnitVector3d(-0.02509225617228684, -0.9149335621264763, -0.4028237276709774),
281 UnitVector3d(-0.0221015078570923, -0.911998223315482, -0.4095982959191202),
282 UnitVector3d(-0.020553220684181986, -0.9104571830599119, -0.4130923418972051),
283 UnitVector3d(-0.017371565494301272, -0.9072522623419655, -0.4202279871541906),
284 UnitVector3d(-0.014347349160349979, -0.9041551644174409, -0.4269632211670476),
285 UnitVector3d(-0.014303105070771129, -0.9041094310633312, -0.42706153871271785),
286 UnitVector3d(-0.012802279210872009, -0.9025514012895127, -0.4303917630221838),
287 UnitVector3d(-0.007862275539177377, -0.9015265103248066, -0.43265244227315125),
288 UnitVector3d(-0.0019484762556141095, -0.900471339489277, -0.4349109911219402),
289 UnitVector3d(0.0032282853511799436, -0.8996943229589627, -0.436508537613075),
290 UnitVector3d(0.006969847869770022, -0.9003377930402403, -0.4351359323752772),
291 UnitVector3d(0.022099717156107892, -0.9027944227880381, -0.42950417074160424),
292 UnitVector3d(0.033589177231523826, -0.9045049275054283, -0.4251383342999184),
293 UnitVector3d(0.03732521827078148, -0.9050320420971739, -0.4237025263772424),
294 UnitVector3d(0.039795546158565454, -0.9070052460628258, -0.41923477684998206),
295 UnitVector3d(0.04228779372661228, -0.9093772751891867, -0.41381724694752126),
296 UnitVector3d(0.04409395578698435, -0.9114581951867685, -0.4090228373696683),
297 UnitVector3d(0.04255005806475871, -0.9130743444969162, -0.4055671756691021),
298 UnitVector3d(0.04109145924730725, -0.91458405813035, -0.4023027374885082),
299 UnitVector3d(0.03791490987091337, -0.9178181221381234, -0.3951864803916365),
300 UnitVector3d(0.03335712714052637, -0.9223387473238281, -0.38493965404208763),
301 UnitVector3d(0.03180762744041508, -0.9238417869862389, -0.3814506881035668),
302 UnitVector3d(0.030248610691484885, -0.9253347742243898, -0.3779425580195124),
303 UnitVector3d(0.02530895801451361, -0.9263964395908428, -0.3756981412751856),
304 UnitVector3d(0.01939515964342646, -0.9274517060282444, -0.37343963470379626),
305 UnitVector3d(0.014215536919345694, -0.9281937989444676, -0.37182548340738003),
306 UnitVector3d(0.010475126504663994, -0.9276710642902235, -0.3732514811803906),
307 ],
308 )
310 h = HealpixPixelization(6)
311 self._check_interior(h, poly, [28890, 28891, 28912, 28913, 28914])
313 def _check_interior(self, pixelization, region, check_pixels):
314 """Check the interior from a region.
316 Parameters
317 ----------
318 pixelization : `lsst.sphgeom.HealpixPixelization`
319 region : `lsst.sphgeom.Region`
320 check_pixels : `list` [`int`]
321 """
322 pixel_range = pixelization.interior(region)
324 pixels = []
325 for r in pixel_range.ranges():
326 pixels.extend(range(r[0], r[1]))
328 self.assertEqual(pixels, check_pixels)
330 def test_index_to_string(self):
331 """Test converting index to string of HealpixPixelization."""
332 h = HealpixPixelization(5)
333 self.assertEqual(h.toString(0), str(0))
334 self.assertEqual(h.toString(100), str(100))
336 def test_string(self):
337 """Test string representation of HealpixPixelization."""
338 h = HealpixPixelization(5)
339 self.assertEqual(str(h), "HealpixPixelization(5)")
340 self.assertEqual(str(h), repr(h))
341 self.assertEqual(h, eval(repr(h), {"HealpixPixelization": HealpixPixelization}))
343 def test_pickle(self):
344 """Test pickling of HealpixPixelization."""
345 a = HealpixPixelization(5)
346 b = pickle.loads(pickle.dumps(a))
347 self.assertEqual(a, b)
349 @unittest.skipIf(not yaml, "YAML module can not be imported")
350 def test_yaml(self):
351 """Test yaml representation of HealpixPixelization."""
352 a = HealpixPixelization(5)
353 b = yaml.safe_load(yaml.dump(a))
354 self.assertEqual(a, b)
357if __name__ == "__main__":
358 unittest.main()