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