Coverage for tests/jointcalTestBase.py : 11%

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/>.
22import copy
23import os
24import inspect
26import lsst.afw.image.utils
27import lsst.obs.base
28import lsst.geom
30from lsst.jointcal import jointcal, utils
33class JointcalTestBase:
34 """
35 Base class for jointcal tests, to genericize some test running and setup.
37 Derive from this first, then from TestCase.
38 """
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.
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
85 # Individual tests may want to tweak the config that is passed to parseAndRun().
86 self.config = None
87 self.configfiles = []
89 # Append `msg` arguments to assert failures.
90 self.longMessage = True
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()
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
108 def _testJointcalTask(self, nCatalogs, dist_rms_relative, dist_rms_absolute, pa1,
109 metrics=None):
110 """
111 Test parseAndRun for jointcal on nCatalogs.
113 Checks relative and absolute astrometric error (arcsec) and photometric
114 repeatability (PA1 from the SRD).
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.
132 Returns
133 -------
134 list of lsst.daf.persistence.ButlerDataRef
135 The dataRefs that were processed.
136 """
138 # the calling method is one step back on the stack: use it to specify the output repo.
139 caller = inspect.stack()[1].function
141 result = self._runJointcalTask(nCatalogs, caller, metrics=metrics)
143 data_refs = result.resultList[0].result.dataRefs
144 oldWcsList = result.resultList[0].result.oldWcsList
146 defaultFilter = result.resultList[0].result.defaultFilter
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
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
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)
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)
175 if pa1 is not None:
176 self.assertLess(rms_result.pa1, pa1)
178 return data_refs
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.
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.
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])
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
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)
220 return result
222 def _plotJointcalTask(self, data_refs, oldWcsList, caller):
223 """
224 Plot the results of a jointcal run.
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))
241 def _test_metrics(self, result, expect):
242 """Test a dictionary of "metrics" against those returned by jointcal.py
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)