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

85 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-02-05 18:01 -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 

30import pkg_resources 

31from typing import Set 

32 

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

34from lsst.obs.base.gen2to3 import TranslatorFactory 

35from lsst.obs.base.yamlCamera import makeCamera 

36from lsst.daf.butler import Registry 

37from lsst.daf.butler import RegistryConfig 

38from lsst.daf.butler.core.utils import getFullTypeName 

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

40 

41from .utils import createInitialSkyWcsFromBoresight 

42 

43DUMMY_FILTER_DEFINITIONS = FilterDefinitionCollection( 

44 FilterDefinition(physical_filter="dummy_u", band="u", lambdaEff=0), 

45 FilterDefinition(physical_filter="dummy_g", band="g", lambdaEff=0), 

46) 

47 

48 

49class DummyCamYamlWcsFormatter(YamlFormatter): 

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

51 

52 @classmethod 

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

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

55 

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

57 can be found in `FitsRawFormatterBase`. 

58 

59 Notes 

60 ----- 

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

62 """ 

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

64 

65 

66class DummyCam(Instrument): 

67 

68 filterDefinitions = DUMMY_FILTER_DEFINITIONS 

69 

70 @classmethod 

71 def getName(cls): 

72 return "DummyCam" 

73 

74 def getCamera(self): 

75 # Return something that can be indexed by detector number 

76 # but also has to support getIdIter. 

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

78 return makeCamera(filename) 

79 

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

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

82 `Registry`. 

83 """ 

84 detector_max = 2 

85 dataId = {"instrument": self.getName(), "class_name": getFullTypeName(DummyCam), 

86 "detector_max": detector_max} 

87 with registry.transaction(): 

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

89 self._registerFilters(registry, update=update) 

90 for d in range(detector_max): 

91 registry.syncDimensionData("detector", 

92 dict(dataId, id=d, full_name=f"RXX_S0{d}",), 

93 update=update) 

94 

95 def getRawFormatter(self, dataId): 

96 # Docstring inherited fromt Instrument.getRawFormatter. 

97 return DummyCamYamlWcsFormatter 

98 

99 def writeCuratedCalibrations(self, butler): 

100 pass 

101 

102 def applyConfigOverrides(self, name, config): 

103 pass 

104 

105 def makeDataIdTranslatorFactory(self) -> TranslatorFactory: 

106 return TranslatorFactory() 

107 

108 

109@dataclasses.dataclass 

110class InstrumentTestData: 

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

112 """ 

113 

114 name: str 

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

116 

117 nDetectors: int 

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

119 

120 firstDetectorName: str 

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

122 

123 physical_filters: Set[str] 

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

125 

126 

127class InstrumentTests(metaclass=abc.ABCMeta): 

128 """Tests of sublcasses of Instrument. 

129 

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

131 ``data`` and ``instrument``. 

132 """ 

133 

134 data = None 

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

136 

137 instrument = None 

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

139 

140 def test_name(self): 

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

142 

143 def test_getCamera(self): 

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

145 """ 

146 camera = self.instrument.getCamera() 

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

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

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

150 

151 def test_register(self): 

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

153 """ 

154 registryConfig = RegistryConfig() 

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

156 registry = Registry.createFromConfig(registryConfig) 

157 # Check that the registry starts out empty. 

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

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

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

161 

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

163 self.instrument.register(registry) 

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

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

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

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

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

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

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

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

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

173 filterNames = {dataId['physical_filter'] for dataId in physicalFilterDataIds} 

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

175 

176 # Check that the instrument class can be retrieved. 

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

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

179 

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

181 self.instrument.register(registry) 

182 

183 def testMakeTranslatorFactory(self): 

184 factory = self.instrument.makeDataIdTranslatorFactory() 

185 self.assertIsInstance(factory, TranslatorFactory) 

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