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

89 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-08 14:44 -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 

28import abc 

29import dataclasses 

30from typing import ClassVar, Optional, Set 

31 

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 

39 

40from .utils import createInitialSkyWcsFromBoresight 

41 

42DUMMY_FILTER_DEFINITIONS = FilterDefinitionCollection( 

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

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

45) 

46 

47 

48class DummyCamYamlWcsFormatter(YamlFormatter): 

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

50 

51 @classmethod 

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

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

54 

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

56 can be found in `FitsRawFormatterBase`. 

57 

58 Notes 

59 ----- 

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

61 """ 

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

63 

64 

65class DummyCam(Instrument): 

66 

67 filterDefinitions = DUMMY_FILTER_DEFINITIONS 

68 

69 @classmethod 

70 def getName(cls): 

71 return "DummyCam" 

72 

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) 

78 

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 ) 

102 

103 def getRawFormatter(self, dataId): 

104 # Docstring inherited fromt Instrument.getRawFormatter. 

105 return DummyCamYamlWcsFormatter 

106 

107 def writeCuratedCalibrations(self, butler): 

108 pass 

109 

110 def applyConfigOverrides(self, name, config): 

111 pass 

112 

113 def makeDataIdTranslatorFactory(self) -> TranslatorFactory: 

114 return TranslatorFactory() 

115 

116 

117@dataclasses.dataclass 

118class InstrumentTestData: 

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

120 

121 name: str 

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

123 

124 nDetectors: int 

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

126 

127 firstDetectorName: str 

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

129 

130 physical_filters: Set[str] 

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

132 

133 

134class InstrumentTests(metaclass=abc.ABCMeta): 

135 """Tests of sublcasses of Instrument. 

136 

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

138 ``data`` and ``instrument``. 

139 """ 

140 

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

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

143 

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

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

146 

147 def test_name(self): 

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

149 

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) 

156 

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

166 

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) 

180 

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

184 

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

186 self.instrument.register(registry) 

187 

188 def testMakeTranslatorFactory(self): 

189 factory = self.instrument.makeDataIdTranslatorFactory() 

190 self.assertIsInstance(factory, TranslatorFactory) 

191 str(factory) # Just make sure this doesn't raise.