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 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 butler = dafPersist.butler.Butler(self.testDir)
257 zps = butler.get('fgcmZeropoints', fgcmcycle=self.config.cycleNumber)
259 # Check the numbers of zeropoints in all, good, okay, and bad
260 self.assertEqual(len(zps), nZp)
262 gd, = np.where(zps['fgcmFlag'] == 1)
263 self.assertEqual(len(gd), nGoodZp)
265 ok, = np.where(zps['fgcmFlag'] < 16)
266 self.assertEqual(len(ok), nOkZp)
268 bd, = np.where(zps['fgcmFlag'] >= 16)
269 self.assertEqual(len(bd), nBadZp)
271 # Check that there are no illegal values with the ok zeropoints
272 test, = np.where(zps['fgcmZpt'][gd] < -9000.0)
273 self.assertEqual(len(test), 0)
275 stds = butler.get('fgcmStandardStars', fgcmcycle=self.config.cycleNumber)
277 self.assertEqual(len(stds), nStdStars)
279 # Check that the expected number of plots are there.
280 plots = glob.glob(os.path.join(self.testDir, self.config.outfileBase
281 + '_cycle%02d_plots/' % (self.config.cycleNumber)
282 + '*.png'))
283 self.assertEqual(len(plots), nPlots)
285 def _testFgcmOutputProducts(self, visitDataRefName, ccdDataRefName,
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 zpOffsets: `np.array`
297 Zeropoint offsets expected
298 testVisit: `int`
299 Visit id to check for round-trip computations
300 testCcd: `int`
301 Ccd id to check for round-trip computations
302 testFilter: `str`
303 Filtername for testVisit/testCcd
304 testBandIndex: `int`
305 Band index for testVisit/testCcd
306 """
308 args = [self.inputDir, '--output', self.testDir,
309 '--doraise']
310 if len(self.configfiles) > 0:
311 args.extend(['--configfile', *self.configfiles])
312 args.extend(self.otherArgs)
314 result = fgcmcal.FgcmOutputProductsTask.parseAndRun(args=args, config=self.config,
315 doReturnResults=True)
316 self._checkResult(result)
318 # Extract the offsets from the results
319 offsets = result.resultList[0].results.offsets
321 self.assertFloatsAlmostEqual(offsets, zpOffsets, atol=1e-6)
323 butler = dafPersist.butler.Butler(self.testDir)
325 # Test the reference catalog stars
327 # Read in the raw stars...
328 rawStars = butler.get('fgcmStandardStars', fgcmcycle=self.config.cycleNumber)
330 # Read in the new reference catalog...
331 config = LoadIndexedReferenceObjectsConfig()
332 config.ref_dataset_name = 'fgcm_stars'
333 task = LoadIndexedReferenceObjectsTask(butler, config=config)
335 # Read in a giant radius to get them all
336 refStruct = task.loadSkyCircle(rawStars[0].getCoord(), 5.0*geom.degrees,
337 filterName='r')
339 # Make sure all the stars are there
340 self.assertEqual(len(rawStars), len(refStruct.refCat))
342 # And make sure the numbers are consistent
343 test, = np.where(rawStars['id'][0] == refStruct.refCat['id'])
345 # Perform math on numpy arrays to maintain datatypes
346 mags = rawStars['mag_std_noabs'][:, 0].astype(np.float64) + offsets[0]
347 fluxes = (mags*units.ABmag).to_value(units.nJy)
348 fluxErrs = (np.log(10.)/2.5)*fluxes*rawStars['magErr_std'][:, 0].astype(np.float64)
349 # Only check the first one
350 self.assertFloatsAlmostEqual(fluxes[0], refStruct.refCat['r_flux'][test[0]])
351 self.assertFloatsAlmostEqual(fluxErrs[0], refStruct.refCat['r_fluxErr'][test[0]])
353 # Test the psf candidate counting, ratio should be between 0.0 and 1.0
354 candRatio = (refStruct.refCat['r_nPsfCandidate'].astype(np.float64)
355 / refStruct.refCat['r_nTotal'].astype(np.float64))
356 self.assertFloatsAlmostEqual(candRatio.min(), 0.0)
357 self.assertFloatsAlmostEqual(candRatio.max(), 1.0)
359 # Test the fgcm_photoCalib output
361 zptCat = butler.get('fgcmZeropoints', fgcmcycle=self.config.cycleNumber)
362 selected = (zptCat['fgcmFlag'] < 16)
364 # Read in all the calibrations, these should all be there
365 # This test is simply to ensure that all the photoCalib files exist
366 for rec in zptCat[selected]:
367 testCal = butler.get('fgcm_photoCalib',
368 dataId={visitDataRefName: int(rec['visit']),
369 ccdDataRefName: int(rec['detector']),
370 'filter': rec['filtername']})
371 self.assertIsNotNone(testCal)
373 # We do round-trip value checking on just the final one (chosen arbitrarily)
374 testCal = butler.get('fgcm_photoCalib',
375 dataId={visitDataRefName: int(testVisit),
376 ccdDataRefName: int(testCcd),
377 'filter': testFilter})
378 self.assertIsNotNone(testCal)
380 src = butler.get('src', dataId={visitDataRefName: int(testVisit),
381 ccdDataRefName: int(testCcd)})
383 # Only test sources with positive flux
384 gdSrc = (src['slot_CalibFlux_instFlux'] > 0.0)
386 # We need to apply the calibration offset to the fgcmzpt (which is internal
387 # and doesn't know about that yet)
388 testZpInd, = np.where((zptCat['visit'] == testVisit)
389 & (zptCat['detector'] == testCcd))
390 fgcmZpt = (zptCat['fgcmZpt'][testZpInd] + offsets[testBandIndex]
391 + zptCat['fgcmDeltaChrom'][testZpInd])
392 fgcmZptGrayErr = np.sqrt(zptCat['fgcmZptVar'][testZpInd])
394 if self.config.doComposeWcsJacobian:
395 # The raw zeropoint needs to be modified to know about the wcs jacobian
396 camera = butler.get('camera')
397 approxPixelAreaFields = fgcmcal.utilities.computeApproxPixelAreaFields(camera)
398 center = approxPixelAreaFields[testCcd].getBBox().getCenter()
399 pixAreaCorr = approxPixelAreaFields[testCcd].evaluate(center)
400 fgcmZpt += -2.5*np.log10(pixAreaCorr)
402 # This is the magnitude through the mean calibration
403 photoCalMeanCalMags = np.zeros(gdSrc.sum())
404 # This is the magnitude through the full focal-plane variable mags
405 photoCalMags = np.zeros_like(photoCalMeanCalMags)
406 # This is the magnitude with the FGCM (central-ccd) zeropoint
407 zptMeanCalMags = np.zeros_like(photoCalMeanCalMags)
409 for i, rec in enumerate(src[gdSrc]):
410 photoCalMeanCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'])
411 photoCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'],
412 rec.getCentroid())
413 zptMeanCalMags[i] = fgcmZpt - 2.5*np.log10(rec['slot_CalibFlux_instFlux'])
415 # These should be very close but some tiny differences because the fgcm value
416 # is defined at the center of the bbox, and the photoCal is the mean over the box
417 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
418 zptMeanCalMags, rtol=1e-6)
419 # These should be roughly equal, but not precisely because of the focal-plane
420 # variation. However, this is a useful sanity check for something going totally
421 # wrong.
422 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
423 photoCalMags, rtol=1e-2)
425 # The next test compares the "FGCM standard magnitudes" (which are output
426 # from the fgcm code itself) to the "calibrated magnitudes" that are
427 # obtained from running photoCalib.calibrateCatalog() on the original
428 # src catalogs. This summary comparison ensures that using photoCalibs
429 # yields the same results as what FGCM is computing internally.
430 # Note that we additionally need to take into account the post-processing
431 # offsets used in the tests.
433 # For decent statistics, we are matching all the sources from one visit
434 # (multiple ccds)
436 subset = butler.subset('src', dataId={visitDataRefName: int(testVisit)})
438 matchMag, matchDelta = self._getMatchedVisitCat(rawStars, subset, testBandIndex, offsets)
440 st = np.argsort(matchMag)
441 # Compare the brightest 25% of stars. No matter the setting of
442 # deltaMagBkgOffsetPercentile, we want to ensure that these stars
443 # match on average.
444 brightest, = np.where(matchMag < matchMag[st[int(0.25*st.size)]])
445 self.assertFloatsAlmostEqual(np.median(matchDelta[brightest]), 0.0, atol=0.002)
447 # And the photoCal error is just the zeropoint gray error
448 self.assertFloatsAlmostEqual(testCal.getCalibrationErr(),
449 (np.log(10.0)/2.5)*testCal.getCalibrationMean()*fgcmZptGrayErr)
451 # Test the transmission output
453 visitCatalog = butler.get('fgcmVisitCatalog')
454 lutCat = butler.get('fgcmLookUpTable')
456 testTrans = butler.get('transmission_atmosphere_fgcm',
457 dataId={visitDataRefName: visitCatalog[0]['visit']})
458 testResp = testTrans.sampleAt(position=geom.Point2D(0, 0),
459 wavelengths=lutCat[0]['atmLambda'])
461 # The test fit is performed with the atmosphere parameters frozen
462 # (freezeStdAtmosphere = True). Thus the only difference between
463 # these output atmospheres and the standard is the different
464 # airmass. Furthermore, this is a very rough comparison because
465 # the look-up table is computed with very coarse sampling for faster
466 # testing.
468 # To account for overall throughput changes, we scale by the median ratio,
469 # we only care about the shape
470 ratio = np.median(testResp/lutCat[0]['atmStdTrans'])
471 self.assertFloatsAlmostEqual(testResp/ratio, lutCat[0]['atmStdTrans'], atol=0.04)
473 # The second should be close to the first, but there is the airmass
474 # difference so they aren't identical.
475 testTrans2 = butler.get('transmission_atmosphere_fgcm',
476 dataId={visitDataRefName: visitCatalog[1]['visit']})
477 testResp2 = testTrans2.sampleAt(position=geom.Point2D(0, 0),
478 wavelengths=lutCat[0]['atmLambda'])
480 # As above, we scale by the ratio to compare the shape of the curve.
481 ratio = np.median(testResp/testResp2)
482 self.assertFloatsAlmostEqual(testResp/ratio, testResp2, atol=0.04)
484 def _testFgcmCalibrateTract(self, visits, tract,
485 rawRepeatability, filterNCalibMap):
486 """
487 Test running of FgcmCalibrateTractTask
489 Parameters
490 ----------
491 visits: `list`
492 List of visits to calibrate
493 tract: `int`
494 Tract number
495 rawRepeatability: `np.array`
496 Expected raw repeatability after convergence.
497 Length should be number of bands.
498 filterNCalibMap: `dict`
499 Mapping from filter name to number of photoCalibs created.
500 """
502 args = [self.inputDir, '--output', self.testDir,
503 '--id', 'visit='+'^'.join([str(visit) for visit in visits]),
504 'tract=%d' % (tract),
505 '--doraise']
506 if len(self.configfiles) > 0:
507 args.extend(['--configfile', *self.configfiles])
508 args.extend(self.otherArgs)
510 # Move into the test directory so the plots will get cleaned in tearDown
511 # In the future, with Gen3, we will probably have a better way of managing
512 # non-data output such as plots.
513 cwd = os.getcwd()
514 os.chdir(self.testDir)
516 result = fgcmcal.FgcmCalibrateTractTableTask.parseAndRun(args=args, config=self.config,
517 doReturnResults=True)
518 self._checkResult(result)
520 # Move back to the previous directory
521 os.chdir(cwd)
523 # Check that the converged repeatability is what we expect
524 repeatability = result.resultList[0].results.repeatability
525 self.assertFloatsAlmostEqual(repeatability, rawRepeatability, atol=4e-6)
527 butler = dafPersist.butler.Butler(self.testDir)
529 # Check that the number of photoCalib objects in each filter are what we expect
530 for filterName in filterNCalibMap.keys():
531 subset = butler.subset('fgcm_tract_photoCalib', tract=tract, filter=filterName)
532 tot = 0
533 for dataRef in subset:
534 if butler.datasetExists('fgcm_tract_photoCalib', dataId=dataRef.dataId):
535 tot += 1
536 self.assertEqual(tot, filterNCalibMap[filterName])
538 # Check that every visit got a transmission
539 visits = butler.queryMetadata('fgcm_tract_photoCalib', ('visit'), tract=tract)
540 for visit in visits:
541 self.assertTrue(butler.datasetExists('transmission_atmosphere_fgcm_tract',
542 tract=tract, visit=visit))
544 # Check that we got the reference catalog output.
545 # This will raise an exception if the catalog is not there.
546 config = LoadIndexedReferenceObjectsConfig()
547 config.ref_dataset_name = 'fgcm_stars_%d' % (tract)
548 task = LoadIndexedReferenceObjectsTask(butler, config=config)
550 coord = geom.SpherePoint(337.656174*geom.degrees, 0.823595*geom.degrees)
552 refStruct = task.loadSkyCircle(coord, 5.0*geom.degrees, filterName='r')
554 # Test the psf candidate counting, ratio should be between 0.0 and 1.0
555 candRatio = (refStruct.refCat['r_nPsfCandidate'].astype(np.float64)
556 / refStruct.refCat['r_nTotal'].astype(np.float64))
557 self.assertFloatsAlmostEqual(candRatio.min(), 0.0)
558 self.assertFloatsAlmostEqual(candRatio.max(), 1.0)
560 # Test that temporary files aren't stored
561 self.assertFalse(butler.datasetExists('fgcmVisitCatalog'))
562 self.assertFalse(butler.datasetExists('fgcmStarObservations'))
563 self.assertFalse(butler.datasetExists('fgcmStarIndices'))
564 self.assertFalse(butler.datasetExists('fgcmReferenceStars'))
566 def _compareBuildStars(self, butler1, butler2):
567 """
568 Compare the full set of BuildStars outputs with files from two
569 repos.
571 Parameters
572 ----------
573 butler1, butler2 : `lsst.daf.persistence.Butler`
574 """
575 # Check the visit catalogs are identical
576 visitCat1 = butler1.get('fgcmVisitCatalog').asAstropy()
577 visitCat2 = butler2.get('fgcmVisitCatalog').asAstropy()
579 for col in visitCat1.columns:
580 if isinstance(visitCat1[col][0], str):
581 testing.assert_array_equal(visitCat1[col], visitCat2[col])
582 else:
583 testing.assert_array_almost_equal(visitCat1[col], visitCat2[col])
585 # Check that the observation catalogs have the same length
586 # Detailed comparisons of the contents are below.
587 starObs1 = butler1.get('fgcmStarObservations')
588 starObs2 = butler2.get('fgcmStarObservations')
589 self.assertEqual(len(starObs1), len(starObs2))
591 # Check that the number of stars is the same and all match.
592 starIds1 = butler1.get('fgcmStarIds')
593 starIds2 = butler2.get('fgcmStarIds')
594 self.assertEqual(len(starIds1), len(starIds2))
595 matcher = esutil.htm.Matcher(11, starIds1['ra'], starIds1['dec'])
596 matches = matcher.match(starIds2['ra'], starIds2['dec'], 1./3600., maxmatch=1)
597 self.assertEqual(len(matches[0]), len(starIds1))
599 # Check that the number of observations of each star is the same.
600 testing.assert_array_equal(starIds1['nObs'][matches[1]],
601 starIds2['nObs'][matches[0]])
603 # And to test the contents, we need to unravel the observations and make
604 # sure that they are matched individually, because the two catalogs
605 # are constructed in a different order.
606 starIndices1 = butler1.get('fgcmStarIndices')
607 starIndices2 = butler2.get('fgcmStarIndices')
609 test1 = np.zeros(len(starIndices1), dtype=[('ra', 'f8'),
610 ('dec', 'f8'),
611 ('x', 'f8'),
612 ('y', 'f8'),
613 ('psf_candidate', 'b1'),
614 ('visit', 'i4'),
615 ('ccd', 'i4'),
616 ('instMag', 'f4'),
617 ('instMagErr', 'f4'),
618 ('jacobian', 'f4')])
619 test2 = np.zeros_like(test1)
621 # Fill the test1 numpy recarray with sorted and unpacked data from starObs1.
622 # Note that each star has a different number of observations, leading to
623 # a "ragged" array that is packed in here.
624 counter = 0
625 obsIndex = starIndices1['obsIndex']
626 for i in range(len(starIds1)):
627 ind = starIds1['obsArrIndex'][matches[1][i]]
628 nObs = starIds1['nObs'][matches[1][i]]
629 for name in test1.dtype.names:
630 test1[name][counter: counter + nObs] = starObs1[name][obsIndex][ind: ind + nObs]
631 counter += nObs
633 # Fill the test2 numpy recarray with sorted and unpacked data from starObs2.
634 # Note that we have to match these observations per star by matching "visit"
635 # (implicitly assuming each star is observed only once per visit) to ensure
636 # that the observations in test2 are in the same order as test1.
637 counter = 0
638 obsIndex = starIndices2['obsIndex']
639 for i in range(len(starIds2)):
640 ind = starIds2['obsArrIndex'][matches[0][i]]
641 nObs = starIds2['nObs'][matches[0][i]]
642 a, b = esutil.numpy_util.match(test1['visit'][counter: counter + nObs],
643 starObs2['visit'][obsIndex][ind: ind + nObs])
644 for name in test2.dtype.names:
645 test2[name][counter: counter + nObs][a] = starObs2[name][obsIndex][ind: ind + nObs][b]
646 counter += nObs
648 for name in test1.dtype.names:
649 testing.assert_array_almost_equal(test1[name], test2[name])
651 def _getMatchedVisitCat(self, rawStars, dataRefs, bandIndex, offsets):
652 """
653 Get a list of matched magnitudes and deltas from calibrated src catalogs.
655 Parameters
656 ----------
657 rawStars : `lsst.afw.table.SourceCatalog`
658 Fgcm standard stars
659 dataRefs : `list` or `lsst.daf.persist.ButlerSubset`
660 Data references for source catalogs to match
661 bandIndex : `int`
662 Index of the band for the source catalogs
663 offsets : `np.ndarray`
664 Testing calibration offsets to apply to rawStars
666 Returns
667 -------
668 matchMag : `np.ndarray`
669 Array of matched magnitudes
670 matchDelta : `np.ndarray`
671 Array of matched deltas between src and standard stars.
672 """
673 matcher = esutil.htm.Matcher(11, np.rad2deg(rawStars['coord_ra']),
674 np.rad2deg(rawStars['coord_dec']))
676 matchDelta = None
677 for dataRef in dataRefs:
678 src = dataRef.get()
679 photoCal = dataRef.get('fgcm_photoCalib')
680 src = photoCal.calibrateCatalog(src)
682 gdSrc, = np.where(np.nan_to_num(src['slot_CalibFlux_flux']) > 0.0)
684 matches = matcher.match(np.rad2deg(src['coord_ra'][gdSrc]),
685 np.rad2deg(src['coord_dec'][gdSrc]),
686 1./3600., maxmatch=1)
688 srcMag = src['slot_CalibFlux_mag'][gdSrc][matches[0]]
689 # Apply offset here to the catalog mag
690 catMag = rawStars['mag_std_noabs'][matches[1]][:, bandIndex] + offsets[bandIndex]
691 delta = srcMag - catMag
692 if matchDelta is None:
693 matchDelta = delta
694 matchMag = catMag
695 else:
696 matchDelta = np.append(matchDelta, delta)
697 matchMag = np.append(matchMag, catMag)
699 return matchMag, matchDelta
701 def _checkResult(self, result):
702 """
703 Check the result output from the task
705 Parameters
706 ----------
707 result: `pipeBase.struct`
708 Result structure output from a task
709 """
711 self.assertNotEqual(result.resultList, [], 'resultList should not be empty')
712 self.assertEqual(result.resultList[0].exitStatus, 0)
714 def tearDown(self):
715 """
716 Tear down and clear directories
717 """
719 if getattr(self, 'config', None) is not None:
720 del self.config
721 if os.path.exists(self.testDir):
722 shutil.rmtree(self.testDir, True)