Coverage for tests/test_intrinsicZernikes.py: 96%
95 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-24 08:38 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-24 08:38 +0000
1# This file is part of ip_isr.
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# 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 <https://www.gnu.org/licenses/>.
22import unittest
23import tempfile
25import numpy as np
26from astropy.table import Table
27import astropy.units as u
29import lsst.utils.tests
31from lsst.ip.isr import IntrinsicZernikes
34class IntrinsicZernikesTestCase(lsst.utils.tests.TestCase):
35 """Test the IntrinsicZernikes calibration class."""
37 def setUp(self):
38 """Create test data for intrinsic Zernikes."""
39 # Create test Zernike coefficients for Noll indices 4, 5, 6
40 # (defocus, astigmatism)
41 self.noll_indices = np.array([4, 5, 6])
43 # Build a regular 3x3 grid of sample points
44 x_unique = np.array([-1.0, 0.0, 1.0])
45 y_unique = np.array([-0.5, 0.0, 0.5])
46 x_grid, y_grid = np.meshgrid(x_unique, y_unique)
47 self.field_x = x_grid.ravel()
48 self.field_y = y_grid.ravel()
50 # Create values: shape (n_points, n_zernikes). The CCS and OCS
51 # systems get independent values so tests can tell their (per-element)
52 # contributions apart.
53 rng = np.random.default_rng(seed=57721)
54 self.values = rng.normal(
55 scale=0.1, size=(len(self.field_x), len(self.noll_indices))
56 ) # microns
57 self.values_ocs = rng.normal(
58 scale=0.1, size=(len(self.field_x), len(self.noll_indices))
59 ) # microns
61 # Create astropy tables in the format expected by __init__
62 self.inputTable = Table()
63 self.inputTable["x"] = self.field_x * u.deg
64 self.inputTable["y"] = self.field_y * u.deg
66 self.inputTableOCS = Table()
67 self.inputTableOCS["x"] = self.field_x * u.deg
68 self.inputTableOCS["y"] = self.field_y * u.deg
70 # Add Zernike columns
71 for i, noll in enumerate(self.noll_indices):
72 self.inputTable[f"Z{noll}"] = self.values[:, i] * u.um
73 self.inputTableOCS[f"Z{noll}"] = self.values_ocs[:, i] * u.um
75 self.inputTableOCS.meta["coord_sys"] = "OCS"
76 # Create the calibration object
77 self.calib = IntrinsicZernikes(
78 table=self.inputTable, table_ocs=self.inputTableOCS
79 )
81 def test_initialization_with_table(self):
82 """Test that IntrinsicZernikes initializes correctly from a table."""
83 np.testing.assert_array_equal(self.calib.field_x, self.field_x)
84 np.testing.assert_array_equal(self.calib.field_y, self.field_y)
85 np.testing.assert_array_equal(self.calib.noll_indices, self.noll_indices)
86 np.testing.assert_array_equal(self.calib.values, self.values)
87 self.assertIsNotNone(self.calib.interpolator)
89 def test_metadata(self):
90 """Test that metadata is properly set."""
91 metadata = self.calib.getMetadata()
92 self.assertEqual(metadata["OBSTYPE"], "INTRINSIC_ZERNIKES")
93 self.assertEqual(metadata["INTRINSIC_ZERNIKES_SCHEMA"], "Intrinsic Zernikes")
94 self.assertEqual(metadata["INTRINSIC_ZERNIKES_VERSION"], 1.1)
96 def test_dict_roundtrip(self):
97 """Test round-tripping through dictionary."""
98 newCalib = IntrinsicZernikes.fromDict(self.calib.toDict())
99 self.assertEqual(newCalib, self.calib)
101 def test_table_roundtrip(self):
102 """Test round-tripping through table."""
103 newCalib = IntrinsicZernikes.fromTable(self.calib.toTable())
104 self.assertEqual(newCalib, self.calib)
106 def test_text_io_not_implemented(self):
107 """Test that text I/O is intentionally not implemented."""
108 for ext in ("yaml", "ecsv"):
109 with self.subTest(ext=ext):
110 filename = f"intrinsic_zernikes.{ext}"
112 with self.assertRaises(NotImplementedError):
113 self.calib.writeText(filename)
115 with self.assertRaises(NotImplementedError):
116 self.calib.readText(filename)
118 def test_fits_roundtrip(self):
119 """Test round-tripping through FITS file."""
120 with tempfile.TemporaryDirectory() as tempdir:
121 import os
123 filename = os.path.join(tempdir, "intrinsic_zernikes.fits")
125 self.calib.writeFits(filename)
126 newCalib = IntrinsicZernikes.readFits(filename)
127 self.assertEqual(newCalib, self.calib)
129 def test_fromDict_wrong_obstype(self):
130 """Test that fromDict raises error for wrong OBSTYPE."""
131 outDict = self.calib.toDict()
132 outDict["metadata"]["OBSTYPE"] = "WRONG_TYPE"
134 with self.assertRaises(RuntimeError) as context:
135 IntrinsicZernikes.fromDict(outDict)
137 self.assertIn("Incorrect intrinsic zernikes supplied", str(context.exception))
138 self.assertIn("INTRINSIC_ZERNIKES", str(context.exception))
139 self.assertIn("WRONG_TYPE", str(context.exception))
141 def test_getIntrinsicZernikes(self):
142 """Test interpolation of Zernike coefficients."""
143 # Test at a grid point
144 field_x_test = 0.0
145 field_y_test = 0.0
147 zernikes = self.calib.getIntrinsicZernikes(field_x_test, field_y_test)
149 # On a grid point each interpolator returns its own stored value, so
150 # the result is the element-wise sum of the CCS and OCS coefficients
151 # (rotTelPos defaults to 0, so the OCS query point is unrotated).
152 center_idx = np.flatnonzero((self.field_x == 0.0) & (self.field_y == 0.0))[0]
153 expected = self.values[center_idx, :] + self.values_ocs[center_idx, :]
154 self.assertFloatsEqual(zernikes, expected)
156 # Test with specific Noll indices
157 zernikes_subset = self.calib.getIntrinsicZernikes(
158 field_x_test, field_y_test, noll_indices=[4]
159 )
160 self.assertFloatsEqual(zernikes_subset, zernikes[:, 0])
162 def test_getIntrinsicZernikes_array(self):
163 """Test interpolation with array inputs."""
164 field_x_test = np.array([0.0, 0.5])
165 field_y_test = np.array([0.0, 0.25])
167 zernikes = self.calib.getIntrinsicZernikes(field_x_test, field_y_test)
168 z0 = self.calib.getIntrinsicZernikes(field_x_test[0], field_y_test[0])
169 z1 = self.calib.getIntrinsicZernikes(field_x_test[1], field_y_test[1])
170 np.testing.assert_array_almost_equal(zernikes[[0]], z0)
171 np.testing.assert_array_almost_equal(zernikes[[1]], z1)
173 # Should return shape (n_points, n_zernikes)
174 self.assertEqual(zernikes.shape, (2, len(self.noll_indices)))
177class MemoryTester(lsst.utils.tests.MemoryTestCase):
178 pass
181def setup_module(module):
182 lsst.utils.tests.init()
185if __name__ == "__main__": 185 ↛ 186line 185 didn't jump to line 186 because the condition on line 185 was never true
186 import sys
188 setup_module(sys.modules[__name__])
189 unittest.main()