Coverage for tests/fgcmcalTestBaseGen2.py : 8%

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)
363 selected = (zptCat['fgcmFlag'] < 16)
365 # Read in all the calibrations, these should all be there
366 # This test is simply to ensure that all the photoCalib files exist
367 for rec in zptCat[selected]:
368 testCal = butler.get('fgcm_photoCalib',
369 dataId={visitDataRefName: int(rec['visit']),
370 ccdDataRefName: int(rec['detector']),
371 'filter': rec['filtername']})
372 self.assertIsNotNone(testCal)
374 # We do round-trip value checking on just the final one (chosen arbitrarily)
375 testCal = butler.get('fgcm_photoCalib',
376 dataId={visitDataRefName: int(testVisit),
377 ccdDataRefName: int(testCcd),
378 'filter': testFilter})
379 self.assertIsNotNone(testCal)
381 src = butler.get('src', dataId={visitDataRefName: int(testVisit),
382 ccdDataRefName: int(testCcd)})
384 # Only test sources with positive flux
385 gdSrc = (src['slot_CalibFlux_instFlux'] > 0.0)
387 # We need to apply the calibration offset to the fgcmzpt (which is internal
388 # and doesn't know about that yet)
389 testZpInd, = np.where((zptCat['visit'] == testVisit)
390 & (zptCat['detector'] == testCcd))
391 fgcmZpt = (zptCat['fgcmZpt'][testZpInd] + offsets[testBandIndex]
392 + zptCat['fgcmDeltaChrom'][testZpInd])
393 fgcmZptGrayErr = np.sqrt(zptCat['fgcmZptVar'][testZpInd])
395 if self.config.doComposeWcsJacobian:
396 # The raw zeropoint needs to be modified to know about the wcs jacobian
397 camera = butler.get('camera')
398 approxPixelAreaFields = fgcmcal.utilities.computeApproxPixelAreaFields(camera)
399 center = approxPixelAreaFields[testCcd].getBBox().getCenter()
400 pixAreaCorr = approxPixelAreaFields[testCcd].evaluate(center)
401 fgcmZpt += -2.5*np.log10(pixAreaCorr)
403 # This is the magnitude through the mean calibration
404 photoCalMeanCalMags = np.zeros(gdSrc.sum())
405 # This is the magnitude through the full focal-plane variable mags
406 photoCalMags = np.zeros_like(photoCalMeanCalMags)
407 # This is the magnitude with the FGCM (central-ccd) zeropoint
408 zptMeanCalMags = np.zeros_like(photoCalMeanCalMags)
410 for i, rec in enumerate(src[gdSrc]):
411 photoCalMeanCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'])
412 photoCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'],
413 rec.getCentroid())
414 zptMeanCalMags[i] = fgcmZpt - 2.5*np.log10(rec['slot_CalibFlux_instFlux'])
416 # These should be very close but some tiny differences because the fgcm value
417 # is defined at the center of the bbox, and the photoCal is the mean over the box
418 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
419 zptMeanCalMags, rtol=1e-6)
420 # These should be roughly equal, but not precisely because of the focal-plane
421 # variation. However, this is a useful sanity check for something going totally
422 # wrong.
423 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
424 photoCalMags, rtol=1e-2)
426 # The next test compares the "FGCM standard magnitudes" (which are output
427 # from the fgcm code itself) to the "calibrated magnitudes" that are
428 # obtained from running photoCalib.calibrateCatalog() on the original
429 # src catalogs. This summary comparison ensures that using photoCalibs
430 # yields the same results as what FGCM is computing internally.
431 # Note that we additionally need to take into account the post-processing
432 # offsets used in the tests.
434 # For decent statistics, we are matching all the sources from one visit
435 # (multiple ccds)
437 subset = butler.subset('src', dataId={visitDataRefName: int(testVisit)})
439 matchMag, matchDelta = self._getMatchedVisitCat(rawStars, subset, testBandIndex, offsets)
441 st = np.argsort(matchMag)
442 # Compare the brightest 25% of stars. No matter the setting of
443 # deltaMagBkgOffsetPercentile, we want to ensure that these stars
444 # match on average.
445 brightest, = np.where(matchMag < matchMag[st[int(0.25*st.size)]])
446 self.assertFloatsAlmostEqual(np.median(matchDelta[brightest]), 0.0, atol=0.002)
448 # And the photoCal error is just the zeropoint gray error
449 self.assertFloatsAlmostEqual(testCal.getCalibrationErr(),
450 (np.log(10.0)/2.5)*testCal.getCalibrationMean()*fgcmZptGrayErr)
452 # Test the transmission output
454 visitCatalog = butler.get('fgcmVisitCatalog')
455 lutCat = butler.get('fgcmLookUpTable')
457 testTrans = butler.get('transmission_atmosphere_fgcm',
458 dataId={visitDataRefName: visitCatalog[0]['visit']})
459 testResp = testTrans.sampleAt(position=geom.Point2D(0, 0),
460 wavelengths=lutCat[0]['atmLambda'])
462 # The test fit is performed with the atmosphere parameters frozen
463 # (freezeStdAtmosphere = True). Thus the only difference between
464 # these output atmospheres and the standard is the different
465 # airmass. Furthermore, this is a very rough comparison because
466 # the look-up table is computed with very coarse sampling for faster
467 # testing.
469 # To account for overall throughput changes, we scale by the median ratio,
470 # we only care about the shape
471 ratio = np.median(testResp/lutCat[0]['atmStdTrans'])
472 self.assertFloatsAlmostEqual(testResp/ratio, lutCat[0]['atmStdTrans'], atol=0.04)
474 # The second should be close to the first, but there is the airmass
475 # difference so they aren't identical.
476 testTrans2 = butler.get('transmission_atmosphere_fgcm',
477 dataId={visitDataRefName: visitCatalog[1]['visit']})
478 testResp2 = testTrans2.sampleAt(position=geom.Point2D(0, 0),
479 wavelengths=lutCat[0]['atmLambda'])
481 # As above, we scale by the ratio to compare the shape of the curve.
482 ratio = np.median(testResp/testResp2)
483 self.assertFloatsAlmostEqual(testResp/ratio, testResp2, atol=0.04)
485 def _testFgcmCalibrateTract(self, visits, tract,
486 rawRepeatability, filterNCalibMap):
487 """
488 Test running of FgcmCalibrateTractTask
490 Parameters
491 ----------
492 visits: `list`
493 List of visits to calibrate
494 tract: `int`
495 Tract number
496 rawRepeatability: `np.array`
497 Expected raw repeatability after convergence.
498 Length should be number of bands.
499 filterNCalibMap: `dict`
500 Mapping from filter name to number of photoCalibs created.
501 """
503 args = [self.inputDir, '--output', self.testDir,
504 '--id', 'visit='+'^'.join([str(visit) for visit in visits]),
505 'tract=%d' % (tract),
506 '--doraise']
507 if len(self.configfiles) > 0:
508 args.extend(['--configfile', *self.configfiles])
509 args.extend(self.otherArgs)
511 # Move into the test directory so the plots will get cleaned in tearDown
512 # In the future, with Gen3, we will probably have a better way of managing
513 # non-data output such as plots.
514 cwd = os.getcwd()
515 os.chdir(self.testDir)
517 result = fgcmcal.FgcmCalibrateTractTableTask.parseAndRun(args=args, config=self.config,
518 doReturnResults=True)
519 self._checkResult(result)
521 # Move back to the previous directory
522 os.chdir(cwd)
524 # Check that the converged repeatability is what we expect
525 repeatability = result.resultList[0].results.repeatability
526 self.assertFloatsAlmostEqual(repeatability, rawRepeatability, atol=4e-6)
528 butler = dafPersist.butler.Butler(self.testDir)
530 # Check that the number of photoCalib objects in each filter are what we expect
531 for filterName in filterNCalibMap.keys():
532 subset = butler.subset('fgcm_tract_photoCalib', tract=tract, filter=filterName)
533 tot = 0
534 for dataRef in subset:
535 if butler.datasetExists('fgcm_tract_photoCalib', dataId=dataRef.dataId):
536 tot += 1
537 self.assertEqual(tot, filterNCalibMap[filterName])
539 # Check that every visit got a transmission
540 visits = butler.queryMetadata('fgcm_tract_photoCalib', ('visit'), tract=tract)
541 for visit in visits:
542 self.assertTrue(butler.datasetExists('transmission_atmosphere_fgcm_tract',
543 tract=tract, visit=visit))
545 # Check that we got the reference catalog output.
546 # This will raise an exception if the catalog is not there.
547 config = LoadIndexedReferenceObjectsConfig()
548 config.ref_dataset_name = 'fgcm_stars_%d' % (tract)
549 task = LoadIndexedReferenceObjectsTask(butler, config=config)
551 coord = geom.SpherePoint(337.656174*geom.degrees, 0.823595*geom.degrees)
553 refStruct = task.loadSkyCircle(coord, 5.0*geom.degrees, filterName='r')
555 # Test the psf candidate counting, ratio should be between 0.0 and 1.0
556 candRatio = (refStruct.refCat['r_nPsfCandidate'].astype(np.float64)
557 / refStruct.refCat['r_nTotal'].astype(np.float64))
558 self.assertFloatsAlmostEqual(candRatio.min(), 0.0)
559 self.assertFloatsAlmostEqual(candRatio.max(), 1.0)
561 # Test that temporary files aren't stored
562 self.assertFalse(butler.datasetExists('fgcmVisitCatalog'))
563 self.assertFalse(butler.datasetExists('fgcmStarObservations'))
564 self.assertFalse(butler.datasetExists('fgcmStarIndices'))
565 self.assertFalse(butler.datasetExists('fgcmReferenceStars'))
567 def _compareBuildStars(self, butler1, butler2):
568 """
569 Compare the full set of BuildStars outputs with files from two
570 repos.
572 Parameters
573 ----------
574 butler1, butler2 : `lsst.daf.persistence.Butler`
575 """
576 # Check the visit catalogs are identical
577 visitCat1 = butler1.get('fgcmVisitCatalog').asAstropy()
578 visitCat2 = butler2.get('fgcmVisitCatalog').asAstropy()
580 for col in visitCat1.columns:
581 if isinstance(visitCat1[col][0], str):
582 testing.assert_array_equal(visitCat1[col], visitCat2[col])
583 else:
584 testing.assert_array_almost_equal(visitCat1[col], visitCat2[col])
586 # Check that the observation catalogs have the same length
587 # Detailed comparisons of the contents are below.
588 starObs1 = butler1.get('fgcmStarObservations')
589 starObs2 = butler2.get('fgcmStarObservations')
590 self.assertEqual(len(starObs1), len(starObs2))
592 # Check that the number of stars is the same and all match.
593 starIds1 = butler1.get('fgcmStarIds')
594 starIds2 = butler2.get('fgcmStarIds')
595 self.assertEqual(len(starIds1), len(starIds2))
596 matcher = esutil.htm.Matcher(11, starIds1['ra'], starIds1['dec'])
597 matches = matcher.match(starIds2['ra'], starIds2['dec'], 1./3600., maxmatch=1)
598 self.assertEqual(len(matches[0]), len(starIds1))
600 # Check that the number of observations of each star is the same.
601 testing.assert_array_equal(starIds1['nObs'][matches[1]],
602 starIds2['nObs'][matches[0]])
604 # And to test the contents, we need to unravel the observations and make
605 # sure that they are matched individually, because the two catalogs
606 # are constructed in a different order.
607 starIndices1 = butler1.get('fgcmStarIndices')
608 starIndices2 = butler2.get('fgcmStarIndices')
610 test1 = np.zeros(len(starIndices1), dtype=[('ra', 'f8'),
611 ('dec', 'f8'),
612 ('x', 'f8'),
613 ('y', 'f8'),
614 ('psf_candidate', 'b1'),
615 ('visit', 'i4'),
616 ('ccd', 'i4'),
617 ('instMag', 'f4'),
618 ('instMagErr', 'f4'),
619 ('jacobian', 'f4')])
620 test2 = np.zeros_like(test1)
622 # Fill the test1 numpy recarray with sorted and unpacked data from starObs1.
623 # Note that each star has a different number of observations, leading to
624 # a "ragged" array that is packed in here.
625 counter = 0
626 obsIndex = starIndices1['obsIndex']
627 for i in range(len(starIds1)):
628 ind = starIds1['obsArrIndex'][matches[1][i]]
629 nObs = starIds1['nObs'][matches[1][i]]
630 for name in test1.dtype.names:
631 test1[name][counter: counter + nObs] = starObs1[name][obsIndex][ind: ind + nObs]
632 counter += nObs
634 # Fill the test2 numpy recarray with sorted and unpacked data from starObs2.
635 # Note that we have to match these observations per star by matching "visit"
636 # (implicitly assuming each star is observed only once per visit) to ensure
637 # that the observations in test2 are in the same order as test1.
638 counter = 0
639 obsIndex = starIndices2['obsIndex']
640 for i in range(len(starIds2)):
641 ind = starIds2['obsArrIndex'][matches[0][i]]
642 nObs = starIds2['nObs'][matches[0][i]]
643 a, b = esutil.numpy_util.match(test1['visit'][counter: counter + nObs],
644 starObs2['visit'][obsIndex][ind: ind + nObs])
645 for name in test2.dtype.names:
646 test2[name][counter: counter + nObs][a] = starObs2[name][obsIndex][ind: ind + nObs][b]
647 counter += nObs
649 for name in test1.dtype.names:
650 testing.assert_array_almost_equal(test1[name], test2[name])
652 def _getMatchedVisitCat(self, rawStars, dataRefs, bandIndex, offsets):
653 """
654 Get a list of matched magnitudes and deltas from calibrated src catalogs.
656 Parameters
657 ----------
658 rawStars : `lsst.afw.table.SourceCatalog`
659 Fgcm standard stars
660 dataRefs : `list` or `lsst.daf.persist.ButlerSubset`
661 Data references for source catalogs to match
662 bandIndex : `int`
663 Index of the band for the source catalogs
664 offsets : `np.ndarray`
665 Testing calibration offsets to apply to rawStars
667 Returns
668 -------
669 matchMag : `np.ndarray`
670 Array of matched magnitudes
671 matchDelta : `np.ndarray`
672 Array of matched deltas between src and standard stars.
673 """
674 matcher = esutil.htm.Matcher(11, np.rad2deg(rawStars['coord_ra']),
675 np.rad2deg(rawStars['coord_dec']))
677 matchDelta = None
678 for dataRef in dataRefs:
679 src = dataRef.get()
680 photoCal = dataRef.get('fgcm_photoCalib')
681 src = photoCal.calibrateCatalog(src)
683 gdSrc, = np.where(np.nan_to_num(src['slot_CalibFlux_flux']) > 0.0)
685 matches = matcher.match(np.rad2deg(src['coord_ra'][gdSrc]),
686 np.rad2deg(src['coord_dec'][gdSrc]),
687 1./3600., maxmatch=1)
689 srcMag = src['slot_CalibFlux_mag'][gdSrc][matches[0]]
690 # Apply offset here to the catalog mag
691 catMag = rawStars['mag_std_noabs'][matches[1]][:, bandIndex] + offsets[bandIndex]
692 delta = srcMag - catMag
693 if matchDelta is None:
694 matchDelta = delta
695 matchMag = catMag
696 else:
697 matchDelta = np.append(matchDelta, delta)
698 matchMag = np.append(matchMag, catMag)
700 return matchMag, matchDelta
702 def _checkResult(self, result):
703 """
704 Check the result output from the task
706 Parameters
707 ----------
708 result: `pipeBase.struct`
709 Result structure output from a task
710 """
712 self.assertNotEqual(result.resultList, [], 'resultList should not be empty')
713 self.assertEqual(result.resultList[0].exitStatus, 0)
715 def tearDown(self):
716 """
717 Tear down and clear directories
718 """
720 if getattr(self, 'config', None) is not None:
721 del self.config
722 if os.path.exists(self.testDir):
723 shutil.rmtree(self.testDir, True)