Coverage for python/lsst/obs/base/instrument_tests.py: 49%
115 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-22 10:15 +0000
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-22 10:15 +0000
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"""
28from __future__ import annotations
30__all__ = [
31 "DummyCam",
32 "InstrumentTestData",
33 "InstrumentTests",
34 "DummyCamYamlWcsFormatter",
35 "CuratedCalibration",
36]
38import abc
39import dataclasses
40import json
41from functools import lru_cache
42from typing import TYPE_CHECKING, Any, ClassVar, Optional, Sequence, Set
44import pkg_resources
45from lsst.daf.butler import CollectionType, DatasetType, Registry, RegistryConfig
46from lsst.daf.butler.formatters.yaml import YamlFormatter
47from lsst.obs.base import FilterDefinition, FilterDefinitionCollection, Instrument
48from lsst.obs.base.yamlCamera import makeCamera
49from lsst.utils.introspection import get_full_type_name
50from pydantic import BaseModel
52from .utils import createInitialSkyWcsFromBoresight
54if TYPE_CHECKING: 54 ↛ 55line 54 didn't jump to line 55, because the condition on line 54 was never true
55 from lsst.daf.butler import Butler
57DUMMY_FILTER_DEFINITIONS = FilterDefinitionCollection(
58 FilterDefinition(physical_filter="dummy_u", band="u"),
59 FilterDefinition(physical_filter="dummy_g", band="g"),
60)
63class DummyCamYamlWcsFormatter(YamlFormatter):
64 """Specialist formatter for tests that can make a WCS."""
66 @classmethod
67 def makeRawSkyWcsFromBoresight(cls, boresight, orientation, detector):
68 """Class method to make a raw sky WCS from boresight and detector.
70 This uses the API expected by define-visits. A working example
71 can be found in `FitsRawFormatterBase`.
73 Notes
74 -----
75 This makes no attempt to create a proper WCS from geometry.
76 """
77 return createInitialSkyWcsFromBoresight(boresight, orientation, detector, flipX=False)
80class CuratedCalibration(BaseModel):
81 """Class that implements minimal read/write interface needed to support
82 curated calibration ingest.
83 """
85 metadata: dict[str, Any]
86 values: list[int]
88 @classmethod
89 def readText(cls, path: str) -> CuratedCalibration:
90 with open(path) as f:
91 data = f.read()
92 return cls.parse_obj(json.loads(data))
94 def writeText(self, path: str) -> None:
95 with open(path, "w") as f:
96 print(self.json(), file=f)
98 def getMetadata(self) -> dict[str, Any]:
99 return self.metadata
102class DummyCam(Instrument):
103 filterDefinitions = DUMMY_FILTER_DEFINITIONS
104 additionalCuratedDatasetTypes = frozenset(["testCalib"])
105 policyName = "dummycam"
106 dataPackageDir: Optional[str] = ""
107 raw_definition = (
108 "raw_dict",
109 ("instrument", "detector", "exposure"),
110 "StructuredDataDict",
111 )
113 @classmethod
114 def getName(cls):
115 return "DummyCam"
117 @classmethod
118 @lru_cache() # For mypy
119 def getObsDataPackageDir(cls) -> Optional[str]:
120 return cls.dataPackageDir
122 def getCamera(self):
123 # Return something that can be indexed by detector number
124 # but also has to support getIdIter.
125 filename = pkg_resources.resource_filename("lsst.obs.base", "test/dummycam.yaml")
126 return makeCamera(filename)
128 def register(self, registry, update=False):
129 """Insert Instrument, physical_filter, and detector entries into a
130 `Registry`.
131 """
132 detector_max = 2
133 dataId = {
134 "instrument": self.getName(),
135 "class_name": get_full_type_name(DummyCam),
136 "detector_max": detector_max,
137 "visit_max": 1_000_000,
138 "exposure_max": 1_000_000,
139 }
140 with registry.transaction():
141 registry.syncDimensionData("instrument", dataId, update=update)
142 self._registerFilters(registry, update=update)
143 for d in range(detector_max):
144 registry.syncDimensionData(
145 "detector",
146 dict(
147 dataId,
148 id=d,
149 full_name=f"RXX_S0{d}",
150 ),
151 update=update,
152 )
154 def getRawFormatter(self, dataId):
155 # Docstring inherited fromt Instrument.getRawFormatter.
156 return DummyCamYamlWcsFormatter
158 def applyConfigOverrides(self, name, config):
159 pass
161 def writeAdditionalCuratedCalibrations(
162 self, butler: Butler, collection: Optional[str] = None, labels: Sequence[str] = ()
163 ) -> None:
164 # We want to test the standard curated calibration ingest
165 # but we do not have a standard class to use. There is no way
166 # at the moment to inject a new class into the standard list
167 # that is a package constant, so instead use this "Additional"
168 # method but call the standard curated calibration code.
169 if collection is None:
170 collection = self.makeCalibrationCollectionName(*labels)
171 butler.registry.registerCollection(collection, type=CollectionType.CALIBRATION)
173 datasetType = DatasetType(
174 "testCalib",
175 universe=butler.registry.dimensions,
176 isCalibration=True,
177 dimensions=("instrument", "detector"),
178 storageClass="CuratedCalibration",
179 )
180 runs: Set[str] = set()
181 self._writeSpecificCuratedCalibrationDatasets(
182 butler, datasetType, collection, runs=runs, labels=labels
183 )
186@dataclasses.dataclass
187class InstrumentTestData:
188 """Values to test against in subclasses of `InstrumentTests`."""
190 name: str
191 """The name of the Camera this instrument describes."""
193 nDetectors: int
194 """The number of detectors in the Camera."""
196 firstDetectorName: str
197 """The name of the first detector in the Camera."""
199 physical_filters: Set[str]
200 """A subset of the physical filters should be registered."""
203class InstrumentTests(metaclass=abc.ABCMeta):
204 """Tests of sublcasses of Instrument.
206 TestCase subclasses must derive from this, then `TestCase`, and override
207 ``data`` and ``instrument``.
208 """
210 data: ClassVar[Optional[InstrumentTestData]] = None
211 """`InstrumentTestData` containing the values to test against."""
213 instrument: ClassVar[Optional[Instrument]] = None
214 """The `~lsst.obs.base.Instrument` to be tested."""
216 def test_name(self):
217 self.assertEqual(self.instrument.getName(), self.data.name)
219 def test_getCamera(self):
220 """Test that getCamera() returns a reasonable Camera definition."""
221 camera = self.instrument.getCamera()
222 self.assertEqual(camera.getName(), self.instrument.getName())
223 self.assertEqual(len(camera), self.data.nDetectors)
224 self.assertEqual(next(iter(camera)).getName(), self.data.firstDetectorName)
226 def test_register(self):
227 """Test that register() sets appropriate Dimensions."""
228 registryConfig = RegistryConfig()
229 registryConfig["db"] = "sqlite://"
230 registry = Registry.createFromConfig(registryConfig)
231 # Check that the registry starts out empty.
232 self.assertFalse(registry.queryDataIds(["instrument"]).toSequence())
233 self.assertFalse(registry.queryDataIds(["detector"]).toSequence())
234 self.assertFalse(registry.queryDataIds(["physical_filter"]).toSequence())
236 # Register the instrument and check that certain dimensions appear.
237 self.instrument.register(registry)
238 instrumentDataIds = registry.queryDataIds(["instrument"]).toSequence()
239 self.assertEqual(len(instrumentDataIds), 1)
240 instrumentNames = {dataId["instrument"] for dataId in instrumentDataIds}
241 self.assertEqual(instrumentNames, {self.data.name})
242 detectorDataIds = registry.queryDataIds(["detector"]).expanded().toSequence()
243 self.assertEqual(len(detectorDataIds), self.data.nDetectors)
244 detectorNames = {dataId.records["detector"].full_name for dataId in detectorDataIds}
245 self.assertIn(self.data.firstDetectorName, detectorNames)
246 physicalFilterDataIds = registry.queryDataIds(["physical_filter"]).toSequence()
247 filterNames = {dataId["physical_filter"] for dataId in physicalFilterDataIds}
248 self.assertGreaterEqual(filterNames, self.data.physical_filters)
250 # Check that the instrument class can be retrieved.
251 registeredInstrument = Instrument.fromName(self.instrument.getName(), registry)
252 self.assertEqual(type(registeredInstrument), type(self.instrument))
254 # Check that re-registration is not an error.
255 self.instrument.register(registry)