Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of jointcal. 

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 

22import copy 

23import os 

24import inspect 

25 

26import lsst.afw.image.utils 

27import lsst.obs.base 

28import lsst.geom 

29 

30from lsst.jointcal import jointcal, utils 

31 

32 

33class JointcalTestBase: 

34 """ 

35 Base class for jointcal tests, to genericize some test running and setup. 

36 

37 Derive from this first, then from TestCase. 

38 """ 

39 

40 def setUp_base(self, center, radius, 

41 match_radius=0.1*lsst.geom.arcseconds, 

42 input_dir="", 

43 all_visits=None, 

44 other_args=None, 

45 do_plot=False, 

46 log_level=None): 

47 """ 

48 Call from your child classes's setUp() to get the necessary variables built. 

49 

50 Parameters 

51 ---------- 

52 center : lsst.geom.SpherePoint 

53 Center of the reference catalog. 

54 radius : lsst.geom.Angle 

55 Radius from center to load reference catalog objects inside. 

56 match_radius : lsst.geom.Angle 

57 matching radius when calculating RMS of result. 

58 input_dir : str 

59 Directory of input butler repository. 

60 all_visits : list 

61 List of the available visits to generate the parseAndRun arguments. 

62 other_args : list 

63 Optional other arguments for the butler dataId. 

64 do_plot : bool 

65 Set to True for a comparison plot and some diagnostic numbers. 

66 log_level : str 

67 Set to the default log level you want jointcal to produce while the 

68 tests are running. See the developer docs about logging for valid 

69 levels: https://developer.lsst.io/coding/logging.html 

70 """ 

71 self.center = center 

72 self.radius = radius 

73 self.jointcalStatistics = utils.JointcalStatistics(match_radius, verbose=True) 

74 self.input_dir = input_dir 

75 self.all_visits = all_visits 

76 if other_args is None: 

77 other_args = [] 

78 self.other_args = other_args 

79 self.do_plot = do_plot 

80 self.log_level = log_level 

81 # Signal/Noise (flux/fluxErr) for sources to be included in the RMS cross-match. 

82 # 100 is a balance between good centroids and enough sources. 

83 self.flux_limit = 100 

84 

85 # Individual tests may want to tweak the config that is passed to parseAndRun(). 

86 self.config = None 

87 self.configfiles = [] 

88 

89 # Append `msg` arguments to assert failures. 

90 self.longMessage = True 

91 

92 # Ensure that the filter list is reset for each test so that we avoid 

93 # confusion or contamination from other instruments. 

94 lsst.obs.base.FilterDefinitionCollection.reset() 

95 

96 def tearDown(self): 

97 if getattr(self, 'reference', None) is not None: 

98 del self.reference 

99 if getattr(self, 'oldWcsList', None) is not None: 

100 del self.oldWcsList 

101 if getattr(self, 'jointcalTask', None) is not None: 

102 del self.jointcalTask 

103 if getattr(self, 'jointcalStatistics', None) is not None: 

104 del self.jointcalStatistics 

105 if getattr(self, 'config', None) is not None: 

106 del self.config 

107 

108 def _testJointcalTask(self, nCatalogs, dist_rms_relative, dist_rms_absolute, pa1, 

109 metrics=None): 

110 """ 

111 Test parseAndRun for jointcal on nCatalogs. 

112 

113 Checks relative and absolute astrometric error (arcsec) and photometric 

114 repeatability (PA1 from the SRD). 

115 

116 Parameters 

117 ---------- 

118 nCatalogs : int 

119 Number of catalogs to run jointcal on. Used to construct the "id" 

120 field for parseAndRun. 

121 dist_rms_relative : astropy.Quantity 

122 Minimum relative astrometric rms post-jointcal to pass the test. 

123 dist_rms_absolute : astropy.Quantity 

124 Minimum absolute astrometric rms post-jointcal to pass the test. 

125 pa1 : float 

126 Minimum PA1 (from Table 14 of the Science Requirements Document: 

127 https://ls.st/LPM-17) post-jointcal to pass the test. 

128 metrics : dict, optional 

129 Dictionary of 'metricName': value to test jointcal's result.metrics 

130 against. 

131 

132 Returns 

133 ------- 

134 list of lsst.daf.persistence.ButlerDataRef 

135 The dataRefs that were processed. 

136 """ 

137 

138 # the calling method is one step back on the stack: use it to specify the output repo. 

139 caller = inspect.stack()[1].function 

140 

141 result = self._runJointcalTask(nCatalogs, caller, metrics=metrics) 

142 

143 data_refs = result.resultList[0].result.dataRefs 

144 oldWcsList = result.resultList[0].result.oldWcsList 

145 

146 defaultFilter = result.resultList[0].result.defaultFilter 

147 

148 def compute_statistics(refObjLoader): 

149 refCat = refObjLoader.loadSkyCircle(self.center, self.radius, defaultFilter).refCat 

150 rms_result = self.jointcalStatistics.compute_rms(data_refs, refCat) 

151 # Make plots before testing, if requested, so we still get plots if tests fail. 

152 if self.do_plot: 

153 self._plotJointcalTask(data_refs, oldWcsList, caller) 

154 return rms_result 

155 

156 # we now have different astrometry/photometry refcats, so have to 

157 # do these calculations separately 

158 if self.jointcalStatistics.do_astrometry: 

159 refObjLoader = result.resultList[0].result.astrometryRefObjLoader 

160 # preserve do_photometry for the next `if` 

161 temp = copy.copy(self.jointcalStatistics.do_photometry) 

162 self.jointcalStatistics.do_photometry = False 

163 rms_result = compute_statistics(refObjLoader) 

164 self.jointcalStatistics.do_photometry = temp # restore do_photometry 

165 

166 if dist_rms_relative is not None and dist_rms_absolute is not None: 

167 self.assertLess(rms_result.dist_relative, dist_rms_relative) 

168 self.assertLess(rms_result.dist_absolute, dist_rms_absolute) 

169 

170 if self.jointcalStatistics.do_photometry: 

171 refObjLoader = result.resultList[0].result.photometryRefObjLoader 

172 self.jointcalStatistics.do_astrometry = False 

173 rms_result = compute_statistics(refObjLoader) 

174 

175 if pa1 is not None: 

176 self.assertLess(rms_result.pa1, pa1) 

177 

178 return data_refs 

179 

180 def _runJointcalTask(self, nCatalogs, caller, metrics=None): 

181 """ 

182 Run jointcalTask on nCatalogs, with the most basic tests. 

183 Tests for non-empty result list, and that the basic metrics are correct. 

184 

185 Parameters 

186 ---------- 

187 nCatalogs : int 

188 Number of catalogs to test on. 

189 caller : str 

190 Name of the calling function (to determine output directory). 

191 metrics : dict, optional 

192 Dictionary of 'metricName': value to test jointcal's result.metrics 

193 against. 

194 

195 Returns 

196 ------- 

197 pipe.base.Struct 

198 The structure returned by jointcalTask.run() 

199 """ 

200 visits = '^'.join(str(v) for v in self.all_visits[:nCatalogs]) 

201 output_dir = os.path.join('.test', self.__class__.__name__, caller) 

202 if self.log_level is not None: 

203 self.other_args.extend(['--loglevel', 'jointcal=%s'%self.log_level]) 

204 

205 # Place default configfile first so that specific subclass configfiles are applied after 

206 test_config = os.path.join(lsst.utils.getPackageDir('jointcal'), 'tests/config/config.py') 

207 self.configfiles = [test_config] + self.configfiles 

208 

209 args = [self.input_dir, '--output', output_dir, 

210 '--clobber-versions', '--clobber-config', 

211 '--doraise', '--configfile', *self.configfiles, 

212 '--id', 'visit=%s'%visits] 

213 args.extend(self.other_args) 

214 result = jointcal.JointcalTask.parseAndRun(args=args, doReturnResults=True, config=self.config) 

215 self.assertNotEqual(result.resultList, [], 'resultList should not be empty') 

216 self.assertEqual(result.resultList[0].exitStatus, 0) 

217 job = result.resultList[0].result.job 

218 self._test_metrics(job.measurements, metrics) 

219 

220 return result 

221 

222 def _plotJointcalTask(self, data_refs, oldWcsList, caller): 

223 """ 

224 Plot the results of a jointcal run. 

225 

226 Parameters 

227 ---------- 

228 data_refs : list of lsst.daf.persistence.ButlerDataRef 

229 The dataRefs that were processed. 

230 oldWcsList : list of lsst.afw.image.Wcs 

231 The original WCS from each dataRef. 

232 caller : str 

233 Name of the calling function (to determine output directory). 

234 """ 

235 plot_dir = os.path.join('.test', self.__class__.__name__, 'plots') 

236 if not os.path.isdir(plot_dir): 

237 os.mkdir(plot_dir) 

238 self.jointcalStatistics.make_plots(data_refs, oldWcsList, name=caller, outdir=plot_dir) 

239 print("Plots saved to: {}".format(plot_dir)) 

240 

241 def _test_metrics(self, result, expect): 

242 """Test a dictionary of "metrics" against those returned by jointcal.py 

243 

244 Parameters 

245 ---------- 

246 result : dict 

247 Result metric dictionary from jointcal.py 

248 expect : dict 

249 Expected metric dictionary; set a value to None to not test it. 

250 """ 

251 for key in result: 

252 if expect[key.metric] is not None: 

253 value = result[key].quantity.value 

254 if isinstance(value, float): 

255 self.assertFloatsAlmostEqual(value, expect[key.metric], msg=key.metric, rtol=1e-5) 

256 else: 

257 self.assertEqual(value, expect[key.metric], msg=key.metric)