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 os 

23import inspect 

24 

25import lsst.afw.image.utils 

26import lsst.obs.base 

27import lsst.geom 

28 

29from lsst.jointcal import jointcal, utils 

30 

31 

32class JointcalTestBase: 

33 """ 

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

35 

36 Derive from this first, then from TestCase. 

37 """ 

38 

39 def setUp_base(self, center, radius, 

40 match_radius=0.1*lsst.geom.arcseconds, 

41 input_dir="", 

42 all_visits=None, 

43 other_args=None, 

44 do_plot=False, 

45 log_level=None): 

46 """ 

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

48 

49 Parameters 

50 ---------- 

51 center : lsst.geom.SpherePoint 

52 Center of the reference catalog. 

53 radius : lsst.geom.Angle 

54 Radius from center to load reference catalog objects inside. 

55 match_radius : lsst.geom.Angle 

56 matching radius when calculating RMS of result. 

57 input_dir : str 

58 Directory of input butler repository. 

59 all_visits : list 

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

61 other_args : list 

62 Optional other arguments for the butler dataId. 

63 do_plot : bool 

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

65 log_level : str 

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

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

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

69 """ 

70 self.center = center 

71 self.radius = radius 

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

73 self.input_dir = input_dir 

74 self.all_visits = all_visits 

75 if other_args is None: 

76 other_args = [] 

77 self.other_args = other_args 

78 self.do_plot = do_plot 

79 self.log_level = log_level 

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

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

82 self.flux_limit = 100 

83 

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

85 self.config = None 

86 self.configfiles = [] 

87 

88 # Append `msg` arguments to assert failures. 

89 self.longMessage = True 

90 

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

92 # confusion or contamination from other instruments. 

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

94 

95 def tearDown(self): 

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

97 del self.reference 

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

99 del self.oldWcsList 

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

101 del self.jointcalTask 

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

103 del self.jointcalStatistics 

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

105 del self.config 

106 

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

108 metrics=None): 

109 """ 

110 Test parseAndRun for jointcal on nCatalogs. 

111 

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

113 repeatability (PA1 from the SRD). 

114 

115 Parameters 

116 ---------- 

117 nCatalogs : int 

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

119 field for parseAndRun. 

120 dist_rms_relative : astropy.Quantity 

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

122 dist_rms_absolute : astropy.Quantity 

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

124 pa1 : float 

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

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

127 metrics : dict, optional 

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

129 against. 

130 

131 Returns 

132 ------- 

133 list of lsst.daf.persistence.ButlerDataRef 

134 The dataRefs that were processed. 

135 """ 

136 

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

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

139 

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

141 

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

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

144 

145 # extract a reference catalog to compute statistics against 

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

147 # Not all tests do astrometry, so might not have the above defined. 

148 if refObjLoader is None: 

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

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

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

152 

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

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

155 if self.do_plot: 

156 self._plotJointcalTask(data_refs, oldWcsList, caller) 

157 

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

159 self.assertLess(rms_result.dist_relative, dist_rms_relative) 

160 self.assertLess(rms_result.dist_absolute, dist_rms_absolute) 

161 if pa1 is not None: 

162 self.assertLess(rms_result.pa1, pa1) 

163 

164 return data_refs 

165 

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

167 """ 

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

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

170 

171 Parameters 

172 ---------- 

173 nCatalogs : int 

174 Number of catalogs to test on. 

175 caller : str 

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

177 metrics : dict, optional 

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

179 against. 

180 

181 Returns 

182 ------- 

183 pipe.base.Struct 

184 The structure returned by jointcalTask.run() 

185 """ 

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

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

188 if self.log_level is not None: 

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

190 

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

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

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

194 

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

196 '--clobber-versions', '--clobber-config', 

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

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

199 args.extend(self.other_args) 

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

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

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

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

204 self._test_metrics(job.measurements, metrics) 

205 

206 return result 

207 

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

209 """ 

210 Plot the results of a jointcal run. 

211 

212 Parameters 

213 ---------- 

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

215 The dataRefs that were processed. 

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

217 The original WCS from each dataRef. 

218 caller : str 

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

220 """ 

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

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

223 os.mkdir(plot_dir) 

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

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

226 

227 def _test_metrics(self, result, expect): 

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

229 

230 Parameters 

231 ---------- 

232 result : dict 

233 Result metric dictionary from jointcal.py 

234 expect : dict 

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

236 """ 

237 for key in result: 

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

239 value = result[key].quantity.value 

240 if isinstance(value, float): 

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

242 else: 

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