Coverage for tests/fgcmcalTestBase.py : 9%

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# See COPYRIGHT file at the top of the source tree.
2#
3# This file is part of fgcmcal.
4#
5# Developed for the LSST Data Management System.
6# This product includes software developed by the LSST Project
7# (https://www.lsst.org).
8# See the COPYRIGHT file at the top-level directory of this distribution
9# for details of code ownership.
10#
11# This program is free software: you can redistribute it and/or modify
12# it under the terms of the GNU General Public License as published by
13# the Free Software Foundation, either version 3 of the License, or
14# (at your option) any later version.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program. If not, see <https://www.gnu.org/licenses/>.
23"""General fgcmcal testing class.
25This class is used as the basis for individual obs package tests using
26data from testdata_jointcal.
27"""
29import os
30import shutil
31import numpy as np
32import glob
34import lsst.daf.persistence as dafPersist
35import lsst.geom as geom
36import lsst.log
37from lsst.meas.algorithms import LoadIndexedReferenceObjectsTask, LoadIndexedReferenceObjectsConfig
38from astropy import units
40import lsst.fgcmcal as fgcmcal
43class FgcmcalTestBase(object):
44 """
45 Base class for fgcmcal tests, to genericize some test running and setup.
47 Derive from this first, then from TestCase.
48 """
50 def setUp_base(self, inputDir=None, testDir=None, logLevel=None, otherArgs=[]):
51 """
52 Call from your child class's setUp() to get variables built.
54 Parameters
55 ----------
56 inputDir: `str`, optional
57 Input directory
58 testDir: `str`, optional
59 Test directory
60 logLevel: `str`, optional
61 Override loglevel for command-line tasks
62 otherArgs: `list`, default=[]
63 List of additional arguments to send to command-line tasks
64 """
66 self.inputDir = inputDir
67 self.testDir = testDir
68 self.logLevel = logLevel
69 self.otherArgs = otherArgs
71 self.config = None
72 self.configfiles = []
74 lsst.log.setLevel("daf.persistence.butler", lsst.log.FATAL)
75 lsst.log.setLevel("CameraMapper", lsst.log.FATAL)
77 if self.logLevel is not None:
78 self.otherArgs.extend(['--loglevel', 'fgcmcal=%s'%self.logLevel])
80 def _testFgcmMakeLut(self, nBand, i0Std, i0Recon, i10Std, i10Recon):
81 """
82 Test running of FgcmMakeLutTask
84 Parameters
85 ----------
86 nBand: `int`
87 Number of bands tested
88 i0Std: `np.array', size nBand
89 Values of i0Std to compare to
90 i10Std: `np.array`, size nBand
91 Values of i10Std to compare to
92 i0Recon: `np.array`, size nBand
93 Values of reconstructed i0 to compare to
94 i10Recon: `np.array`, size nBand
95 Values of reconsntructed i10 to compare to
97 Raises
98 ------
99 Exceptions on test failures
100 """
102 args = [self.inputDir, '--output', self.testDir,
103 '--doraise']
104 if len(self.configfiles) > 0:
105 args.extend(['--configfile', *self.configfiles])
106 args.extend(self.otherArgs)
108 result = fgcmcal.FgcmMakeLutTask.parseAndRun(args=args, config=self.config)
109 self._checkResult(result)
111 butler = dafPersist.butler.Butler(self.testDir)
112 tempTask = fgcmcal.FgcmFitCycleTask()
113 lutCat = butler.get('fgcmLookUpTable')
114 fgcmLut, lutIndexVals, lutStd = fgcmcal.utilities.translateFgcmLut(lutCat,
115 dict(tempTask.config.filterMap))
117 # Check that we got the requested number of bands...
118 self.assertEqual(nBand, len(lutIndexVals[0]['FILTERNAMES']))
120 self.assertFloatsAlmostEqual(i0Std, lutStd[0]['I0STD'], msg='I0Std', rtol=1e-5)
121 self.assertFloatsAlmostEqual(i10Std, lutStd[0]['I10STD'], msg='I10Std', rtol=1e-5)
123 indices = fgcmLut.getIndices(np.arange(nBand, dtype=np.int32),
124 np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']),
125 np.zeros(nBand) + lutStd[0]['O3STD'],
126 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']),
127 np.zeros(nBand) + lutStd[0]['ALPHASTD'],
128 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])),
129 np.zeros(nBand, dtype=np.int32),
130 np.zeros(nBand) + lutStd[0]['PMBSTD'])
131 i0 = fgcmLut.computeI0(np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']),
132 np.zeros(nBand) + lutStd[0]['O3STD'],
133 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']),
134 np.zeros(nBand) + lutStd[0]['ALPHASTD'],
135 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])),
136 np.zeros(nBand) + lutStd[0]['PMBSTD'],
137 indices)
139 self.assertFloatsAlmostEqual(i0Recon, i0, msg='i0Recon', rtol=1e-5)
141 i1 = fgcmLut.computeI1(np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']),
142 np.zeros(nBand) + lutStd[0]['O3STD'],
143 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']),
144 np.zeros(nBand) + lutStd[0]['ALPHASTD'],
145 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])),
146 np.zeros(nBand) + lutStd[0]['PMBSTD'],
147 indices)
149 self.assertFloatsAlmostEqual(i10Recon, i1/i0, msg='i10Recon', rtol=1e-5)
151 def _testFgcmBuildStars(self, visits, nStar, nObs):
152 """
153 Test running of FgcmBuildStarsTask
155 Parameters
156 ----------
157 visits: `list`
158 List of visits to calibrate
159 nStar: `int`
160 Number of stars expected
161 nObs: `int`
162 Number of observations of stars expected
164 Raises
165 ------
166 Exceptions on test failures
167 """
169 args = [self.inputDir, '--output', self.testDir,
170 '--id', 'visit='+'^'.join([str(visit) for visit in visits]),
171 '--doraise']
172 if len(self.configfiles) > 0:
173 args.extend(['--configfile', *self.configfiles])
174 args.extend(self.otherArgs)
176 result = fgcmcal.FgcmBuildStarsTask.parseAndRun(args=args, config=self.config)
177 self._checkResult(result)
179 butler = dafPersist.butler.Butler(self.testDir)
181 visitCat = butler.get('fgcmVisitCatalog')
182 self.assertEqual(len(visits), len(visitCat))
184 starIds = butler.get('fgcmStarIds')
185 self.assertEqual(nStar, len(starIds))
187 starObs = butler.get('fgcmStarObservations')
188 self.assertEqual(nObs, len(starObs))
190 def _testFgcmFitCycle(self, nZp, nGoodZp, nOkZp, nBadZp, nStdStars, nPlots, skipChecks=False):
191 """
192 Test running of FgcmFitCycleTask
194 Parameters
195 ----------
196 nZp: `int`
197 Number of zeropoints created by the task
198 nGoodZp: `int`
199 Number of good (photometric) zeropoints created
200 nOkZp: `int`
201 Number of constrained zeropoints (photometric or not)
202 nBadZp: `int`
203 Number of unconstrained (bad) zeropoints
204 nStdStars: `int`
205 Number of standard stars produced
206 nPlots: `int`
207 Number of plots produced
208 skipChecks: `bool`, optional
209 Skip number checks, when running less-than-final cycle.
210 Default is False.
211 """
213 args = [self.inputDir, '--output', self.testDir,
214 '--doraise']
215 if len(self.configfiles) > 0:
216 args.extend(['--configfile', *self.configfiles])
217 args.extend(self.otherArgs)
219 # Move into the test directory so the plots will get cleaned in tearDown
220 # In the future, with Gen3, we will probably have a better way of managing
221 # non-data output such as plots.
222 cwd = os.getcwd()
223 os.chdir(self.testDir)
225 result = fgcmcal.FgcmFitCycleTask.parseAndRun(args=args, config=self.config)
226 self._checkResult(result)
228 # Move back to the previous directory
229 os.chdir(cwd)
231 if skipChecks:
232 return
234 # Check that the expected number of plots are there.
235 plots = glob.glob(os.path.join(self.testDir, self.config.outfileBase +
236 '_cycle%02d_plots/' % (self.config.cycleNumber) +
237 '*.png'))
238 self.assertEqual(len(plots), nPlots)
240 butler = dafPersist.butler.Butler(self.testDir)
242 zps = butler.get('fgcmZeropoints', fgcmcycle=self.config.cycleNumber)
244 # Check the numbers of zeropoints in all, good, okay, and bad
245 self.assertEqual(len(zps), nZp)
247 gd, = np.where(zps['fgcmFlag'] == 1)
248 self.assertEqual(len(gd), nGoodZp)
250 ok, = np.where(zps['fgcmFlag'] < 16)
251 self.assertEqual(len(ok), nOkZp)
253 bd, = np.where(zps['fgcmFlag'] >= 16)
254 self.assertEqual(len(bd), nBadZp)
256 # Check that there are no illegal values with the ok zeropoints
257 test, = np.where(zps['fgcmZpt'][gd] < -9000.0)
258 self.assertEqual(len(test), 0)
260 stds = butler.get('fgcmStandardStars', fgcmcycle=self.config.cycleNumber)
262 self.assertEqual(len(stds), nStdStars)
264 def _testFgcmOutputProducts(self, visitDataRefName, ccdDataRefName, filterMapping,
265 zpOffsets, testVisit, testCcd, testFilter, testBandIndex):
266 """
267 Test running of FgcmOutputProductsTask
269 Parameters
270 ----------
271 visitDataRefName: `str`
272 Name of column in dataRef to get the visit
273 ccdDataRefName: `str`
274 Name of column in dataRef to get the ccd
275 filterMapping: `dict`
276 Mapping of filterName to dataRef filter names
277 zpOffsets: `np.array`
278 Zeropoint offsets expected
279 testVisit: `int`
280 Visit id to check for round-trip computations
281 testCcd: `int`
282 Ccd id to check for round-trip computations
283 testFilter: `str`
284 Filtername for testVisit/testCcd
285 testBandIndex: `int`
286 Band index for testVisit/testCcd
287 """
289 args = [self.inputDir, '--output', self.testDir,
290 '--doraise']
291 if len(self.configfiles) > 0:
292 args.extend(['--configfile', *self.configfiles])
293 args.extend(self.otherArgs)
295 result = fgcmcal.FgcmOutputProductsTask.parseAndRun(args=args, config=self.config,
296 doReturnResults=True)
297 self._checkResult(result)
299 # Extract the offsets from the results
300 offsets = result.resultList[0].results.offsets
302 self.assertFloatsAlmostEqual(offsets, zpOffsets, atol=1e-6)
304 butler = dafPersist.butler.Butler(self.testDir)
306 # Test the reference catalog stars
308 # Read in the raw stars...
309 rawStars = butler.get('fgcmStandardStars', fgcmcycle=self.config.cycleNumber)
311 # Read in the new reference catalog...
312 config = LoadIndexedReferenceObjectsConfig()
313 config.ref_dataset_name = 'fgcm_stars'
314 task = LoadIndexedReferenceObjectsTask(butler, config=config)
316 # Read in a giant radius to get them all
317 refStruct = task.loadSkyCircle(rawStars[0].getCoord(), 5.0*geom.degrees,
318 filterName='r')
320 # Make sure all the stars are there
321 self.assertEqual(len(rawStars), len(refStruct.refCat))
323 # And make sure the numbers are consistent
324 test, = np.where(rawStars['id'][0] == refStruct.refCat['id'])
326 # Perform math on numpy arrays to maintain datatypes
327 mags = rawStars['mag_std_noabs'][:, 0].astype(np.float64) + offsets[0]
328 fluxes = (mags*units.ABmag).to_value(units.nJy)
329 fluxErrs = (np.log(10.)/2.5)*fluxes*rawStars['magErr_std'][:, 0].astype(np.float64)
330 # Only check the first one
331 self.assertFloatsAlmostEqual(fluxes[0], refStruct.refCat['r_flux'][test[0]])
332 self.assertFloatsAlmostEqual(fluxErrs[0], refStruct.refCat['r_fluxErr'][test[0]])
334 # Test the psf candidate counting, ratio should be between 0.0 and 1.0
335 candRatio = (refStruct.refCat['r_nPsfCandidate'].astype(np.float64) /
336 refStruct.refCat['r_nTotal'].astype(np.float64))
337 self.assertFloatsAlmostEqual(candRatio.min(), 0.0)
338 self.assertFloatsAlmostEqual(candRatio.max(), 1.0)
340 # Test the fgcm_photoCalib output
342 zptCat = butler.get('fgcmZeropoints', fgcmcycle=self.config.cycleNumber)
343 selected = (zptCat['fgcmFlag'] < 16)
345 # Read in all the calibrations, these should all be there
346 # This test is simply to ensure that all the photoCalib files exist
347 for rec in zptCat[selected]:
348 testCal = butler.get('fgcm_photoCalib',
349 dataId={visitDataRefName: int(rec['visit']),
350 ccdDataRefName: int(rec['ccd']),
351 'filter': filterMapping[rec['filtername']]})
352 self.assertIsNotNone(testCal)
354 # We do round-trip value checking on just the final one (chosen arbitrarily)
355 testCal = butler.get('fgcm_photoCalib',
356 dataId={visitDataRefName: int(testVisit),
357 ccdDataRefName: int(testCcd),
358 'filter': filterMapping[testFilter]})
359 self.assertIsNotNone(testCal)
361 src = butler.get('src', dataId={visitDataRefName: int(testVisit),
362 ccdDataRefName: int(testCcd)})
364 # Only test sources with positive flux
365 gdSrc = (src['slot_CalibFlux_instFlux'] > 0.0)
367 # We need to apply the calibration offset to the fgcmzpt (which is internal
368 # and doesn't know about that yet)
369 testZpInd, = np.where((zptCat['visit'] == testVisit) &
370 (zptCat['ccd'] == testCcd))
371 fgcmZpt = zptCat['fgcmZpt'][testZpInd] + offsets[testBandIndex]
372 fgcmZptGrayErr = np.sqrt(zptCat['fgcmZptVar'][testZpInd])
374 if self.config.doComposeWcsJacobian:
375 # The raw zeropoint needs to be modified to know about the wcs jacobian
376 camera = butler.get('camera')
377 approxPixelAreaFields = fgcmcal.utilities.computeApproxPixelAreaFields(camera)
378 center = approxPixelAreaFields[testCcd].getBBox().getCenter()
379 pixAreaCorr = approxPixelAreaFields[testCcd].evaluate(center)
380 fgcmZpt += -2.5*np.log10(pixAreaCorr)
382 # This is the magnitude through the mean calibration
383 photoCalMeanCalMags = np.zeros(gdSrc.sum())
384 # This is the magnitude through the full focal-plane variable mags
385 photoCalMags = np.zeros_like(photoCalMeanCalMags)
386 # This is the magnitude with the FGCM (central-ccd) zeropoint
387 zptMeanCalMags = np.zeros_like(photoCalMeanCalMags)
389 for i, rec in enumerate(src[gdSrc]):
390 photoCalMeanCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'])
391 photoCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'],
392 rec.getCentroid())
393 zptMeanCalMags[i] = fgcmZpt - 2.5*np.log10(rec['slot_CalibFlux_instFlux'])
395 # These should be very close but some tiny differences because the fgcm value
396 # is defined at the center of the bbox, and the photoCal is the mean over the box
397 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
398 zptMeanCalMags, rtol=1e-6)
399 # These should be roughly equal, but not precisely because of the focal-plane
400 # variation. However, this is a useful sanity check for something going totally
401 # wrong.
402 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
403 photoCalMags, rtol=1e-2)
405 # And the photoCal error is just the zeropoint gray error
406 self.assertFloatsAlmostEqual(testCal.getCalibrationErr(),
407 (np.log(10.0)/2.5)*testCal.getCalibrationMean()*fgcmZptGrayErr)
409 # Test the transmission output
411 visitCatalog = butler.get('fgcmVisitCatalog')
412 lutCat = butler.get('fgcmLookUpTable')
414 testTrans = butler.get('transmission_atmosphere_fgcm',
415 dataId={visitDataRefName: visitCatalog[0]['visit']})
416 testResp = testTrans.sampleAt(position=geom.Point2D(0, 0),
417 wavelengths=lutCat[0]['atmLambda'])
419 # The test fit is performed with the atmosphere parameters frozen
420 # (freezeStdAtmosphere = True). Thus the only difference between
421 # these output atmospheres and the standard is the different
422 # airmass. Furthermore, this is a very rough comparison because
423 # the look-up table is computed with very coarse sampling for faster
424 # testing.
426 # To account for overall throughput changes, we scale by the median ratio,
427 # we only care about the shape
428 ratio = np.median(testResp/lutCat[0]['atmStdTrans'])
429 self.assertFloatsAlmostEqual(testResp/ratio, lutCat[0]['atmStdTrans'], atol=0.04)
431 # The second should be close to the first, but there is the airmass
432 # difference so they aren't identical.
433 testTrans2 = butler.get('transmission_atmosphere_fgcm',
434 dataId={visitDataRefName: visitCatalog[1]['visit']})
435 testResp2 = testTrans2.sampleAt(position=geom.Point2D(0, 0),
436 wavelengths=lutCat[0]['atmLambda'])
437 # As above, we scale by the ratio to compare the shape of the curve.
438 ratio = np.median(testResp/testResp2)
439 self.assertFloatsAlmostEqual(testResp/ratio, testResp2, atol=0.04)
441 def _testFgcmCalibrateTract(self, visits, tract,
442 rawRepeatability, filterNCalibMap):
443 """
444 Test running of FgcmCalibrateTractTask
446 Parameters
447 ----------
448 visits: `list`
449 List of visits to calibrate
450 tract: `int`
451 Tract number
452 rawRepeatability: `np.array`
453 Expected raw repeatability after convergence.
454 Length should be number of bands.
455 filterNCalibMap: `dict`
456 Mapping from filter name to number of photoCalibs created.
457 """
459 args = [self.inputDir, '--output', self.testDir,
460 '--id', 'visit='+'^'.join([str(visit) for visit in visits]),
461 'tract=%d' % (tract),
462 '--doraise']
463 if len(self.configfiles) > 0:
464 args.extend(['--configfile', *self.configfiles])
465 args.extend(self.otherArgs)
467 # Move into the test directory so the plots will get cleaned in tearDown
468 # In the future, with Gen3, we will probably have a better way of managing
469 # non-data output such as plots.
470 cwd = os.getcwd()
471 os.chdir(self.testDir)
473 result = fgcmcal.FgcmCalibrateTractTask.parseAndRun(args=args, config=self.config,
474 doReturnResults=True)
475 self._checkResult(result)
477 # Move back to the previous directory
478 os.chdir(cwd)
480 # Check that the converged repeatability is what we expect
481 repeatability = result.resultList[0].results.repeatability
482 self.assertFloatsAlmostEqual(repeatability, rawRepeatability, atol=1e-6)
484 butler = dafPersist.butler.Butler(self.testDir)
486 # Check that the number of photoCalib objects in each filter are what we expect
487 for filterName in filterNCalibMap.keys():
488 subset = butler.subset('fgcm_tract_photoCalib', tract=tract, filter=filterName)
489 tot = 0
490 for dataRef in subset:
491 if butler.datasetExists('fgcm_tract_photoCalib', dataId=dataRef.dataId):
492 tot += 1
493 self.assertEqual(tot, filterNCalibMap[filterName])
495 # Check that every visit got a transmission
496 visits = butler.queryMetadata('fgcm_tract_photoCalib', ('visit'), tract=tract)
497 for visit in visits:
498 self.assertTrue(butler.datasetExists('transmission_atmosphere_fgcm_tract',
499 tract=tract, visit=visit))
501 # Check that we got the reference catalog output.
502 # This will raise an exception if the catalog is not there.
503 config = LoadIndexedReferenceObjectsConfig()
504 config.ref_dataset_name = 'fgcm_stars_%d' % (tract)
505 task = LoadIndexedReferenceObjectsTask(butler, config=config)
507 coord = geom.SpherePoint(337.656174*geom.degrees, 0.823595*geom.degrees)
509 refStruct = task.loadSkyCircle(coord, 5.0*geom.degrees, filterName='r')
511 # Test the psf candidate counting, ratio should be between 0.0 and 1.0
512 candRatio = (refStruct.refCat['r_nPsfCandidate'].astype(np.float64) /
513 refStruct.refCat['r_nTotal'].astype(np.float64))
514 self.assertFloatsAlmostEqual(candRatio.min(), 0.0)
515 self.assertFloatsAlmostEqual(candRatio.max(), 1.0)
517 # Test that temporary files aren't stored
518 self.assertFalse(butler.datasetExists('fgcmVisitCatalog'))
519 self.assertFalse(butler.datasetExists('fgcmStarObservations'))
520 self.assertFalse(butler.datasetExists('fgcmStarIndices'))
521 self.assertFalse(butler.datasetExists('fgcmReferenceStars'))
523 def _checkResult(self, result):
524 """
525 Check the result output from the task
527 Parameters
528 ----------
529 result: `pipeBase.struct`
530 Result structure output from a task
532 Raises
533 ------
534 Exceptions on test failures
535 """
537 self.assertNotEqual(result.resultList, [], 'resultList should not be empty')
538 self.assertEqual(result.resultList[0].exitStatus, 0)
540 def tearDown(self):
541 """
542 Tear down and clear directories
543 """
545 if getattr(self, 'config', None) is not None:
546 del self.config
547 if os.path.exists(self.testDir):
548 shutil.rmtree(self.testDir, True)