Coverage for python/lsst/summit/utils/bestEffort.py: 16%

90 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-19 03:45 -0800

1# This file is part of summit_utils. 

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 <https://www.gnu.org/licenses/>. 

21 

22from sqlite3 import OperationalError 

23 

24import logging 

25from lsst.ip.isr import IsrTask 

26import lsst.daf.butler as dafButler 

27from lsst.daf.butler.registry import ConflictingDefinitionError 

28 

29from lsst.summit.utils.quickLook import QuickLookIsrTask 

30from lsst.summit.utils.butlerUtils import getLatissDefaultCollections 

31 

32# TODO: add attempt for fringe once registry & templates are fixed 

33 

34CURRENT_RUN = "LATISS/runs/quickLook/1" 

35ALLOWED_REPOS = ['/repo/main', '/repo/LATISS', '/readonly/repo/main'] 

36 

37 

38class BestEffortIsr(): 

39 """Class for getting an assembled image with the maximum amount of isr. 

40 

41 BestEffortIsr.getExposure(dataId) returns an assembled image with as much 

42 isr performed as possible, dictated by the calibration products available, 

43 and optionally interpolates over cosmic rays. If an image image already 

44 exists in the butler, it is returned (for the sake of speed), otherwise it 

45 is generated and put(). Calibration products are loaded and cached to 

46 improve performance. 

47 

48 This class uses the ``quickLookIsrTask``, see docs there for details. 

49 

50 Acceptable repodir values are currently listed in ALLOWED_REPOS. This will 

51 be updated (removed) once DM-33849 is done. 

52 

53 defaultExtraIsrOptions is a dict of options applied to all images. 

54 

55 Parameters 

56 ---------- 

57 repoDir : `str` 

58 The repo root. Will be removed after DM-33849. 

59 extraCollections : `list` of `str`, optional 

60 Extra collections to add to the butler init. Collections are prepended. 

61 defaultExtraIsrOptions : `dict`, optional 

62 A dict of extra isr config options to apply. Each key should be an 

63 attribute of an isrTaskConfigClass. 

64 doRepairCosmics : `bool`, optional 

65 Repair cosmic ray hits? 

66 doWrite : `bool`, optional 

67 Write the outputs to the quickLook rerun/collection? 

68 

69 Raises 

70 ------ 

71 FileNotFoundError: 

72 Raised when a butler cannot be automatically instantiated using 

73 the DAF_BUTLER_REPOSITORY_INDEX environment variable. 

74 """ 

75 _datasetName = 'quickLookExp' 

76 

77 def __init__(self, *, 

78 extraCollections=[], 

79 defaultExtraIsrOptions={}, 

80 doRepairCosmics=True, 

81 doWrite=True, 

82 embargo=False): 

83 self.log = logging.getLogger(__name__) 

84 

85 collections = getLatissDefaultCollections() 

86 self.collections = extraCollections + collections 

87 self.log.info(f'Instantiating butler with collections={self.collections}') 

88 try: 

89 repoString = "LATISS" if not embargo else "/repo/embargo" 

90 self.butler = dafButler.Butler(repoString, collections=self.collections, 

91 instrument='LATISS', 

92 run=CURRENT_RUN if doWrite else None) 

93 except(FileNotFoundError, RuntimeError): 

94 # Depending on the value of DAF_BUTLER_REPOSITORY_INDEX and whether 

95 # it is present and blank, or just not set, both these exception 

96 # types can be raised, see 

97 # tests/test_butlerUtils.py:ButlerInitTestCase 

98 # for details and tests which confirm these have not changed 

99 raise FileNotFoundError # unify exception type 

100 

101 quickLookIsrConfig = QuickLookIsrTask.ConfigClass() 

102 quickLookIsrConfig.doRepairCosmics = doRepairCosmics 

103 self.doWrite = doWrite # the task, as run by run() method, can't do the write, so we handle in here 

104 self.quickLookIsrTask = QuickLookIsrTask(config=quickLookIsrConfig) 

105 

106 self.defaultExtraIsrOptions = defaultExtraIsrOptions 

107 

108 self._cache = {} 

109 

110 def _applyConfigOverrides(self, config, overrides): 

111 """Update a config class with a dict of options. 

112 

113 Parameters 

114 ---------- 

115 config : `lsst.pex.config.Config` 

116 The config class to update. 

117 overrides : `dict` 

118 The override options as a dict. 

119 

120 Raises 

121 ------ 

122 ValueError 

123 Raised if the override option isn't found in the config. 

124 """ 

125 for option, value in overrides.items(): 

126 if hasattr(config, option): 

127 setattr(config, option, value) 

128 self.log.info(f"Set isr config override {option} to {value}") 

129 else: 

130 raise ValueError(f"Override option {option} not found in isrConfig") 

131 

132 @staticmethod 

133 def _parseExpIdOrDataId(expIdOrDataId, **kwargs): 

134 """Sanitize the expIdOrDataId to allow support both expIds and dataIds 

135 

136 Supports expId as an integer, or a complete or partial dict. The dict 

137 is updated with the supplied kwargs. 

138 

139 Parameters 

140 ---------- 

141 expIdOrDataId : `int` or `dict 

142 The exposure id as an int or the dataId as as dict, or an expRecord 

143 or a dataCoordinate. 

144 

145 Returns 

146 ------- 

147 dataId : `dict` 

148 The sanitized dataId. 

149 """ 

150 match expIdOrDataId: 

151 case int() as expId: 

152 return {"expId": expId} 

153 case dafButler.DataCoordinate() as dataId: 

154 return dataId 

155 case dafButler.DimensionRecord() as record: 

156 return record.dataId 

157 case dict() as dataId: 

158 return dataId 

159 raise RuntimeError(f"Invalid expId or dataId type {expIdOrDataId}: {type(expIdOrDataId)}") 

160 

161 def clearCache(self): 

162 """Clear the internal cache of loaded calibration products. 

163 

164 Only necessary if you want to use an existing bestEffortIsr object 

165 after adding new calibration products to the calibration collection. 

166 """ 

167 self._cache = {} 

168 

169 def getExposure(self, expIdOrDataId, extraIsrOptions={}, skipCosmics=False, forceRemake=False, 

170 **kwargs): 

171 """Get the postIsr and cosmic-repaired image for this dataId. 

172 

173 Note that when using the forceRemake option the image will not be 

174 written to the repo for reuse. 

175 

176 Parameters 

177 ---------- 

178 expIdOrDataId : `dict` 

179 The dataId 

180 extraIsrOptions : `dict`, optional 

181 extraIsrOptions is a dict of extra isr options applied to this 

182 image only. 

183 skipCosmics : `bool`, optional # XXX THIS CURRENTLY DOESN'T WORK! 

184 Skip doing cosmic ray repair for this image? 

185 forceRemake : `bool` 

186 Remake the exposure even if there is a pre-existing one in the 

187 repo. Images that are force-remade are never written, as this is 

188 assumed to be used for testing/debug purposes, as opposed to normal 

189 operation. For updating individual images, removal from the 

190 registry can be used, and for bulk-updates the overall run number 

191 can be incremented. 

192 

193 Returns 

194 ------- 

195 exp : `lsst.afw.image.Exposure` 

196 The postIsr exposure 

197 """ 

198 dataId = self._parseExpIdOrDataId(expIdOrDataId) 

199 

200 if not forceRemake: 

201 try: 

202 exp = self.butler.get(self._datasetName, dataId, **kwargs) 

203 self.log.info("Found a ready-made quickLookExp in the repo. Returning that.") 

204 return exp 

205 except LookupError: 

206 pass 

207 

208 try: 

209 raw = self.butler.get('raw', dataId, **kwargs) 

210 except LookupError: 

211 raise RuntimeError(f"Failed to retrieve raw for exp {dataId}") from None 

212 

213 # default options that are probably good for most engineering time 

214 isrConfig = IsrTask.ConfigClass() 

215 isrConfig.doWrite = False # this task writes separately, no need for this 

216 isrConfig.doSaturation = True # saturation very important for roundness measurement in qfm 

217 isrConfig.doSaturationInterpolation = True 

218 isrConfig.overscan.leadingColumnsToSkip = 5 

219 isrConfig.overscan.fitType = 'MEDIAN_PER_ROW' 

220 

221 # apply general overrides 

222 self._applyConfigOverrides(isrConfig, self.defaultExtraIsrOptions) 

223 # apply per-image overrides 

224 self._applyConfigOverrides(isrConfig, extraIsrOptions) 

225 

226 isrParts = ['camera', 'bias', 'dark', 'flat', 'defects', 'linearizer', 'crosstalk', 'bfKernel', 

227 'bfGains', 'ptc'] 

228 

229 isrDict = {} 

230 # we build a cache of all the isr components which will be used to save 

231 # the IO time on subsequent calls. This assumes people will not update 

232 # calibration products while this object lives, but this is a fringe 

233 # use case, and if they do, all they would need to do would be call 

234 # .clearCache() and this will rebuild with the new products. 

235 for component in isrParts: 

236 if component in self._cache and component != 'flat': 

237 self.log.info(f"Using {component} from cache...") 

238 isrDict[component] = self._cache[component] 

239 continue 

240 try: 

241 # TODO: add caching for flats 

242 item = self.butler.get(component, dataId=dataId) 

243 self._cache[component] = item 

244 isrDict[component] = self._cache[component] 

245 except (RuntimeError, LookupError, OperationalError): 

246 pass 

247 

248 quickLookExp = self.quickLookIsrTask.run(raw, **isrDict, isrBaseConfig=isrConfig).outputExposure 

249 

250 if self.doWrite and not forceRemake: 

251 try: 

252 self.butler.put(quickLookExp, self._datasetName, dataId, **kwargs) 

253 self.log.info(f'Put {self._datasetName} for {dataId}') 

254 except ConflictingDefinitionError: 

255 # TODO: DM-34302 fix this message so that it's less scary for 

256 # users. Do this by having daemons know they're daemons. 

257 self.log.warning('Skipped putting existing exp into collection! (ignore if there was a race)') 

258 pass 

259 

260 return quickLookExp