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

114 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-11 02:55 -0800

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 

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 

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 

108 @classmethod 

109 def getName(cls): 

110 return "DummyCam" 

111 

112 @classmethod 

113 @lru_cache() # For mypy 

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

115 return cls.dataPackageDir 

116 

117 def getCamera(self): 

118 # Return something that can be indexed by detector number 

119 # but also has to support getIdIter. 

120 filename = pkg_resources.resource_filename("lsst.obs.base", "test/dummycam.yaml") 

121 return makeCamera(filename) 

122 

123 def register(self, registry, update=False): 

124 """Insert Instrument, physical_filter, and detector entries into a 

125 `Registry`. 

126 """ 

127 detector_max = 2 

128 dataId = { 

129 "instrument": self.getName(), 

130 "class_name": get_full_type_name(DummyCam), 

131 "detector_max": detector_max, 

132 "visit_max": 1_000_000, 

133 "exposure_max": 1_000_000, 

134 } 

135 with registry.transaction(): 

136 registry.syncDimensionData("instrument", dataId, update=update) 

137 self._registerFilters(registry, update=update) 

138 for d in range(detector_max): 

139 registry.syncDimensionData( 

140 "detector", 

141 dict( 

142 dataId, 

143 id=d, 

144 full_name=f"RXX_S0{d}", 

145 ), 

146 update=update, 

147 ) 

148 

149 def getRawFormatter(self, dataId): 

150 # Docstring inherited fromt Instrument.getRawFormatter. 

151 return DummyCamYamlWcsFormatter 

152 

153 def applyConfigOverrides(self, name, config): 

154 pass 

155 

156 def writeAdditionalCuratedCalibrations( 

157 self, butler: Butler, collection: Optional[str] = None, labels: Sequence[str] = () 

158 ) -> None: 

159 # We want to test the standard curated calibration ingest 

160 # but we do not have a standard class to use. There is no way 

161 # at the moment to inject a new class into the standard list 

162 # that is a package constant, so instead use this "Additional" 

163 # method but call the standard curated calibration code. 

164 if collection is None: 

165 collection = self.makeCalibrationCollectionName(*labels) 

166 butler.registry.registerCollection(collection, type=CollectionType.CALIBRATION) 

167 

168 datasetType = DatasetType( 

169 "testCalib", 

170 universe=butler.registry.dimensions, 

171 isCalibration=True, 

172 dimensions=("instrument", "detector"), 

173 storageClass="CuratedCalibration", 

174 ) 

175 runs: Set[str] = set() 

176 self._writeSpecificCuratedCalibrationDatasets( 

177 butler, datasetType, collection, runs=runs, labels=labels 

178 ) 

179 

180 

181@dataclasses.dataclass 

182class InstrumentTestData: 

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

184 

185 name: str 

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

187 

188 nDetectors: int 

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

190 

191 firstDetectorName: str 

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

193 

194 physical_filters: Set[str] 

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

196 

197 

198class InstrumentTests(metaclass=abc.ABCMeta): 

199 """Tests of sublcasses of Instrument. 

200 

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

202 ``data`` and ``instrument``. 

203 """ 

204 

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

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

207 

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

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

210 

211 def test_name(self): 

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

213 

214 def test_getCamera(self): 

215 """Test that getCamera() returns a reasonable Camera definition.""" 

216 camera = self.instrument.getCamera() 

217 self.assertEqual(camera.getName(), self.instrument.getName()) 

218 self.assertEqual(len(camera), self.data.nDetectors) 

219 self.assertEqual(next(iter(camera)).getName(), self.data.firstDetectorName) 

220 

221 def test_register(self): 

222 """Test that register() sets appropriate Dimensions.""" 

223 registryConfig = RegistryConfig() 

224 registryConfig["db"] = "sqlite://" 

225 registry = Registry.createFromConfig(registryConfig) 

226 # Check that the registry starts out empty. 

227 self.assertFalse(registry.queryDataIds(["instrument"]).toSequence()) 

228 self.assertFalse(registry.queryDataIds(["detector"]).toSequence()) 

229 self.assertFalse(registry.queryDataIds(["physical_filter"]).toSequence()) 

230 

231 # Register the instrument and check that certain dimensions appear. 

232 self.instrument.register(registry) 

233 instrumentDataIds = registry.queryDataIds(["instrument"]).toSequence() 

234 self.assertEqual(len(instrumentDataIds), 1) 

235 instrumentNames = {dataId["instrument"] for dataId in instrumentDataIds} 

236 self.assertEqual(instrumentNames, {self.data.name}) 

237 detectorDataIds = registry.queryDataIds(["detector"]).expanded().toSequence() 

238 self.assertEqual(len(detectorDataIds), self.data.nDetectors) 

239 detectorNames = {dataId.records["detector"].full_name for dataId in detectorDataIds} 

240 self.assertIn(self.data.firstDetectorName, detectorNames) 

241 physicalFilterDataIds = registry.queryDataIds(["physical_filter"]).toSequence() 

242 filterNames = {dataId["physical_filter"] for dataId in physicalFilterDataIds} 

243 self.assertGreaterEqual(filterNames, self.data.physical_filters) 

244 

245 # Check that the instrument class can be retrieved. 

246 registeredInstrument = Instrument.fromName(self.instrument.getName(), registry) 

247 self.assertEqual(type(registeredInstrument), type(self.instrument)) 

248 

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

250 self.instrument.register(registry)