Coverage for tests/fgcmcalTestBaseGen2.py : 7%

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 for Gen2 repos.
27"""
29import os
30import shutil
31import numpy as np
32import numpy.testing as testing
33import glob
34import esutil
36import lsst.daf.persistence as dafPersist
37import lsst.geom as geom
38import lsst.log
39from lsst.meas.algorithms import LoadIndexedReferenceObjectsTask, LoadIndexedReferenceObjectsConfig
40from astropy import units
42import lsst.fgcmcal as fgcmcal
45class FgcmcalTestBaseGen2(object):
46 """
47 Base class for gen2 fgcmcal tests, to genericize some test running and setup.
49 Derive from this first, then from TestCase.
50 """
52 def setUp_base(self, inputDir=None, testDir=None, logLevel=None, otherArgs=[]):
53 """
54 Call from your child class's setUp() to get variables built.
56 Parameters
57 ----------
58 inputDir: `str`, optional
59 Input directory
60 testDir: `str`, optional
61 Test directory
62 logLevel: `str`, optional
63 Override loglevel for command-line tasks
64 otherArgs: `list`, default=[]
65 List of additional arguments to send to command-line tasks
66 """
68 self.inputDir = inputDir
69 self.testDir = testDir
70 self.logLevel = logLevel
71 self.otherArgs = otherArgs
73 self.config = None
74 self.configfiles = []
76 lsst.log.setLevel("daf.persistence.butler", lsst.log.FATAL)
77 lsst.log.setLevel("CameraMapper", lsst.log.FATAL)
79 if self.logLevel is not None:
80 self.otherArgs.extend(['--loglevel', 'fgcmcal=%s'%self.logLevel])
82 def _testFgcmMakeLut(self, nBand, i0Std, i0Recon, i10Std, i10Recon):
83 """
84 Test running of FgcmMakeLutTask
86 Parameters
87 ----------
88 nBand: `int`
89 Number of bands tested
90 i0Std: `np.array', size nBand
91 Values of i0Std to compare to
92 i10Std: `np.array`, size nBand
93 Values of i10Std to compare to
94 i0Recon: `np.array`, size nBand
95 Values of reconstructed i0 to compare to
96 i10Recon: `np.array`, size nBand
97 Values of reconsntructed i10 to compare to
98 """
100 args = [self.inputDir, '--output', self.testDir,
101 '--doraise']
102 if len(self.configfiles) > 0:
103 args.extend(['--configfile', *self.configfiles])
104 args.extend(self.otherArgs)
106 result = fgcmcal.FgcmMakeLutTask.parseAndRun(args=args, config=self.config)
107 self._checkResult(result)
109 butler = dafPersist.butler.Butler(self.testDir)
110 tempTask = fgcmcal.FgcmFitCycleTask()
111 lutCat = butler.get('fgcmLookUpTable')
112 filterMapDict = dict(tempTask.config.physicalFilterMap)
113 fgcmLut, lutIndexVals, lutStd = fgcmcal.utilities.translateFgcmLut(lutCat,
114 filterMapDict)
116 # Check that we got the requested number of bands...
117 self.assertEqual(nBand, len(lutIndexVals[0]['FILTERNAMES']))
119 self.assertFloatsAlmostEqual(i0Std, lutStd[0]['I0STD'], msg='I0Std', rtol=1e-5)
120 self.assertFloatsAlmostEqual(i10Std, lutStd[0]['I10STD'], msg='I10Std', rtol=1e-5)
122 indices = fgcmLut.getIndices(np.arange(nBand, dtype=np.int32),
123 np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']),
124 np.zeros(nBand) + lutStd[0]['O3STD'],
125 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']),
126 np.zeros(nBand) + lutStd[0]['ALPHASTD'],
127 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])),
128 np.zeros(nBand, dtype=np.int32),
129 np.zeros(nBand) + lutStd[0]['PMBSTD'])
130 i0 = fgcmLut.computeI0(np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']),
131 np.zeros(nBand) + lutStd[0]['O3STD'],
132 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']),
133 np.zeros(nBand) + lutStd[0]['ALPHASTD'],
134 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])),
135 np.zeros(nBand) + lutStd[0]['PMBSTD'],
136 indices)
138 self.assertFloatsAlmostEqual(i0Recon, i0, msg='i0Recon', rtol=1e-5)
140 i1 = fgcmLut.computeI1(np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']),
141 np.zeros(nBand) + lutStd[0]['O3STD'],
142 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']),
143 np.zeros(nBand) + lutStd[0]['ALPHASTD'],
144 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])),
145 np.zeros(nBand) + lutStd[0]['PMBSTD'],
146 indices)
148 self.assertFloatsAlmostEqual(i10Recon, i1/i0, msg='i10Recon', rtol=1e-5)
150 def _testFgcmBuildStarsTable(self, visits, nStar, nObs):
151 """
152 Test running of FgcmBuildStarsTableTask
154 Parameters
155 ----------
156 visits: `list`
157 List of visits to calibrate
158 nStar: `int`
159 Number of stars expected
160 nObs: `int`
161 Number of observations of stars expected
162 """
164 args = [self.inputDir, '--output', self.testDir,
165 '--id', 'visit='+'^'.join([str(visit) for visit in visits]),
166 '--doraise']
167 if len(self.configfiles) > 0:
168 args.extend(['--configfile', *self.configfiles])
169 args.extend(self.otherArgs)
171 result = fgcmcal.FgcmBuildStarsTableTask.parseAndRun(args=args, config=self.config)
172 self._checkResult(result)
174 butler = dafPersist.butler.Butler(self.testDir)
176 visitCat = butler.get('fgcmVisitCatalog')
177 self.assertEqual(len(visits), len(visitCat))
179 starIds = butler.get('fgcmStarIds')
180 self.assertEqual(nStar, len(starIds))
182 starObs = butler.get('fgcmStarObservations')
183 self.assertEqual(nObs, len(starObs))
185 def _testFgcmBuildStarsAndCompare(self, visits):
186 """
187 Test running of FgcmBuildStarsTask and compare to Table run
189 Parameters
190 ----------
191 visits: `list`
192 List of visits to calibrate
193 """
194 args = [self.testDir, '--output', os.path.join(self.testDir, 'rerun', 'src'),
195 '--id', 'visit='+'^'.join([str(visit) for visit in visits]),
196 '--doraise']
197 if len(self.configfiles) > 0:
198 args.extend(['--configfile', *self.configfiles])
199 args.extend(self.otherArgs)
201 result = fgcmcal.FgcmBuildStarsTask.parseAndRun(args=args, config=self.config)
202 self._checkResult(result)
204 butlerSrc = dafPersist.Butler(os.path.join(self.testDir, 'rerun', 'src'))
205 butlerTable = dafPersist.Butler(os.path.join(self.testDir))
207 # We compare the two catalogs to ensure they contain the same data. They will
208 # not be identical in ordering because the input data was ingested in a different
209 # order (hence the stars are rearranged).
210 self._compareBuildStars(butlerSrc, butlerTable)
212 def _testFgcmFitCycle(self, nZp, nGoodZp, nOkZp, nBadZp, nStdStars, nPlots, skipChecks=False):
213 """
214 Test running of FgcmFitCycleTask
216 Parameters
217 ----------
218 nZp: `int`
219 Number of zeropoints created by the task
220 nGoodZp: `int`
221 Number of good (photometric) zeropoints created
222 nOkZp: `int`
223 Number of constrained zeropoints (photometric or not)
224 nBadZp: `int`
225 Number of unconstrained (bad) zeropoints
226 nStdStars: `int`
227 Number of standard stars produced
228 nPlots: `int`
229 Number of plots produced
230 skipChecks: `bool`, optional
231 Skip number checks, when running less-than-final cycle.
232 Default is False.
233 """
235 args = [self.inputDir, '--output', self.testDir,
236 '--doraise']
237 if len(self.configfiles) > 0:
238 args.extend(['--configfile', *self.configfiles])
239 args.extend(self.otherArgs)
241 # Move into the test directory so the plots will get cleaned in tearDown
242 # In the future, with Gen3, we will probably have a better way of managing
243 # non-data output such as plots.
244 cwd = os.getcwd()
245 os.chdir(self.testDir)
247 result = fgcmcal.FgcmFitCycleTask.parseAndRun(args=args, config=self.config)
248 self._checkResult(result)
250 # Move back to the previous directory
251 os.chdir(cwd)
253 if skipChecks:
254 return
256 butler = dafPersist.butler.Butler(self.testDir)
258 zps = butler.get('fgcmZeropoints', fgcmcycle=self.config.cycleNumber)
260 # Check the numbers of zeropoints in all, good, okay, and bad
261 self.assertEqual(len(zps), nZp)
263 gd, = np.where(zps['fgcmFlag'] == 1)
264 self.assertEqual(len(gd), nGoodZp)
266 ok, = np.where(zps['fgcmFlag'] < 16)
267 self.assertEqual(len(ok), nOkZp)
269 bd, = np.where(zps['fgcmFlag'] >= 16)
270 self.assertEqual(len(bd), nBadZp)
272 # Check that there are no illegal values with the ok zeropoints
273 test, = np.where(zps['fgcmZpt'][gd] < -9000.0)
274 self.assertEqual(len(test), 0)
276 stds = butler.get('fgcmStandardStars', fgcmcycle=self.config.cycleNumber)
278 self.assertEqual(len(stds), nStdStars)
280 # Check that the expected number of plots are there.
281 plots = glob.glob(os.path.join(self.testDir, self.config.outfileBase
282 + '_cycle%02d_plots/' % (self.config.cycleNumber)
283 + '*.png'))
284 self.assertEqual(len(plots), nPlots)
286 def _testFgcmOutputProducts(self, visitDataRefName, ccdDataRefName,
287 zpOffsets, testVisit, testCcd, testFilter, testBandIndex):
288 """
289 Test running of FgcmOutputProductsTask
291 Parameters
292 ----------
293 visitDataRefName: `str`
294 Name of column in dataRef to get the visit
295 ccdDataRefName: `str`
296 Name of column in dataRef to get the ccd
297 zpOffsets: `np.array`
298 Zeropoint offsets expected
299 testVisit: `int`
300 Visit id to check for round-trip computations
301 testCcd: `int`
302 Ccd id to check for round-trip computations
303 testFilter: `str`
304 Filtername for testVisit/testCcd
305 testBandIndex: `int`
306 Band index for testVisit/testCcd
307 """
309 args = [self.inputDir, '--output', self.testDir,
310 '--doraise']
311 if len(self.configfiles) > 0:
312 args.extend(['--configfile', *self.configfiles])
313 args.extend(self.otherArgs)
315 result = fgcmcal.FgcmOutputProductsTask.parseAndRun(args=args, config=self.config,
316 doReturnResults=True)
317 self._checkResult(result)
319 # Extract the offsets from the results
320 offsets = result.resultList[0].results.offsets
322 self.assertFloatsAlmostEqual(offsets, zpOffsets, atol=1e-6)
324 butler = dafPersist.butler.Butler(self.testDir)
326 # Test the reference catalog stars
328 # Read in the raw stars...
329 rawStars = butler.get('fgcmStandardStars', fgcmcycle=self.config.cycleNumber)
331 # Read in the new reference catalog...
332 config = LoadIndexedReferenceObjectsConfig()
333 config.ref_dataset_name = 'fgcm_stars'
334 task = LoadIndexedReferenceObjectsTask(butler, config=config)
336 # Read in a giant radius to get them all
337 refStruct = task.loadSkyCircle(rawStars[0].getCoord(), 5.0*geom.degrees,
338 filterName='r')
340 # Make sure all the stars are there
341 self.assertEqual(len(rawStars), len(refStruct.refCat))
343 # And make sure the numbers are consistent
344 test, = np.where(rawStars['id'][0] == refStruct.refCat['id'])
346 # Perform math on numpy arrays to maintain datatypes
347 mags = rawStars['mag_std_noabs'][:, 0].astype(np.float64) + offsets[0]
348 fluxes = (mags*units.ABmag).to_value(units.nJy)
349 fluxErrs = (np.log(10.)/2.5)*fluxes*rawStars['magErr_std'][:, 0].astype(np.float64)
350 # Only check the first one
351 self.assertFloatsAlmostEqual(fluxes[0], refStruct.refCat['r_flux'][test[0]])
352 self.assertFloatsAlmostEqual(fluxErrs[0], refStruct.refCat['r_fluxErr'][test[0]])
354 # Test the psf candidate counting, ratio should be between 0.0 and 1.0
355 candRatio = (refStruct.refCat['r_nPsfCandidate'].astype(np.float64)
356 / refStruct.refCat['r_nTotal'].astype(np.float64))
357 self.assertFloatsAlmostEqual(candRatio.min(), 0.0)
358 self.assertFloatsAlmostEqual(candRatio.max(), 1.0)
360 # Test the fgcm_photoCalib output
362 zptCat = butler.get('fgcmZeropoints', fgcmcycle=self.config.cycleNumber)
364 good = (zptCat['fgcmFlag'] < 16)
365 bad = (zptCat['fgcmFlag'] >= 16)
367 # Check that all the good photocalibs are output.
368 for rec in zptCat[good]:
369 testCal = None
370 try:
371 testCal = butler.get('fgcm_photoCalib',
372 dataId={visitDataRefName: int(rec['visit']),
373 ccdDataRefName: int(rec['detector']),
374 'filter': rec['filtername']})
375 except dafPersist.NoResults:
376 pass
378 self.assertIsNotNone(testCal)
380 # Check that none of the bad photocalibs are output.
381 for rec in zptCat[bad]:
382 testCal = None
383 try:
384 testCal = butler.get('fgcm_photoCalib',
385 dataId={visitDataRefName: int(rec['visit']),
386 ccdDataRefName: int(rec['detector']),
387 'filter': rec['filtername']})
388 except dafPersist.NoResults:
389 pass
390 self.assertIsNone(testCal)
392 # We do round-trip value checking on just the final one (chosen arbitrarily)
393 testCal = butler.get('fgcm_photoCalib',
394 dataId={visitDataRefName: int(testVisit),
395 ccdDataRefName: int(testCcd),
396 'filter': testFilter})
397 self.assertIsNotNone(testCal)
399 src = butler.get('src', dataId={visitDataRefName: int(testVisit),
400 ccdDataRefName: int(testCcd)})
402 # Only test sources with positive flux
403 gdSrc = (src['slot_CalibFlux_instFlux'] > 0.0)
405 # We need to apply the calibration offset to the fgcmzpt (which is internal
406 # and doesn't know about that yet)
407 testZpInd, = np.where((zptCat['visit'] == testVisit)
408 & (zptCat['detector'] == testCcd))
409 fgcmZpt = (zptCat['fgcmZpt'][testZpInd] + offsets[testBandIndex]
410 + zptCat['fgcmDeltaChrom'][testZpInd])
411 fgcmZptGrayErr = np.sqrt(zptCat['fgcmZptVar'][testZpInd])
413 if self.config.doComposeWcsJacobian:
414 # The raw zeropoint needs to be modified to know about the wcs jacobian
415 camera = butler.get('camera')
416 approxPixelAreaFields = fgcmcal.utilities.computeApproxPixelAreaFields(camera)
417 center = approxPixelAreaFields[testCcd].getBBox().getCenter()
418 pixAreaCorr = approxPixelAreaFields[testCcd].evaluate(center)
419 fgcmZpt += -2.5*np.log10(pixAreaCorr)
421 # This is the magnitude through the mean calibration
422 photoCalMeanCalMags = np.zeros(gdSrc.sum())
423 # This is the magnitude through the full focal-plane variable mags
424 photoCalMags = np.zeros_like(photoCalMeanCalMags)
425 # This is the magnitude with the FGCM (central-ccd) zeropoint
426 zptMeanCalMags = np.zeros_like(photoCalMeanCalMags)
428 for i, rec in enumerate(src[gdSrc]):
429 photoCalMeanCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'])
430 photoCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'],
431 rec.getCentroid())
432 zptMeanCalMags[i] = fgcmZpt - 2.5*np.log10(rec['slot_CalibFlux_instFlux'])
434 # These should be very close but some tiny differences because the fgcm value
435 # is defined at the center of the bbox, and the photoCal is the mean over the box
436 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
437 zptMeanCalMags, rtol=1e-6)
438 # These should be roughly equal, but not precisely because of the focal-plane
439 # variation. However, this is a useful sanity check for something going totally
440 # wrong.
441 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
442 photoCalMags, rtol=1e-2)
444 # The next test compares the "FGCM standard magnitudes" (which are output
445 # from the fgcm code itself) to the "calibrated magnitudes" that are
446 # obtained from running photoCalib.calibrateCatalog() on the original
447 # src catalogs. This summary comparison ensures that using photoCalibs
448 # yields the same results as what FGCM is computing internally.
449 # Note that we additionally need to take into account the post-processing
450 # offsets used in the tests.
452 # For decent statistics, we are matching all the sources from one visit
453 # (multiple ccds)
455 subset = butler.subset('src', dataId={visitDataRefName: int(testVisit)})
457 matchMag, matchDelta = self._getMatchedVisitCat(rawStars, subset, testBandIndex, offsets)
459 st = np.argsort(matchMag)
460 # Compare the brightest 25% of stars. No matter the setting of
461 # deltaMagBkgOffsetPercentile, we want to ensure that these stars
462 # match on average.
463 brightest, = np.where(matchMag < matchMag[st[int(0.25*st.size)]])
464 self.assertFloatsAlmostEqual(np.median(matchDelta[brightest]), 0.0, atol=0.002)
466 # And the photoCal error is just the zeropoint gray error
467 self.assertFloatsAlmostEqual(testCal.getCalibrationErr(),
468 (np.log(10.0)/2.5)*testCal.getCalibrationMean()*fgcmZptGrayErr)
470 # Test the transmission output
472 visitCatalog = butler.get('fgcmVisitCatalog')
473 lutCat = butler.get('fgcmLookUpTable')
475 testTrans = butler.get('transmission_atmosphere_fgcm',
476 dataId={visitDataRefName: visitCatalog[0]['visit']})
477 testResp = testTrans.sampleAt(position=geom.Point2D(0, 0),
478 wavelengths=lutCat[0]['atmLambda'])
480 # The test fit is performed with the atmosphere parameters frozen
481 # (freezeStdAtmosphere = True). Thus the only difference between
482 # these output atmospheres and the standard is the different
483 # airmass. Furthermore, this is a very rough comparison because
484 # the look-up table is computed with very coarse sampling for faster
485 # testing.
487 # To account for overall throughput changes, we scale by the median ratio,
488 # we only care about the shape
489 ratio = np.median(testResp/lutCat[0]['atmStdTrans'])
490 self.assertFloatsAlmostEqual(testResp/ratio, lutCat[0]['atmStdTrans'], atol=0.04)
492 # The second should be close to the first, but there is the airmass
493 # difference so they aren't identical.
494 testTrans2 = butler.get('transmission_atmosphere_fgcm',
495 dataId={visitDataRefName: visitCatalog[1]['visit']})
496 testResp2 = testTrans2.sampleAt(position=geom.Point2D(0, 0),
497 wavelengths=lutCat[0]['atmLambda'])
499 # As above, we scale by the ratio to compare the shape of the curve.
500 ratio = np.median(testResp/testResp2)
501 self.assertFloatsAlmostEqual(testResp/ratio, testResp2, atol=0.04)
503 def _testFgcmCalibrateTract(self, visits, tract,
504 rawRepeatability, filterNCalibMap):
505 """
506 Test running of FgcmCalibrateTractTask
508 Parameters
509 ----------
510 visits: `list`
511 List of visits to calibrate
512 tract: `int`
513 Tract number
514 rawRepeatability: `np.array`
515 Expected raw repeatability after convergence.
516 Length should be number of bands.
517 filterNCalibMap: `dict`
518 Mapping from filter name to number of photoCalibs created.
519 """
521 args = [self.inputDir, '--output', self.testDir,
522 '--id', 'visit='+'^'.join([str(visit) for visit in visits]),
523 'tract=%d' % (tract),
524 '--doraise']
525 if len(self.configfiles) > 0:
526 args.extend(['--configfile', *self.configfiles])
527 args.extend(self.otherArgs)
529 # Move into the test directory so the plots will get cleaned in tearDown
530 # In the future, with Gen3, we will probably have a better way of managing
531 # non-data output such as plots.
532 cwd = os.getcwd()
533 os.chdir(self.testDir)
535 result = fgcmcal.FgcmCalibrateTractTableTask.parseAndRun(args=args, config=self.config,
536 doReturnResults=True)
537 self._checkResult(result)
539 # Move back to the previous directory
540 os.chdir(cwd)
542 # Check that the converged repeatability is what we expect
543 repeatability = result.resultList[0].results.repeatability
544 self.assertFloatsAlmostEqual(repeatability, rawRepeatability, atol=4e-6)
546 butler = dafPersist.butler.Butler(self.testDir)
548 # Check that the number of photoCalib objects in each filter are what we expect
549 for filterName in filterNCalibMap.keys():
550 subset = butler.subset('fgcm_tract_photoCalib', tract=tract, filter=filterName)
551 tot = 0
552 for dataRef in subset:
553 if butler.datasetExists('fgcm_tract_photoCalib', dataId=dataRef.dataId):
554 tot += 1
555 self.assertEqual(tot, filterNCalibMap[filterName])
557 # Check that every visit got a transmission
558 visits = butler.queryMetadata('fgcm_tract_photoCalib', ('visit'), tract=tract)
559 for visit in visits:
560 self.assertTrue(butler.datasetExists('transmission_atmosphere_fgcm_tract',
561 tract=tract, visit=visit))
563 # Check that we got the reference catalog output.
564 # This will raise an exception if the catalog is not there.
565 config = LoadIndexedReferenceObjectsConfig()
566 config.ref_dataset_name = 'fgcm_stars_%d' % (tract)
567 task = LoadIndexedReferenceObjectsTask(butler, config=config)
569 coord = geom.SpherePoint(337.656174*geom.degrees, 0.823595*geom.degrees)
571 refStruct = task.loadSkyCircle(coord, 5.0*geom.degrees, filterName='r')
573 # Test the psf candidate counting, ratio should be between 0.0 and 1.0
574 candRatio = (refStruct.refCat['r_nPsfCandidate'].astype(np.float64)
575 / refStruct.refCat['r_nTotal'].astype(np.float64))
576 self.assertFloatsAlmostEqual(candRatio.min(), 0.0)
577 self.assertFloatsAlmostEqual(candRatio.max(), 1.0)
579 # Test that temporary files aren't stored
580 self.assertFalse(butler.datasetExists('fgcmVisitCatalog'))
581 self.assertFalse(butler.datasetExists('fgcmStarObservations'))
582 self.assertFalse(butler.datasetExists('fgcmStarIndices'))
583 self.assertFalse(butler.datasetExists('fgcmReferenceStars'))
585 def _compareBuildStars(self, butler1, butler2):
586 """
587 Compare the full set of BuildStars outputs with files from two
588 repos.
590 Parameters
591 ----------
592 butler1, butler2 : `lsst.daf.persistence.Butler`
593 """
594 # Check the visit catalogs are identical
595 visitCat1 = butler1.get('fgcmVisitCatalog').asAstropy()
596 visitCat2 = butler2.get('fgcmVisitCatalog').asAstropy()
598 for col in visitCat1.columns:
599 if isinstance(visitCat1[col][0], str):
600 testing.assert_array_equal(visitCat1[col], visitCat2[col])
601 else:
602 testing.assert_array_almost_equal(visitCat1[col], visitCat2[col])
604 # Check that the observation catalogs have the same length
605 # Detailed comparisons of the contents are below.
606 starObs1 = butler1.get('fgcmStarObservations')
607 starObs2 = butler2.get('fgcmStarObservations')
608 self.assertEqual(len(starObs1), len(starObs2))
610 # Check that the number of stars is the same and all match.
611 starIds1 = butler1.get('fgcmStarIds')
612 starIds2 = butler2.get('fgcmStarIds')
613 self.assertEqual(len(starIds1), len(starIds2))
614 matcher = esutil.htm.Matcher(11, starIds1['ra'], starIds1['dec'])
615 matches = matcher.match(starIds2['ra'], starIds2['dec'], 1./3600., maxmatch=1)
616 self.assertEqual(len(matches[0]), len(starIds1))
618 # Check that the number of observations of each star is the same.
619 testing.assert_array_equal(starIds1['nObs'][matches[1]],
620 starIds2['nObs'][matches[0]])
622 # And to test the contents, we need to unravel the observations and make
623 # sure that they are matched individually, because the two catalogs
624 # are constructed in a different order.
625 starIndices1 = butler1.get('fgcmStarIndices')
626 starIndices2 = butler2.get('fgcmStarIndices')
628 test1 = np.zeros(len(starIndices1), dtype=[('ra', 'f8'),
629 ('dec', 'f8'),
630 ('x', 'f8'),
631 ('y', 'f8'),
632 ('psf_candidate', 'b1'),
633 ('visit', 'i4'),
634 ('ccd', 'i4'),
635 ('instMag', 'f4'),
636 ('instMagErr', 'f4'),
637 ('jacobian', 'f4')])
638 test2 = np.zeros_like(test1)
640 # Fill the test1 numpy recarray with sorted and unpacked data from starObs1.
641 # Note that each star has a different number of observations, leading to
642 # a "ragged" array that is packed in here.
643 counter = 0
644 obsIndex = starIndices1['obsIndex']
645 for i in range(len(starIds1)):
646 ind = starIds1['obsArrIndex'][matches[1][i]]
647 nObs = starIds1['nObs'][matches[1][i]]
648 for name in test1.dtype.names:
649 test1[name][counter: counter + nObs] = starObs1[name][obsIndex][ind: ind + nObs]
650 counter += nObs
652 # Fill the test2 numpy recarray with sorted and unpacked data from starObs2.
653 # Note that we have to match these observations per star by matching "visit"
654 # (implicitly assuming each star is observed only once per visit) to ensure
655 # that the observations in test2 are in the same order as test1.
656 counter = 0
657 obsIndex = starIndices2['obsIndex']
658 for i in range(len(starIds2)):
659 ind = starIds2['obsArrIndex'][matches[0][i]]
660 nObs = starIds2['nObs'][matches[0][i]]
661 a, b = esutil.numpy_util.match(test1['visit'][counter: counter + nObs],
662 starObs2['visit'][obsIndex][ind: ind + nObs])
663 for name in test2.dtype.names:
664 test2[name][counter: counter + nObs][a] = starObs2[name][obsIndex][ind: ind + nObs][b]
665 counter += nObs
667 for name in test1.dtype.names:
668 testing.assert_array_almost_equal(test1[name], test2[name])
670 def _getMatchedVisitCat(self, rawStars, dataRefs, bandIndex, offsets):
671 """
672 Get a list of matched magnitudes and deltas from calibrated src catalogs.
674 Parameters
675 ----------
676 rawStars : `lsst.afw.table.SourceCatalog`
677 Fgcm standard stars
678 dataRefs : `list` or `lsst.daf.persist.ButlerSubset`
679 Data references for source catalogs to match
680 bandIndex : `int`
681 Index of the band for the source catalogs
682 offsets : `np.ndarray`
683 Testing calibration offsets to apply to rawStars
685 Returns
686 -------
687 matchMag : `np.ndarray`
688 Array of matched magnitudes
689 matchDelta : `np.ndarray`
690 Array of matched deltas between src and standard stars.
691 """
692 matcher = esutil.htm.Matcher(11, np.rad2deg(rawStars['coord_ra']),
693 np.rad2deg(rawStars['coord_dec']))
695 matchDelta = None
696 for dataRef in dataRefs:
697 src = dataRef.get()
698 photoCal = dataRef.get('fgcm_photoCalib')
699 src = photoCal.calibrateCatalog(src)
701 gdSrc, = np.where(np.nan_to_num(src['slot_CalibFlux_flux']) > 0.0)
703 matches = matcher.match(np.rad2deg(src['coord_ra'][gdSrc]),
704 np.rad2deg(src['coord_dec'][gdSrc]),
705 1./3600., maxmatch=1)
707 srcMag = src['slot_CalibFlux_mag'][gdSrc][matches[0]]
708 # Apply offset here to the catalog mag
709 catMag = rawStars['mag_std_noabs'][matches[1]][:, bandIndex] + offsets[bandIndex]
710 delta = srcMag - catMag
711 if matchDelta is None:
712 matchDelta = delta
713 matchMag = catMag
714 else:
715 matchDelta = np.append(matchDelta, delta)
716 matchMag = np.append(matchMag, catMag)
718 return matchMag, matchDelta
720 def _checkResult(self, result):
721 """
722 Check the result output from the task
724 Parameters
725 ----------
726 result: `pipeBase.struct`
727 Result structure output from a task
728 """
730 self.assertNotEqual(result.resultList, [], 'resultList should not be empty')
731 self.assertEqual(result.resultList[0].exitStatus, 0)
733 def tearDown(self):
734 """
735 Tear down and clear directories
736 """
738 if getattr(self, 'config', None) is not None:
739 del self.config
740 if os.path.exists(self.testDir):
741 shutil.rmtree(self.testDir, True)