Coverage for python/lsst/obs/base/instrument_tests.py: 50%
89 statements
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-05 02:41 -0700
« prev ^ index » next coverage.py v6.4.1, created at 2022-06-05 02:41 -0700
1# This file is part of obs_base.
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 <http://www.gnu.org/licenses/>.
22"""Helpers for writing tests against subclassses of Instrument.
24These are not tests themselves, but can be subclassed (plus unittest.TestCase)
25to get a functional test of an Instrument.
26"""
28import abc
29import dataclasses
30from typing import ClassVar, Optional, Set
32import pkg_resources
33from lsst.daf.butler import Registry, RegistryConfig
34from lsst.daf.butler.formatters.yaml import YamlFormatter
35from lsst.obs.base import FilterDefinition, FilterDefinitionCollection, Instrument
36from lsst.obs.base.gen2to3 import TranslatorFactory
37from lsst.obs.base.yamlCamera import makeCamera
38from lsst.utils.introspection import get_full_type_name
40from .utils import createInitialSkyWcsFromBoresight
42DUMMY_FILTER_DEFINITIONS = FilterDefinitionCollection(
43 FilterDefinition(physical_filter="dummy_u", band="u", lambdaEff=0),
44 FilterDefinition(physical_filter="dummy_g", band="g", lambdaEff=0),
45)
48class DummyCamYamlWcsFormatter(YamlFormatter):
49 """Specialist formatter for tests that can make a WCS."""
51 @classmethod
52 def makeRawSkyWcsFromBoresight(cls, boresight, orientation, detector):
53 """Class method to make a raw sky WCS from boresight and detector.
55 This uses the API expected by define-visits. A working example
56 can be found in `FitsRawFormatterBase`.
58 Notes
59 -----
60 This makes no attempt to create a proper WCS from geometry.
61 """
62 return createInitialSkyWcsFromBoresight(boresight, orientation, detector, flipX=False)
65class DummyCam(Instrument):
67 filterDefinitions = DUMMY_FILTER_DEFINITIONS
69 @classmethod
70 def getName(cls):
71 return "DummyCam"
73 def getCamera(self):
74 # Return something that can be indexed by detector number
75 # but also has to support getIdIter.
76 filename = pkg_resources.resource_filename("lsst.obs.base", "test/dummycam.yaml")
77 return makeCamera(filename)
79 def register(self, registry, update=False):
80 """Insert Instrument, physical_filter, and detector entries into a
81 `Registry`.
82 """
83 detector_max = 2
84 dataId = {
85 "instrument": self.getName(),
86 "class_name": get_full_type_name(DummyCam),
87 "detector_max": detector_max,
88 }
89 with registry.transaction():
90 registry.syncDimensionData("instrument", dataId, update=update)
91 self._registerFilters(registry, update=update)
92 for d in range(detector_max):
93 registry.syncDimensionData(
94 "detector",
95 dict(
96 dataId,
97 id=d,
98 full_name=f"RXX_S0{d}",
99 ),
100 update=update,
101 )
103 def getRawFormatter(self, dataId):
104 # Docstring inherited fromt Instrument.getRawFormatter.
105 return DummyCamYamlWcsFormatter
107 def writeCuratedCalibrations(self, butler):
108 pass
110 def applyConfigOverrides(self, name, config):
111 pass
113 def makeDataIdTranslatorFactory(self) -> TranslatorFactory:
114 return TranslatorFactory()
117@dataclasses.dataclass
118class InstrumentTestData:
119 """Values to test against in subclasses of `InstrumentTests`."""
121 name: str
122 """The name of the Camera this instrument describes."""
124 nDetectors: int
125 """The number of detectors in the Camera."""
127 firstDetectorName: str
128 """The name of the first detector in the Camera."""
130 physical_filters: Set[str]
131 """A subset of the physical filters should be registered."""
134class InstrumentTests(metaclass=abc.ABCMeta):
135 """Tests of sublcasses of Instrument.
137 TestCase subclasses must derive from this, then `TestCase`, and override
138 ``data`` and ``instrument``.
139 """
141 data: ClassVar[Optional[InstrumentTestData]] = None
142 """`InstrumentTestData` containing the values to test against."""
144 instrument: ClassVar[Optional[Instrument]] = None
145 """The `~lsst.obs.base.Instrument` to be tested."""
147 def test_name(self):
148 self.assertEqual(self.instrument.getName(), self.data.name)
150 def test_getCamera(self):
151 """Test that getCamera() returns a reasonable Camera definition."""
152 camera = self.instrument.getCamera()
153 self.assertEqual(camera.getName(), self.instrument.getName())
154 self.assertEqual(len(camera), self.data.nDetectors)
155 self.assertEqual(next(iter(camera)).getName(), self.data.firstDetectorName)
157 def test_register(self):
158 """Test that register() sets appropriate Dimensions."""
159 registryConfig = RegistryConfig()
160 registryConfig["db"] = "sqlite://"
161 registry = Registry.createFromConfig(registryConfig)
162 # Check that the registry starts out empty.
163 self.assertFalse(registry.queryDataIds(["instrument"]).toSequence())
164 self.assertFalse(registry.queryDataIds(["detector"]).toSequence())
165 self.assertFalse(registry.queryDataIds(["physical_filter"]).toSequence())
167 # Register the instrument and check that certain dimensions appear.
168 self.instrument.register(registry)
169 instrumentDataIds = registry.queryDataIds(["instrument"]).toSequence()
170 self.assertEqual(len(instrumentDataIds), 1)
171 instrumentNames = {dataId["instrument"] for dataId in instrumentDataIds}
172 self.assertEqual(instrumentNames, {self.data.name})
173 detectorDataIds = registry.queryDataIds(["detector"]).expanded().toSequence()
174 self.assertEqual(len(detectorDataIds), self.data.nDetectors)
175 detectorNames = {dataId.records["detector"].full_name for dataId in detectorDataIds}
176 self.assertIn(self.data.firstDetectorName, detectorNames)
177 physicalFilterDataIds = registry.queryDataIds(["physical_filter"]).toSequence()
178 filterNames = {dataId["physical_filter"] for dataId in physicalFilterDataIds}
179 self.assertGreaterEqual(filterNames, self.data.physical_filters)
181 # Check that the instrument class can be retrieved.
182 registeredInstrument = Instrument.fromName(self.instrument.getName(), registry)
183 self.assertEqual(type(registeredInstrument), type(self.instrument))
185 # Check that re-registration is not an error.
186 self.instrument.register(registry)
188 def testMakeTranslatorFactory(self):
189 factory = self.instrument.makeDataIdTranslatorFactory()
190 self.assertIsInstance(factory, TranslatorFactory)
191 str(factory) # Just make sure this doesn't raise.