Coverage for tests/test_tests_checks.py: 100%
187 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 09:26 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 09:26 +0000
1# This file is part of lsst-images.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12from __future__ import annotations
14import astropy.time
15import astropy.units as u
16import numpy as np
17import pytest
18from astropy.coordinates import SkyCoord
20from lsst.images import Box, Mask, MaskPlane, MaskSchema
21from lsst.images.tests import annotate_errors, assert_masks_equal, assert_values_equal
24def test_assert_values_equal_passes():
25 """Assert equal arrays pass."""
26 assert_values_equal(np.array([1, 2, 3]), np.array([1, 2, 3]))
29def test_assert_values_equal_nan_equal_by_default():
30 """Assert NaN compares equal by default."""
31 a = np.array([1.0, np.nan])
32 assert_values_equal(a, a.copy())
35def test_assert_values_equal_exact_by_default():
36 """Assert a small difference fails by default (exact equality)."""
37 with pytest.raises(AssertionError, match="max abs diff"):
38 assert_values_equal(np.array([1.0]), np.array([1.0 + 1e-6]))
41def test_assert_values_equal_rtol_passes():
42 """Assert an explicit relative tolerance passes."""
43 assert_values_equal(np.array([1.0]), np.array([1.0 + 1e-6]), rtol=1e-5)
46def test_assert_values_equal_fails_with_report():
47 """Assert a mismatch raises a report message."""
48 a = np.array([1.0, 2.0, 3.0])
49 b = np.array([1.0, 2.0, 4.0])
50 with pytest.raises(AssertionError, match="1/3 values differ"):
51 assert_values_equal(a, b)
54def test_assert_values_equal_label_prefix():
55 """Assert the label appears in the failure message."""
56 with pytest.raises(AssertionError, match=r"mask\[BAD\]: arrays are not equal"):
57 assert_values_equal(np.array([True]), np.array([False]), label="mask[BAD]")
60def test_assert_values_equal_shape_mismatch():
61 """Assert a shape mismatch raises."""
62 with pytest.raises(AssertionError, match="shape"):
63 assert_values_equal(np.zeros((2, 2)), np.zeros((3, 3)))
66def test_assert_values_equal_non_numeric_reports_indices():
67 """Assert non-numeric arrays report differing indices (via numpy)."""
68 a = np.array(["a", "b", "c"])
69 b = np.array(["a", "b", "z"])
70 with pytest.raises(AssertionError, match=r"Mismatched elements: 1 / 3"):
71 assert_values_equal(a, b)
74def test_assert_values_equal_nan_not_equal_to_number():
75 """Assert NaN still does not equal a finite number."""
76 with pytest.raises(AssertionError):
77 assert_values_equal(np.array([np.nan]), np.array([1.0]))
80def test_assert_values_equal_atol_quantity_unit_aware():
81 """Assert a Quantity atol is converted to the unit of a."""
82 a = np.array([1.0, 2.0]) * u.deg
83 b = a.to(u.arcsec)
84 assert_values_equal(a, b, rtol=0.0, atol=1e-7 * u.arcsec)
87def test_assert_values_equal_unit_aware():
88 """Assert unit-aware comparison converts b to the unit of a."""
89 a = np.array([1.0, 2.0]) * u.deg
90 b = a.to(u.arcsec)
91 assert_values_equal(a, b, rtol=1e-5, atol=0.0)
94def test_assert_values_equal_incompatible_units_raise():
95 """Assert incompatible units raise."""
96 with pytest.raises(u.UnitConversionError):
97 assert_values_equal(np.array([1.0]) * u.deg, np.array([1.0]) * u.s)
100def test_assert_values_equal_time_within_atol():
101 """Assert times close enough (with atol in seconds) pass."""
102 t0 = astropy.time.Time("2026-01-01T00:00:00", scale="utc")
103 t1 = t0 + astropy.time.TimeDelta(1e-6, format="sec")
104 assert_values_equal(t1, t0, atol=1e-5 * u.s)
105 assert_values_equal(t1, t0, atol=astropy.time.TimeDelta(1e-5, format="sec"))
106 with pytest.raises(u.UnitConversionError):
107 assert_values_equal(t1, t0, atol=1e-5)
110def test_assert_values_equal_time_exact_by_default():
111 """Assert times differing by any amount fail with zero tolerance."""
112 t0 = astropy.time.Time("2026-01-01T00:00:00", scale="utc")
113 t1 = t0 + astropy.time.TimeDelta(1e-6, format="sec")
114 with pytest.raises(AssertionError, match="differ by"):
115 assert_values_equal(t1, t0)
118def test_assert_values_equal_time_quantity_atol():
119 """Assert a time-unit Quantity atol bounds the time difference."""
120 t0 = astropy.time.Time("2026-01-01T00:00:00", scale="utc")
121 t1 = t0 + astropy.time.TimeDelta(0.002, format="sec")
122 assert_values_equal(t1, t0, atol=u.Quantity(3, "ms"))
123 with pytest.raises(AssertionError, match="differ by"):
124 assert_values_equal(t1, t0, atol=u.Quantity(1, "ms"))
127def test_assert_values_equal_time_incompatible_atol_raises():
128 """Assert a non-time Quantity atol for times raises."""
129 t0 = astropy.time.Time("2026-01-01T00:00:00", scale="utc")
130 with pytest.raises(u.UnitConversionError):
131 assert_values_equal(t0, t0, atol=u.Quantity(1.0, "m"))
134def test_assert_values_equal_timedelta():
135 """Assert timedeltas compare by their difference in seconds."""
136 d0 = astropy.time.TimeDelta(30.0, format="sec")
137 assert_values_equal(d0, astropy.time.TimeDelta(30.0, format="sec"))
138 assert_values_equal(d0, astropy.time.TimeDelta(30.000001, format="sec"), atol=1e-5 * u.s)
139 assert_values_equal(
140 d0, astropy.time.TimeDelta(30.000001, format="sec"), atol=astropy.time.TimeDelta(1e-5, format="sec")
141 )
142 with pytest.raises(AssertionError, match="differ by"):
143 assert_values_equal(d0, astropy.time.TimeDelta(30.5, format="sec"))
146def test_assert_values_equal_timedelta_rtol():
147 """Assert timedeltas accept a relative tolerance on their magnitude."""
148 d0 = astropy.time.TimeDelta(30.0, format="sec")
149 assert_values_equal(d0, astropy.time.TimeDelta(30.5, format="sec"), rtol=0.1)
150 with pytest.raises(AssertionError, match="differ by"):
151 assert_values_equal(d0, astropy.time.TimeDelta(30.5, format="sec"), rtol=1e-6)
154def test_assert_values_equal_nan_semantics():
155 """Assert NaN matches NaN only when equal_nan, for timedeltas too."""
156 d0 = astropy.time.TimeDelta([1.0, np.nan], format="sec")
157 assert_values_equal(d0, d0.copy())
158 with pytest.raises(AssertionError, match="values differ"):
159 assert_values_equal(d0, d0.copy(), equal_nan=False)
160 with pytest.raises(AssertionError, match="values differ"):
161 assert_values_equal(d0, astropy.time.TimeDelta([1.0, 2.0], format="sec"))
164def test_assert_values_equal_mixed_operand_kinds_raise():
165 """Assert mixing operand kinds (bare numeric with unitful, or different
166 astropy types) raises instead of comparing as objects.
167 """
168 t0 = astropy.time.Time("2026-01-01T00:00:00", scale="utc")
169 d0 = astropy.time.TimeDelta(30.0, format="sec")
170 with pytest.raises(TypeError, match="cannot compare Time with TimeDelta"):
171 assert_values_equal(t0, d0)
172 with pytest.raises(TypeError, match="cannot compare TimeDelta with Time"):
173 assert_values_equal(d0, t0)
174 with pytest.raises(TypeError, match="cannot compare Time with float"):
175 assert_values_equal(t0, 1.0)
176 with pytest.raises(u.UnitConversionError):
177 assert_values_equal(1.0 * u.s, 1.0)
178 with pytest.raises(u.UnitConversionError):
179 assert_values_equal(1.0, 1.0 * u.s)
180 with pytest.raises(TypeError, match="cannot compare SkyCoord with float"):
181 assert_values_equal(SkyCoord(1.0, 2.0, unit="deg"), 1.0)
184def test_assert_values_equal_exact_operands():
185 """Assert non-floating-point operands (ints, bools, strings) compare
186 exactly and report mismatches through numpy.testing.assert_array_equal.
187 """
188 assert_values_equal(np.array([1, 2, 3]), np.array([1, 2, 3]))
189 assert_values_equal(np.array([True, False]), np.array([True, False]))
190 assert_values_equal(["a", "b"], ["a", "b"])
191 assert_values_equal("a", "a")
192 with pytest.raises(AssertionError, match=r"Mismatched elements: 1 / 3"):
193 assert_values_equal(np.array([1, 2, 3]), np.array([1, 5, 3]))
194 with pytest.raises(AssertionError, match=r"ACTUAL: array\('a'"):
195 assert_values_equal("a", "b")
196 with pytest.raises(AssertionError, match=r"mask: arrays are not equal"):
197 assert_values_equal(np.array([True, False]), np.array([True, True]), label="mask")
198 with pytest.raises(AssertionError, match=r"shapes \(3,\), \(2,\) mismatch"):
199 assert_values_equal(np.array([1, 2, 3]), np.array([1, 2]))
202def test_assert_values_equal_exact_operands_reject_tolerances():
203 """Assert rtol/atol on non-floating-point operands raise (they have no
204 numeric tolerance semantics), while a unit-independent zero atol is
205 still accepted.
206 """
207 with pytest.raises(ValueError, match="floating-point"):
208 assert_values_equal(np.array([1, 2]), np.array([1, 2]), rtol=1e-9)
209 with pytest.raises(ValueError, match="floating-point"):
210 assert_values_equal(["a"], ["a"], atol=1e-3)
211 assert_values_equal(np.array([1, 2]), np.array([1, 2]), atol=0.0 * u.m)
214def test_assert_values_equal_atol_units_must_match_operands():
215 """Assert atol units follow the operands: bare for unitless, Quantity
216 (or exactly zero) for unitful.
217 """
218 with pytest.raises(u.UnitConversionError):
219 assert_values_equal(np.array([1.0]), np.array([1.0]), atol=1e-3 * u.m)
220 with pytest.raises(u.UnitConversionError):
221 assert_values_equal(1.0, 1.0, atol=astropy.time.TimeDelta(1e-3, format="sec"))
222 with pytest.raises(u.UnitConversionError):
223 assert_values_equal(np.array([1.0]) * u.deg, np.array([1.0]) * u.deg, atol=1e-6)
224 with pytest.raises(u.UnitConversionError):
225 assert_values_equal(np.array([1.0]) * u.deg, np.array([1.0]) * u.deg, atol=1e-6 * u.s)
228def test_assert_values_equal_sky_coords_within_atol():
229 """Assert sky coordinates close enough (with an angular atol) pass."""
230 a = SkyCoord(10.0, 45.0, unit="deg")
231 b = SkyCoord(10.0 + 1e-4, 45.0, unit="deg")
232 assert_values_equal(a, b, atol=1 * u.arcsec)
233 with pytest.raises(AssertionError, match=r"differ by .*arcsec"):
234 assert_values_equal(a, b, atol=0.1 * u.arcsec)
237def test_assert_values_equal_sky_coords_wrap():
238 """Assert separations across the RA = 0 meridian compare correctly."""
239 a = SkyCoord(359.9999, 30.0, unit="deg")
240 b = SkyCoord(0.0001, 30.0, unit="deg")
241 assert_values_equal(a, b, atol=1 * u.arcsec)
244def test_assert_values_equal_sky_coords_exact_atol_zero():
245 """Assert bare atol=0.0 demands exact coincidence."""
246 a = SkyCoord(10.0, 45.0, unit="deg")
247 assert_values_equal(a, SkyCoord(10.0, 45.0, unit="deg"), atol=0.0)
248 with pytest.raises(AssertionError, match=r"differ by .*arcsec"):
249 assert_values_equal(a, SkyCoord(10.0 + 1e-9, 45.0, unit="deg"), atol=0.0)
252def test_assert_values_equal_sky_coords_unitless_atol_raises():
253 """Assert a nonzero bare-float atol for sky coords raises."""
254 a = SkyCoord(10.0, 45.0, unit="deg")
255 with pytest.raises(u.UnitConversionError):
256 assert_values_equal(a, a, atol=1e-3)
257 with pytest.raises(ValueError, match="rtol"):
258 assert_values_equal(a, a, rtol=1e-6)
261def test_assert_values_equal_wrap_angles():
262 """Assert angles differing by whole turns compare equal."""
263 a = 359.9999 * u.deg
264 b = 0.0001 * u.deg
265 assert_values_equal(a, b, atol=1 * u.arcsec, wrap_angles=True)
266 assert_values_equal(1 * u.deg, -359.0 * u.deg, atol=0.0, wrap_angles=True)
267 with pytest.raises(AssertionError, match="!="):
268 assert_values_equal(a, b, atol=0.1 * u.arcsec, wrap_angles=True)
271def test_assert_values_equal_wrap_angles_requires_units():
272 """Assert wrap_angles demands angular units on operands and atol."""
273 with pytest.raises(u.UnitConversionError):
274 assert_values_equal(1.0, 1.0, wrap_angles=True)
275 with pytest.raises(u.UnitConversionError):
276 assert_values_equal(1 * u.deg, 1 * u.deg, atol=1e-3, wrap_angles=True)
277 with pytest.raises(ValueError, match="rtol"):
278 assert_values_equal(1 * u.deg, 1 * u.deg, rtol=1e-6, wrap_angles=True)
279 with pytest.raises(u.UnitConversionError):
280 assert_values_equal(1 * u.s, 1 * u.s, wrap_angles=True)
283def test_label_assertions_notes():
284 """Assert a failing check is re-raised with the label as a note."""
285 with pytest.raises(AssertionError) as exc_info:
286 with annotate_errors("psf"):
287 raise AssertionError("inner")
288 assert exc_info.value.args == ("inner",)
289 assert exc_info.value.__notes__ == ["psf"]
292def test_label_assertions_passthrough_on_success():
293 """Assert a passing block is not re-raised."""
294 with annotate_errors("psf"):
295 pass # must not raise
298def test_assert_masks_equal_notes_plane_diff() -> None:
299 """Assert that a mask mismatch adds a per-plane +/- note."""
300 schema = MaskSchema([MaskPlane("A", "dA")], dtype=np.uint8)
301 m1 = Mask(0, schema=schema, bbox=Box.factory[0:2, 0:1])
302 m1.set("A", np.array([[True], [False]]))
303 m2 = Mask(0, schema=schema, bbox=Box.factory[0:2, 0:1])
304 m2.set("A", np.array([[True], [True]]))
305 with pytest.raises(AssertionError) as excinfo:
306 assert_masks_equal(m1, m2)
307 notes = excinfo.value.__notes__ or []
308 assert any("mask[A]: +0 -1" in n for n in notes)