Coverage for python/lsst/obs/base/instrument_tests.py: 52%

115 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-14 20:02 +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/>. 

21 

22"""Helpers for writing tests against subclassses of Instrument. 

23 

24These are not tests themselves, but can be subclassed (plus unittest.TestCase) 

25to get a functional test of an Instrument. 

26""" 

27 

28from __future__ import annotations 

29 

30__all__ = [ 

31 "DummyCam", 

32 "InstrumentTestData", 

33 "InstrumentTests", 

34 "DummyCamYamlWcsFormatter", 

35 "CuratedCalibration", 

36] 

37 

38import abc 

39import dataclasses 

40import json 

41from functools import lru_cache 

42from typing import TYPE_CHECKING, Any, ClassVar, Optional, Sequence, Set 

43 

44from lsst.daf.butler import CollectionType, DatasetType, Registry, RegistryConfig 

45from lsst.daf.butler.formatters.yaml import YamlFormatter 

46from lsst.obs.base import FilterDefinition, FilterDefinitionCollection, Instrument 

47from lsst.obs.base.yamlCamera import makeCamera 

48from lsst.resources import ResourcePath 

49from lsst.utils.introspection import get_full_type_name 

50from pydantic import BaseModel 

51 

52from .utils import createInitialSkyWcsFromBoresight 

53 

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 

56 

57DUMMY_FILTER_DEFINITIONS = FilterDefinitionCollection( 

58 FilterDefinition(physical_filter="dummy_u", band="u"), 

59 FilterDefinition(physical_filter="dummy_g", band="g"), 

60) 

61 

62 

63class DummyCamYamlWcsFormatter(YamlFormatter): 

64 """Specialist formatter for tests that can make a WCS.""" 

65 

66 @classmethod 

67 def makeRawSkyWcsFromBoresight(cls, boresight, orientation, detector): 

68 """Class method to make a raw sky WCS from boresight and detector. 

69 

70 This uses the API expected by define-visits. A working example 

71 can be found in `FitsRawFormatterBase`. 

72 

73 Notes 

74 ----- 

75 This makes no attempt to create a proper WCS from geometry. 

76 """ 

77 return createInitialSkyWcsFromBoresight(boresight, orientation, detector, flipX=False) 

78 

79 

80class CuratedCalibration(BaseModel): 

81 """Class that implements minimal read/write interface needed to support 

82 curated calibration ingest. 

83 """ 

84 

85 metadata: dict[str, Any] 

86 values: list[int] 

87 

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)) 

93 

94 def writeText(self, path: str) -> None: 

95 with open(path, "w") as f: 

96 print(self.json(), file=f) 

97 

98 def getMetadata(self) -> dict[str, Any]: 

99 return self.metadata 

100 

101 

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 ) 

112 

113 @classmethod 

114 def getName(cls): 

115 return "DummyCam" 

116 

117 @classmethod 

118 @lru_cache() # For mypy 

119 def getObsDataPackageDir(cls) -> Optional[str]: 

120 return cls.dataPackageDir 

121 

122 def getCamera(self): 

123 # Return something that can be indexed by detector number 

124 # but also has to support getIdIter. 

125 with ResourcePath("resource://lsst.obs.base/test/dummycam.yaml").as_local() as local_file: 

126 return makeCamera(local_file.ospath) 

127 

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 ) 

153 

154 def getRawFormatter(self, dataId): 

155 # Docstring inherited fromt Instrument.getRawFormatter. 

156 return DummyCamYamlWcsFormatter 

157 

158 def applyConfigOverrides(self, name, config): 

159 pass 

160 

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) 

172 

173 datasetType = DatasetType( 

174 "testCalib", 

175 universe=butler.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 ) 

184 

185 

186@dataclasses.dataclass 

187class InstrumentTestData: 

188 """Values to test against in subclasses of `InstrumentTests`.""" 

189 

190 name: str 

191 """The name of the Camera this instrument describes.""" 

192 

193 nDetectors: int 

194 """The number of detectors in the Camera.""" 

195 

196 firstDetectorName: str 

197 """The name of the first detector in the Camera.""" 

198 

199 physical_filters: Set[str] 

200 """A subset of the physical filters should be registered.""" 

201 

202 

203class InstrumentTests(metaclass=abc.ABCMeta): 

204 """Tests of sublcasses of Instrument. 

205 

206 TestCase subclasses must derive from this, then `TestCase`, and override 

207 ``data`` and ``instrument``. 

208 """ 

209 

210 data: ClassVar[Optional[InstrumentTestData]] = None 

211 """`InstrumentTestData` containing the values to test against.""" 

212 

213 instrument: ClassVar[Optional[Instrument]] = None 

214 """The `~lsst.obs.base.Instrument` to be tested.""" 

215 

216 def test_name(self): 

217 self.assertEqual(self.instrument.getName(), self.data.name) 

218 

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) 

225 

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()) 

235 

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) 

249 

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)) 

253 

254 # Check that re-registration is not an error. 

255 self.instrument.register(registry)