Coverage for tests/fgcmcalTestBase.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.
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 FgcmcalTestBase(object):
46 """
47 Base class for 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 fgcmLut, lutIndexVals, lutStd = fgcmcal.utilities.translateFgcmLut(lutCat,
113 dict(tempTask.config.filterMap))
115 # Check that we got the requested number of bands...
116 self.assertEqual(nBand, len(lutIndexVals[0]['FILTERNAMES']))
118 self.assertFloatsAlmostEqual(i0Std, lutStd[0]['I0STD'], msg='I0Std', rtol=1e-5)
119 self.assertFloatsAlmostEqual(i10Std, lutStd[0]['I10STD'], msg='I10Std', rtol=1e-5)
121 indices = fgcmLut.getIndices(np.arange(nBand, dtype=np.int32),
122 np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']),
123 np.zeros(nBand) + lutStd[0]['O3STD'],
124 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']),
125 np.zeros(nBand) + lutStd[0]['ALPHASTD'],
126 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])),
127 np.zeros(nBand, dtype=np.int32),
128 np.zeros(nBand) + lutStd[0]['PMBSTD'])
129 i0 = fgcmLut.computeI0(np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']),
130 np.zeros(nBand) + lutStd[0]['O3STD'],
131 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']),
132 np.zeros(nBand) + lutStd[0]['ALPHASTD'],
133 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])),
134 np.zeros(nBand) + lutStd[0]['PMBSTD'],
135 indices)
137 self.assertFloatsAlmostEqual(i0Recon, i0, msg='i0Recon', rtol=1e-5)
139 i1 = fgcmLut.computeI1(np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']),
140 np.zeros(nBand) + lutStd[0]['O3STD'],
141 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']),
142 np.zeros(nBand) + lutStd[0]['ALPHASTD'],
143 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])),
144 np.zeros(nBand) + lutStd[0]['PMBSTD'],
145 indices)
147 self.assertFloatsAlmostEqual(i10Recon, i1/i0, msg='i10Recon', rtol=1e-5)
149 def _testFgcmBuildStarsTable(self, visits, nStar, nObs):
150 """
151 Test running of FgcmBuildStarsTableTask
153 Parameters
154 ----------
155 visits: `list`
156 List of visits to calibrate
157 nStar: `int`
158 Number of stars expected
159 nObs: `int`
160 Number of observations of stars expected
161 """
163 args = [self.inputDir, '--output', self.testDir,
164 '--id', 'visit='+'^'.join([str(visit) for visit in visits]),
165 '--doraise']
166 if len(self.configfiles) > 0:
167 args.extend(['--configfile', *self.configfiles])
168 args.extend(self.otherArgs)
170 result = fgcmcal.FgcmBuildStarsTableTask.parseAndRun(args=args, config=self.config)
171 self._checkResult(result)
173 butler = dafPersist.butler.Butler(self.testDir)
175 visitCat = butler.get('fgcmVisitCatalog')
176 self.assertEqual(len(visits), len(visitCat))
178 starIds = butler.get('fgcmStarIds')
179 self.assertEqual(nStar, len(starIds))
181 starObs = butler.get('fgcmStarObservations')
182 self.assertEqual(nObs, len(starObs))
184 def _testFgcmBuildStarsAndCompare(self, visits):
185 """
186 Test running of FgcmBuildStarsTask and compare to Table run
188 Parameters
189 ----------
190 visits: `list`
191 List of visits to calibrate
192 """
193 args = [self.testDir, '--output', os.path.join(self.testDir, 'rerun', 'src'),
194 '--id', 'visit='+'^'.join([str(visit) for visit in visits]),
195 '--doraise']
196 if len(self.configfiles) > 0:
197 args.extend(['--configfile', *self.configfiles])
198 args.extend(self.otherArgs)
200 result = fgcmcal.FgcmBuildStarsTask.parseAndRun(args=args, config=self.config)
201 self._checkResult(result)
203 butlerSrc = dafPersist.Butler(os.path.join(self.testDir, 'rerun', 'src'))
204 butlerTable = dafPersist.Butler(os.path.join(self.testDir))
206 # We compare the two catalogs to ensure they contain the same data. They will
207 # not be identical in ordering because the input data was ingested in a different
208 # order (hence the stars are rearranged).
209 self._compareBuildStars(butlerSrc, butlerTable)
211 def _testFgcmFitCycle(self, nZp, nGoodZp, nOkZp, nBadZp, nStdStars, nPlots, skipChecks=False):
212 """
213 Test running of FgcmFitCycleTask
215 Parameters
216 ----------
217 nZp: `int`
218 Number of zeropoints created by the task
219 nGoodZp: `int`
220 Number of good (photometric) zeropoints created
221 nOkZp: `int`
222 Number of constrained zeropoints (photometric or not)
223 nBadZp: `int`
224 Number of unconstrained (bad) zeropoints
225 nStdStars: `int`
226 Number of standard stars produced
227 nPlots: `int`
228 Number of plots produced
229 skipChecks: `bool`, optional
230 Skip number checks, when running less-than-final cycle.
231 Default is False.
232 """
234 args = [self.inputDir, '--output', self.testDir,
235 '--doraise']
236 if len(self.configfiles) > 0:
237 args.extend(['--configfile', *self.configfiles])
238 args.extend(self.otherArgs)
240 # Move into the test directory so the plots will get cleaned in tearDown
241 # In the future, with Gen3, we will probably have a better way of managing
242 # non-data output such as plots.
243 cwd = os.getcwd()
244 os.chdir(self.testDir)
246 result = fgcmcal.FgcmFitCycleTask.parseAndRun(args=args, config=self.config)
247 self._checkResult(result)
249 # Move back to the previous directory
250 os.chdir(cwd)
252 if skipChecks:
253 return
255 # Check that the expected number of plots are there.
256 plots = glob.glob(os.path.join(self.testDir, self.config.outfileBase +
257 '_cycle%02d_plots/' % (self.config.cycleNumber) +
258 '*.png'))
259 self.assertEqual(len(plots), nPlots)
261 butler = dafPersist.butler.Butler(self.testDir)
263 zps = butler.get('fgcmZeropoints', fgcmcycle=self.config.cycleNumber)
265 # Check the numbers of zeropoints in all, good, okay, and bad
266 self.assertEqual(len(zps), nZp)
268 gd, = np.where(zps['fgcmFlag'] == 1)
269 self.assertEqual(len(gd), nGoodZp)
271 ok, = np.where(zps['fgcmFlag'] < 16)
272 self.assertEqual(len(ok), nOkZp)
274 bd, = np.where(zps['fgcmFlag'] >= 16)
275 self.assertEqual(len(bd), nBadZp)
277 # Check that there are no illegal values with the ok zeropoints
278 test, = np.where(zps['fgcmZpt'][gd] < -9000.0)
279 self.assertEqual(len(test), 0)
281 stds = butler.get('fgcmStandardStars', fgcmcycle=self.config.cycleNumber)
283 self.assertEqual(len(stds), nStdStars)
285 def _testFgcmOutputProducts(self, visitDataRefName, ccdDataRefName, filterMapping,
286 zpOffsets, testVisit, testCcd, testFilter, testBandIndex):
287 """
288 Test running of FgcmOutputProductsTask
290 Parameters
291 ----------
292 visitDataRefName: `str`
293 Name of column in dataRef to get the visit
294 ccdDataRefName: `str`
295 Name of column in dataRef to get the ccd
296 filterMapping: `dict`
297 Mapping of filterName to dataRef filter names
298 zpOffsets: `np.array`
299 Zeropoint offsets expected
300 testVisit: `int`
301 Visit id to check for round-trip computations
302 testCcd: `int`
303 Ccd id to check for round-trip computations
304 testFilter: `str`
305 Filtername for testVisit/testCcd
306 testBandIndex: `int`
307 Band index for testVisit/testCcd
308 """
310 args = [self.inputDir, '--output', self.testDir,
311 '--doraise']
312 if len(self.configfiles) > 0:
313 args.extend(['--configfile', *self.configfiles])
314 args.extend(self.otherArgs)
316 result = fgcmcal.FgcmOutputProductsTask.parseAndRun(args=args, config=self.config,
317 doReturnResults=True)
318 self._checkResult(result)
320 # Extract the offsets from the results
321 offsets = result.resultList[0].results.offsets
323 self.assertFloatsAlmostEqual(offsets, zpOffsets, atol=1e-6)
325 butler = dafPersist.butler.Butler(self.testDir)
327 # Test the reference catalog stars
329 # Read in the raw stars...
330 rawStars = butler.get('fgcmStandardStars', fgcmcycle=self.config.cycleNumber)
332 # Read in the new reference catalog...
333 config = LoadIndexedReferenceObjectsConfig()
334 config.ref_dataset_name = 'fgcm_stars'
335 task = LoadIndexedReferenceObjectsTask(butler, config=config)
337 # Read in a giant radius to get them all
338 refStruct = task.loadSkyCircle(rawStars[0].getCoord(), 5.0*geom.degrees,
339 filterName='r')
341 # Make sure all the stars are there
342 self.assertEqual(len(rawStars), len(refStruct.refCat))
344 # And make sure the numbers are consistent
345 test, = np.where(rawStars['id'][0] == refStruct.refCat['id'])
347 # Perform math on numpy arrays to maintain datatypes
348 mags = rawStars['mag_std_noabs'][:, 0].astype(np.float64) + offsets[0]
349 fluxes = (mags*units.ABmag).to_value(units.nJy)
350 fluxErrs = (np.log(10.)/2.5)*fluxes*rawStars['magErr_std'][:, 0].astype(np.float64)
351 # Only check the first one
352 self.assertFloatsAlmostEqual(fluxes[0], refStruct.refCat['r_flux'][test[0]])
353 self.assertFloatsAlmostEqual(fluxErrs[0], refStruct.refCat['r_fluxErr'][test[0]])
355 # Test the psf candidate counting, ratio should be between 0.0 and 1.0
356 candRatio = (refStruct.refCat['r_nPsfCandidate'].astype(np.float64) /
357 refStruct.refCat['r_nTotal'].astype(np.float64))
358 self.assertFloatsAlmostEqual(candRatio.min(), 0.0)
359 self.assertFloatsAlmostEqual(candRatio.max(), 1.0)
361 # Test the fgcm_photoCalib output
363 zptCat = butler.get('fgcmZeropoints', fgcmcycle=self.config.cycleNumber)
364 selected = (zptCat['fgcmFlag'] < 16)
366 # Read in all the calibrations, these should all be there
367 # This test is simply to ensure that all the photoCalib files exist
368 for rec in zptCat[selected]:
369 testCal = butler.get('fgcm_photoCalib',
370 dataId={visitDataRefName: int(rec['visit']),
371 ccdDataRefName: int(rec['ccd']),
372 'filter': filterMapping[rec['filtername']]})
373 self.assertIsNotNone(testCal)
375 # We do round-trip value checking on just the final one (chosen arbitrarily)
376 testCal = butler.get('fgcm_photoCalib',
377 dataId={visitDataRefName: int(testVisit),
378 ccdDataRefName: int(testCcd),
379 'filter': filterMapping[testFilter]})
380 self.assertIsNotNone(testCal)
382 src = butler.get('src', dataId={visitDataRefName: int(testVisit),
383 ccdDataRefName: int(testCcd)})
385 # Only test sources with positive flux
386 gdSrc = (src['slot_CalibFlux_instFlux'] > 0.0)
388 # We need to apply the calibration offset to the fgcmzpt (which is internal
389 # and doesn't know about that yet)
390 testZpInd, = np.where((zptCat['visit'] == testVisit) &
391 (zptCat['ccd'] == testCcd))
392 fgcmZpt = (zptCat['fgcmZpt'][testZpInd] + offsets[testBandIndex] +
393 zptCat['fgcmDeltaChrom'][testZpInd])
394 fgcmZptGrayErr = np.sqrt(zptCat['fgcmZptVar'][testZpInd])
396 if self.config.doComposeWcsJacobian:
397 # The raw zeropoint needs to be modified to know about the wcs jacobian
398 camera = butler.get('camera')
399 approxPixelAreaFields = fgcmcal.utilities.computeApproxPixelAreaFields(camera)
400 center = approxPixelAreaFields[testCcd].getBBox().getCenter()
401 pixAreaCorr = approxPixelAreaFields[testCcd].evaluate(center)
402 fgcmZpt += -2.5*np.log10(pixAreaCorr)
404 # This is the magnitude through the mean calibration
405 photoCalMeanCalMags = np.zeros(gdSrc.sum())
406 # This is the magnitude through the full focal-plane variable mags
407 photoCalMags = np.zeros_like(photoCalMeanCalMags)
408 # This is the magnitude with the FGCM (central-ccd) zeropoint
409 zptMeanCalMags = np.zeros_like(photoCalMeanCalMags)
411 for i, rec in enumerate(src[gdSrc]):
412 photoCalMeanCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'])
413 photoCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'],
414 rec.getCentroid())
415 zptMeanCalMags[i] = fgcmZpt - 2.5*np.log10(rec['slot_CalibFlux_instFlux'])
417 # These should be very close but some tiny differences because the fgcm value
418 # is defined at the center of the bbox, and the photoCal is the mean over the box
419 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
420 zptMeanCalMags, rtol=1e-6)
421 # These should be roughly equal, but not precisely because of the focal-plane
422 # variation. However, this is a useful sanity check for something going totally
423 # wrong.
424 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
425 photoCalMags, rtol=1e-2)
427 # The next test compares the "FGCM standard magnitudes" (which are output
428 # from the fgcm code itself) to the "calibrated magnitudes" that are
429 # obtained from running photoCalib.calibrateCatalog() on the original
430 # src catalogs. This summary comparison ensures that using photoCalibs
431 # yields the same results as what FGCM is computing internally.
432 # Note that we additionally need to take into account the post-processing
433 # offsets used in the tests.
435 # For decent statistics, we are matching all the sources from one visit
436 # (multiple ccds)
438 subset = butler.subset('src', dataId={visitDataRefName: int(testVisit)})
440 matchMag, matchDelta = self._getMatchedVisitCat(rawStars, subset, testBandIndex, offsets)
442 st = np.argsort(matchMag)
443 # Compare the brightest 25% of stars. No matter the setting of
444 # deltaMagBkgOffsetPercentile, we want to ensure that these stars
445 # match on average.
446 brightest, = np.where(matchMag < matchMag[st[int(0.25*st.size)]])
447 self.assertFloatsAlmostEqual(np.median(matchDelta[brightest]), 0.0, atol=0.002)
449 # And the photoCal error is just the zeropoint gray error
450 self.assertFloatsAlmostEqual(testCal.getCalibrationErr(),
451 (np.log(10.0)/2.5)*testCal.getCalibrationMean()*fgcmZptGrayErr)
453 # Test the transmission output
455 visitCatalog = butler.get('fgcmVisitCatalog')
456 lutCat = butler.get('fgcmLookUpTable')
458 testTrans = butler.get('transmission_atmosphere_fgcm',
459 dataId={visitDataRefName: visitCatalog[0]['visit']})
460 testResp = testTrans.sampleAt(position=geom.Point2D(0, 0),
461 wavelengths=lutCat[0]['atmLambda'])
463 # The test fit is performed with the atmosphere parameters frozen
464 # (freezeStdAtmosphere = True). Thus the only difference between
465 # these output atmospheres and the standard is the different
466 # airmass. Furthermore, this is a very rough comparison because
467 # the look-up table is computed with very coarse sampling for faster
468 # testing.
470 # To account for overall throughput changes, we scale by the median ratio,
471 # we only care about the shape
472 ratio = np.median(testResp/lutCat[0]['atmStdTrans'])
473 self.assertFloatsAlmostEqual(testResp/ratio, lutCat[0]['atmStdTrans'], atol=0.04)
475 # The second should be close to the first, but there is the airmass
476 # difference so they aren't identical.
477 testTrans2 = butler.get('transmission_atmosphere_fgcm',
478 dataId={visitDataRefName: visitCatalog[1]['visit']})
479 testResp2 = testTrans2.sampleAt(position=geom.Point2D(0, 0),
480 wavelengths=lutCat[0]['atmLambda'])
482 # As above, we scale by the ratio to compare the shape of the curve.
483 ratio = np.median(testResp/testResp2)
484 self.assertFloatsAlmostEqual(testResp/ratio, testResp2, atol=0.04)
486 def _testFgcmCalibrateTract(self, visits, tract,
487 rawRepeatability, filterNCalibMap):
488 """
489 Test running of FgcmCalibrateTractTask
491 Parameters
492 ----------
493 visits: `list`
494 List of visits to calibrate
495 tract: `int`
496 Tract number
497 rawRepeatability: `np.array`
498 Expected raw repeatability after convergence.
499 Length should be number of bands.
500 filterNCalibMap: `dict`
501 Mapping from filter name to number of photoCalibs created.
502 """
504 args = [self.inputDir, '--output', self.testDir,
505 '--id', 'visit='+'^'.join([str(visit) for visit in visits]),
506 'tract=%d' % (tract),
507 '--doraise']
508 if len(self.configfiles) > 0:
509 args.extend(['--configfile', *self.configfiles])
510 args.extend(self.otherArgs)
512 # Move into the test directory so the plots will get cleaned in tearDown
513 # In the future, with Gen3, we will probably have a better way of managing
514 # non-data output such as plots.
515 cwd = os.getcwd()
516 os.chdir(self.testDir)
518 result = fgcmcal.FgcmCalibrateTractTableTask.parseAndRun(args=args, config=self.config,
519 doReturnResults=True)
520 self._checkResult(result)
522 # Move back to the previous directory
523 os.chdir(cwd)
525 # Check that the converged repeatability is what we expect
526 repeatability = result.resultList[0].results.repeatability
527 self.assertFloatsAlmostEqual(repeatability, rawRepeatability, atol=4e-6)
529 butler = dafPersist.butler.Butler(self.testDir)
531 # Check that the number of photoCalib objects in each filter are what we expect
532 for filterName in filterNCalibMap.keys():
533 subset = butler.subset('fgcm_tract_photoCalib', tract=tract, filter=filterName)
534 tot = 0
535 for dataRef in subset:
536 if butler.datasetExists('fgcm_tract_photoCalib', dataId=dataRef.dataId):
537 tot += 1
538 self.assertEqual(tot, filterNCalibMap[filterName])
540 # Check that every visit got a transmission
541 visits = butler.queryMetadata('fgcm_tract_photoCalib', ('visit'), tract=tract)
542 for visit in visits:
543 self.assertTrue(butler.datasetExists('transmission_atmosphere_fgcm_tract',
544 tract=tract, visit=visit))
546 # Check that we got the reference catalog output.
547 # This will raise an exception if the catalog is not there.
548 config = LoadIndexedReferenceObjectsConfig()
549 config.ref_dataset_name = 'fgcm_stars_%d' % (tract)
550 task = LoadIndexedReferenceObjectsTask(butler, config=config)
552 coord = geom.SpherePoint(337.656174*geom.degrees, 0.823595*geom.degrees)
554 refStruct = task.loadSkyCircle(coord, 5.0*geom.degrees, filterName='r')
556 # Test the psf candidate counting, ratio should be between 0.0 and 1.0
557 candRatio = (refStruct.refCat['r_nPsfCandidate'].astype(np.float64) /
558 refStruct.refCat['r_nTotal'].astype(np.float64))
559 self.assertFloatsAlmostEqual(candRatio.min(), 0.0)
560 self.assertFloatsAlmostEqual(candRatio.max(), 1.0)
562 # Test that temporary files aren't stored
563 self.assertFalse(butler.datasetExists('fgcmVisitCatalog'))
564 self.assertFalse(butler.datasetExists('fgcmStarObservations'))
565 self.assertFalse(butler.datasetExists('fgcmStarIndices'))
566 self.assertFalse(butler.datasetExists('fgcmReferenceStars'))
568 def _compareBuildStars(self, butler1, butler2):
569 """
570 Compare the full set of BuildStars outputs with files from two
571 repos.
573 Parameters
574 ----------
575 butler1, butler2 : `lsst.daf.persistence.Butler`
576 """
577 # Check the visit catalogs are identical
578 visitCat1 = butler1.get('fgcmVisitCatalog').asAstropy()
579 visitCat2 = butler2.get('fgcmVisitCatalog').asAstropy()
581 for col in visitCat1.columns:
582 if isinstance(visitCat1[col][0], str):
583 testing.assert_array_equal(visitCat1[col], visitCat2[col])
584 else:
585 testing.assert_array_almost_equal(visitCat1[col], visitCat2[col])
587 # Check that the observation catalogs have the same length
588 # Detailed comparisons of the contents are below.
589 starObs1 = butler1.get('fgcmStarObservations')
590 starObs2 = butler2.get('fgcmStarObservations')
591 self.assertEqual(len(starObs1), len(starObs2))
593 # Check that the number of stars is the same and all match.
594 starIds1 = butler1.get('fgcmStarIds')
595 starIds2 = butler2.get('fgcmStarIds')
596 self.assertEqual(len(starIds1), len(starIds2))
597 matcher = esutil.htm.Matcher(11, starIds1['ra'], starIds1['dec'])
598 matches = matcher.match(starIds2['ra'], starIds2['dec'], 1./3600., maxmatch=1)
599 self.assertEqual(len(matches[0]), len(starIds1))
601 # Check that the number of observations of each star is the same.
602 testing.assert_array_equal(starIds1['nObs'][matches[1]],
603 starIds2['nObs'][matches[0]])
605 # And to test the contents, we need to unravel the observations and make
606 # sure that they are matched individually, because the two catalogs
607 # are constructed in a different order.
608 starIndices1 = butler1.get('fgcmStarIndices')
609 starIndices2 = butler2.get('fgcmStarIndices')
611 test1 = np.zeros(len(starIndices1), dtype=[('ra', 'f8'),
612 ('dec', 'f8'),
613 ('x', 'f8'),
614 ('y', 'f8'),
615 ('psf_candidate', 'b1'),
616 ('visit', 'i4'),
617 ('ccd', 'i4'),
618 ('instMag', 'f4'),
619 ('instMagErr', 'f4'),
620 ('jacobian', 'f4')])
621 test2 = np.zeros_like(test1)
623 # Fill the test1 numpy recarray with sorted and unpacked data from starObs1.
624 # Note that each star has a different number of observations, leading to
625 # a "ragged" array that is packed in here.
626 counter = 0
627 obsIndex = starIndices1['obsIndex']
628 for i in range(len(starIds1)):
629 ind = starIds1['obsArrIndex'][matches[1][i]]
630 nObs = starIds1['nObs'][matches[1][i]]
631 for name in test1.dtype.names:
632 test1[name][counter: counter + nObs] = starObs1[name][obsIndex][ind: ind + nObs]
633 counter += nObs
635 # Fill the test2 numpy recarray with sorted and unpacked data from starObs2.
636 # Note that we have to match these observations per star by matching "visit"
637 # (implicitly assuming each star is observed only once per visit) to ensure
638 # that the observations in test2 are in the same order as test1.
639 counter = 0
640 obsIndex = starIndices2['obsIndex']
641 for i in range(len(starIds2)):
642 ind = starIds2['obsArrIndex'][matches[0][i]]
643 nObs = starIds2['nObs'][matches[0][i]]
644 a, b = esutil.numpy_util.match(test1['visit'][counter: counter + nObs],
645 starObs2['visit'][obsIndex][ind: ind + nObs])
646 for name in test2.dtype.names:
647 test2[name][counter: counter + nObs][a] = starObs2[name][obsIndex][ind: ind + nObs][b]
648 counter += nObs
650 for name in test1.dtype.names:
651 testing.assert_array_almost_equal(test1[name], test2[name])
653 def _getMatchedVisitCat(self, rawStars, dataRefs, bandIndex, offsets):
654 """
655 Get a list of matched magnitudes and deltas from calibrated src catalogs.
657 Parameters
658 ----------
659 rawStars : `lsst.afw.table.SourceCatalog`
660 Fgcm standard stars
661 dataRefs : `list` or `lsst.daf.persist.ButlerSubset`
662 Data references for source catalogs to match
663 bandIndex : `int`
664 Index of the band for the source catalogs
665 offsets : `np.ndarray`
666 Testing calibration offsets to apply to rawStars
668 Returns
669 -------
670 matchMag : `np.ndarray`
671 Array of matched magnitudes
672 matchDelta : `np.ndarray`
673 Array of matched deltas between src and standard stars.
674 """
675 matcher = esutil.htm.Matcher(11, np.rad2deg(rawStars['coord_ra']),
676 np.rad2deg(rawStars['coord_dec']))
678 matchDelta = None
679 for dataRef in dataRefs:
680 src = dataRef.get()
681 photoCal = dataRef.get('fgcm_photoCalib')
682 src = photoCal.calibrateCatalog(src)
684 gdSrc, = np.where(np.nan_to_num(src['slot_CalibFlux_flux']) > 0.0)
686 matches = matcher.match(np.rad2deg(src['coord_ra'][gdSrc]),
687 np.rad2deg(src['coord_dec'][gdSrc]),
688 1./3600., maxmatch=1)
690 srcMag = src['slot_CalibFlux_mag'][gdSrc][matches[0]]
691 # Apply offset here to the catalog mag
692 catMag = rawStars['mag_std_noabs'][matches[1]][:, bandIndex] + offsets[bandIndex]
693 delta = srcMag - catMag
694 if matchDelta is None:
695 matchDelta = delta
696 matchMag = catMag
697 else:
698 matchDelta = np.append(matchDelta, delta)
699 matchMag = np.append(matchMag, catMag)
701 return matchMag, matchDelta
703 def _checkResult(self, result):
704 """
705 Check the result output from the task
707 Parameters
708 ----------
709 result: `pipeBase.struct`
710 Result structure output from a task
711 """
713 self.assertNotEqual(result.resultList, [], 'resultList should not be empty')
714 self.assertEqual(result.resultList[0].exitStatus, 0)
716 def tearDown(self):
717 """
718 Tear down and clear directories
719 """
721 if getattr(self, 'config', None) is not None:
722 del self.config
723 if os.path.exists(self.testDir):
724 shutil.rmtree(self.testDir, True)