Coverage for tests/fgcmcalTestBase.py: 10%
227 statements
« prev ^ index » next coverage.py v6.4.4, created at 2022-09-03 09:53 +0000
« prev ^ index » next coverage.py v6.4.4, created at 2022-09-03 09:53 +0000
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 Gen3 repos.
27"""
29import os
30import shutil
31import numpy as np
32import glob
33import esutil
35import lsst.daf.butler as dafButler
36import lsst.pipe.base as pipeBase
37import lsst.geom as geom
38from lsst.pipe.base import Pipeline
39from lsst.ctrl.mpexec import SimplePipelineExecutor
41import lsst.fgcmcal as fgcmcal
43ROOT = os.path.abspath(os.path.dirname(__file__))
46class FgcmcalTestBase(object):
47 """Base class for gen3 fgcmcal tests, to genericize some test running and setup.
49 Derive from this first, then from TestCase.
50 """
51 @classmethod
52 def _importRepository(cls, instrument, exportPath, exportFile):
53 """Import a test repository into self.testDir
55 Parameters
56 ----------
57 instrument : `str`
58 Full string name for the instrument.
59 exportPath : `str`
60 Path to location of repository to export.
61 exportFile : `str`
62 Filename of export data.
63 """
64 cls.repo = os.path.join(cls.testDir, 'testrepo')
66 # Make the repo and retrieve a writeable Butler
67 _ = dafButler.Butler.makeRepo(cls.repo)
68 butler = dafButler.Butler(cls.repo, writeable=True)
69 # Register the instrument
70 instrInstance = pipeBase.Instrument.from_string(instrument)
71 instrInstance.register(butler.registry)
72 # Import the exportFile
73 butler.import_(directory=exportPath, filename=exportFile,
74 transfer='symlink',
75 skip_dimensions={'instrument', 'detector', 'physical_filter'})
77 def _runPipeline(self, repo, pipelineFile, queryString='',
78 inputCollections=None, outputCollection=None,
79 configFiles={}, configOptions={},
80 registerDatasetTypes=False):
81 """Run a pipeline via pipetask.
83 Parameters
84 ----------
85 repo : `str`
86 Gen3 repository yaml file.
87 pipelineFile : `str`
88 Pipeline definition file.
89 queryString : `str`, optional
90 Where query that defines the data to use.
91 inputCollections : `list` [`str`], optional
92 Input collections list.
93 outputCollection : `str`, optional
94 Output collection name.
95 configFiles : `dict` [`list`], optional
96 Dictionary of config files. The key of the ``configFiles``
97 dict is the relevant task label. The value of ``configFiles``
98 is a list of config files to apply (in order) to that task.
99 configOptions : `dict` [`dict`], optional
100 Dictionary of individual config options. The key of the
101 ``configOptions`` dict is the relevant task label. The value
102 of ``configOptions`` is another dict that contains config
103 key/value overrides to apply.
104 configOptions : `list` [`str`], optional
105 List of individual config options to use. Each string will
106 be of the form ``taskName:configField=value``.
107 registerDatasetTypes : `bool`, optional
108 Register new dataset types?
110 Returns
111 -------
112 exit_code : `int`
113 Exit code for pipetask run.
115 Raises
116 ------
117 RuntimeError : Raised if the "pipetask" call fails.
118 """
119 butler = SimplePipelineExecutor.prep_butler(repo,
120 inputs=inputCollections,
121 output=outputCollection)
123 pipeline = Pipeline.fromFile(pipelineFile)
124 for taskName, fileList in configFiles.items():
125 for fileName in fileList:
126 pipeline.addConfigFile(taskName, fileName)
127 for taskName, configDict in configOptions.items():
128 for option, value in configDict.items():
129 pipeline.addConfigOverride(taskName, option, value)
131 executor = SimplePipelineExecutor.from_pipeline(pipeline,
132 where=queryString,
133 root=repo,
134 butler=butler)
135 quanta = executor.run(register_dataset_types=registerDatasetTypes)
137 return len(quanta)
139 def _testFgcmMakeLut(self, instName, testName, nBand, i0Std, i0Recon, i10Std, i10Recon):
140 """Test running of FgcmMakeLutTask
142 Parameters
143 ----------
144 instName : `str`
145 Short name of the instrument
146 testName : `str`
147 Base name of the test collection
148 nBand : `int`
149 Number of bands tested
150 i0Std : `np.ndarray'
151 Values of i0Std to compare to
152 i10Std : `np.ndarray`
153 Values of i10Std to compare to
154 i0Recon : `np.ndarray`
155 Values of reconstructed i0 to compare to
156 i10Recon : `np.ndarray`
157 Values of reconsntructed i10 to compare to
158 """
159 instCamel = instName.title()
161 configFiles = {'fgcmMakeLut': [os.path.join(ROOT,
162 'config',
163 f'fgcmMakeLut{instCamel}.py')]}
164 outputCollection = f'{instName}/{testName}/lut'
166 self._runPipeline(self.repo,
167 os.path.join(ROOT,
168 'pipelines',
169 f'fgcmMakeLut{instCamel}.yaml'),
170 configFiles=configFiles,
171 inputCollections=[f'{instName}/calib', f'{instName}/testdata'],
172 outputCollection=outputCollection,
173 registerDatasetTypes=True)
175 # Check output values
176 butler = dafButler.Butler(self.repo)
177 lutCat = butler.get('fgcmLookUpTable',
178 collections=[outputCollection],
179 instrument=instName)
180 fgcmLut, lutIndexVals, lutStd = fgcmcal.utilities.translateFgcmLut(lutCat, {})
182 self.assertEqual(nBand, len(lutIndexVals[0]['FILTERNAMES']))
184 indices = fgcmLut.getIndices(np.arange(nBand, dtype=np.int32),
185 np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']),
186 np.zeros(nBand) + lutStd[0]['O3STD'],
187 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']),
188 np.zeros(nBand) + lutStd[0]['ALPHASTD'],
189 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])),
190 np.zeros(nBand, dtype=np.int32),
191 np.zeros(nBand) + lutStd[0]['PMBSTD'])
192 i0 = fgcmLut.computeI0(np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']),
193 np.zeros(nBand) + lutStd[0]['O3STD'],
194 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']),
195 np.zeros(nBand) + lutStd[0]['ALPHASTD'],
196 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])),
197 np.zeros(nBand) + lutStd[0]['PMBSTD'],
198 indices)
200 self.assertFloatsAlmostEqual(i0Recon, i0, msg='i0Recon', rtol=1e-5)
202 i1 = fgcmLut.computeI1(np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']),
203 np.zeros(nBand) + lutStd[0]['O3STD'],
204 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']),
205 np.zeros(nBand) + lutStd[0]['ALPHASTD'],
206 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])),
207 np.zeros(nBand) + lutStd[0]['PMBSTD'],
208 indices)
210 self.assertFloatsAlmostEqual(i10Recon, i1/i0, msg='i10Recon', rtol=1e-5)
212 def _testFgcmBuildStarsTable(self, instName, testName, queryString, visits, nStar, nObs):
213 """Test running of FgcmBuildStarsTableTask
215 Parameters
216 ----------
217 instName : `str`
218 Short name of the instrument
219 testName : `str`
220 Base name of the test collection
221 queryString : `str`
222 Query to send to the pipetask.
223 visits : `list`
224 List of visits to calibrate
225 nStar : `int`
226 Number of stars expected
227 nObs : `int`
228 Number of observations of stars expected
229 """
230 instCamel = instName.title()
232 configFiles = {'fgcmBuildStarsTable': [os.path.join(ROOT,
233 'config',
234 f'fgcmBuildStarsTable{instCamel}.py')]}
235 outputCollection = f'{instName}/{testName}/buildstars'
237 self._runPipeline(self.repo,
238 os.path.join(ROOT,
239 'pipelines',
240 'fgcmBuildStarsTable%s.yaml' % (instCamel)),
241 configFiles=configFiles,
242 inputCollections=[f'{instName}/{testName}/lut',
243 'refcats/gen2'],
244 outputCollection=outputCollection,
245 queryString=queryString,
246 registerDatasetTypes=True)
248 butler = dafButler.Butler(self.repo)
250 visitCat = butler.get('fgcmVisitCatalog', collections=[outputCollection],
251 instrument=instName)
252 self.assertEqual(len(visits), len(visitCat))
254 starIds = butler.get('fgcmStarIds', collections=[outputCollection],
255 instrument=instName)
256 self.assertEqual(len(starIds), nStar)
258 starObs = butler.get('fgcmStarObservations', collections=[outputCollection],
259 instrument=instName)
260 self.assertEqual(len(starObs), nObs)
262 def _testFgcmFitCycle(self, instName, testName, cycleNumber,
263 nZp, nGoodZp, nOkZp, nBadZp, nStdStars, nPlots,
264 skipChecks=False, extraConfig=None):
265 """Test running of FgcmFitCycleTask
267 Parameters
268 ----------
269 instName : `str`
270 Short name of the instrument
271 testName : `str`
272 Base name of the test collection
273 cycleNumber : `int`
274 Fit cycle number.
275 nZp : `int`
276 Number of zeropoints created by the task
277 nGoodZp : `int`
278 Number of good (photometric) zeropoints created
279 nOkZp : `int`
280 Number of constrained zeropoints (photometric or not)
281 nBadZp : `int`
282 Number of unconstrained (bad) zeropoints
283 nStdStars : `int`
284 Number of standard stars produced
285 nPlots : `int`
286 Number of plots produced
287 skipChecks : `bool`, optional
288 Skip number checks, when running less-than-final cycle.
289 extraConfig : `str`, optional
290 Name of an extra config file to apply.
291 """
292 instCamel = instName.title()
294 configFiles = {'fgcmFitCycle': [os.path.join(ROOT,
295 'config',
296 f'fgcmFitCycle{instCamel}.py')]}
297 if extraConfig is not None:
298 configFiles['fgcmFitCycle'].append(extraConfig)
300 outputCollection = f'{instName}/{testName}/fit'
302 if cycleNumber == 0:
303 inputCollections = [f'{instName}/{testName}/buildstars']
304 else:
305 # In these tests we are running the fit cycle task multiple
306 # times into the same output collection. This code allows
307 # us to find the correct chained input collections to use
308 # so that we can both read from previous runs in the output
309 # collection and write to a new run in the output collection.
310 # Note that this behavior is handled automatically by the
311 # pipetask command-line interface, but not by the python
312 # API.
313 butler = dafButler.Butler(self.repo)
314 inputCollections = list(butler.registry.getCollectionChain(outputCollection))
316 cwd = os.getcwd()
317 runDir = os.path.join(self.testDir, testName)
318 os.makedirs(runDir, exist_ok=True)
319 os.chdir(runDir)
321 configOptions = {'fgcmFitCycle':
322 {'cycleNumber': f'{cycleNumber}',
323 'connections.previousCycleNumber': f'{cycleNumber - 1}',
324 'connections.cycleNumber': f'{cycleNumber}'}}
325 self._runPipeline(self.repo,
326 os.path.join(ROOT,
327 'pipelines',
328 f'fgcmFitCycle{instCamel}.yaml'),
329 configFiles=configFiles,
330 inputCollections=inputCollections,
331 outputCollection=outputCollection,
332 configOptions=configOptions,
333 registerDatasetTypes=True)
335 os.chdir(cwd)
337 if skipChecks:
338 return
340 butler = dafButler.Butler(self.repo)
342 config = butler.get('fgcmFitCycle_config', collections=[outputCollection])
344 # Check that the expected number of plots are there.
345 plots = glob.glob(os.path.join(runDir, config.outfileBase
346 + '_cycle%02d_plots/' % (cycleNumber)
347 + '*.png'))
348 self.assertEqual(len(plots), nPlots)
350 zps = butler.get('fgcmZeropoints%d' % (cycleNumber),
351 collections=[outputCollection],
352 instrument=instName)
353 self.assertEqual(len(zps), nZp)
355 gd, = np.where(zps['fgcmFlag'] == 1)
356 self.assertEqual(len(gd), nGoodZp)
358 ok, = np.where(zps['fgcmFlag'] < 16)
359 self.assertEqual(len(ok), nOkZp)
361 bd, = np.where(zps['fgcmFlag'] >= 16)
362 self.assertEqual(len(bd), nBadZp)
364 # Check that there are no illegal values with the ok zeropoints
365 test, = np.where(zps['fgcmZpt'][gd] < -9000.0)
366 self.assertEqual(len(test), 0)
368 stds = butler.get('fgcmStandardStars%d' % (cycleNumber),
369 collections=[outputCollection],
370 instrument=instName)
372 self.assertEqual(len(stds), nStdStars)
374 def _testFgcmOutputProducts(self, instName, testName,
375 zpOffsets, testVisit, testCcd, testFilter, testBandIndex):
376 """Test running of FgcmOutputProductsTask.
378 Parameters
379 ----------
380 instName : `str`
381 Short name of the instrument
382 testName : `str`
383 Base name of the test collection
384 zpOffsets : `np.ndarray`
385 Zeropoint offsets expected
386 testVisit : `int`
387 Visit id to check for round-trip computations
388 testCcd : `int`
389 Ccd id to check for round-trip computations
390 testFilter : `str`
391 Filtername for testVisit/testCcd
392 testBandIndex : `int`
393 Band index for testVisit/testCcd
394 """
395 instCamel = instName.title()
397 configFiles = {'fgcmOutputProducts': [os.path.join(ROOT,
398 'config',
399 f'fgcmOutputProducts{instCamel}.py')]}
400 inputCollection = f'{instName}/{testName}/fit'
401 outputCollection = f'{instName}/{testName}/fit/output'
403 self._runPipeline(self.repo,
404 os.path.join(ROOT,
405 'pipelines',
406 'fgcmOutputProducts%s.yaml' % (instCamel)),
407 configFiles=configFiles,
408 inputCollections=[inputCollection],
409 outputCollection=outputCollection,
410 registerDatasetTypes=True)
412 butler = dafButler.Butler(self.repo)
413 offsetCat = butler.get('fgcmReferenceCalibrationOffsets',
414 collections=[outputCollection], instrument=instName)
415 offsets = offsetCat['offset'][:]
416 self.assertFloatsAlmostEqual(offsets, zpOffsets, atol=1e-6)
418 config = butler.get('fgcmOutputProducts_config',
419 collections=[outputCollection], instrument=instName)
421 rawStars = butler.get('fgcmStandardStars' + config.connections.cycleNumber,
422 collections=[inputCollection], instrument=instName)
424 candRatio = (rawStars['npsfcand'][:, 0].astype(np.float64)
425 / rawStars['ntotal'][:, 0].astype(np.float64))
426 self.assertFloatsAlmostEqual(candRatio.min(), 0.0)
427 self.assertFloatsAlmostEqual(candRatio.max(), 1.0)
429 # Test the fgcm_photoCalib output
430 zptCat = butler.get('fgcmZeropoints' + config.connections.cycleNumber,
431 collections=[inputCollection], instrument=instName)
433 good = (zptCat['fgcmFlag'] < 16)
434 bad = (zptCat['fgcmFlag'] >= 16)
436 # Read in all the calibrations, these should all be there
437 # This test is simply to ensure that all the photoCalib files exist
438 visits = np.unique(zptCat['visit'][good])
439 photoCalibDict = {}
440 for visit in visits:
441 expCat = butler.get('fgcmPhotoCalibCatalog',
442 visit=visit,
443 collections=[outputCollection], instrument=instName)
444 for row in expCat:
445 if row['visit'] == visit:
446 photoCalibDict[(visit, row['id'])] = row.getPhotoCalib()
448 # Check that all of the good photocalibs are there.
449 for rec in zptCat[good]:
450 self.assertTrue((rec['visit'], rec['detector']) in photoCalibDict)
452 # Check that none of the bad photocalibs are there.
453 for rec in zptCat[bad]:
454 self.assertFalse((rec['visit'], rec['detector']) in photoCalibDict)
456 # We do round-trip value checking on just the final one (chosen arbitrarily)
457 testCal = photoCalibDict[(testVisit, testCcd)]
459 src = butler.get('src', visit=int(testVisit), detector=int(testCcd),
460 collections=[outputCollection], instrument=instName)
462 # Only test sources with positive flux
463 gdSrc = (src['slot_CalibFlux_instFlux'] > 0.0)
465 # We need to apply the calibration offset to the fgcmzpt (which is internal
466 # and doesn't know about that yet)
467 testZpInd, = np.where((zptCat['visit'] == testVisit)
468 & (zptCat['detector'] == testCcd))
469 fgcmZpt = (zptCat['fgcmZpt'][testZpInd] + offsets[testBandIndex]
470 + zptCat['fgcmDeltaChrom'][testZpInd])
471 fgcmZptGrayErr = np.sqrt(zptCat['fgcmZptVar'][testZpInd])
473 if config.doComposeWcsJacobian:
474 # The raw zeropoint needs to be modified to know about the wcs jacobian
475 refs = butler.registry.queryDatasets('camera', dimensions=['instrument'],
476 collections=...)
477 camera = butler.getDirect(list(refs)[0])
478 approxPixelAreaFields = fgcmcal.utilities.computeApproxPixelAreaFields(camera)
479 center = approxPixelAreaFields[testCcd].getBBox().getCenter()
480 pixAreaCorr = approxPixelAreaFields[testCcd].evaluate(center)
481 fgcmZpt += -2.5*np.log10(pixAreaCorr)
483 # This is the magnitude through the mean calibration
484 photoCalMeanCalMags = np.zeros(gdSrc.sum())
485 # This is the magnitude through the full focal-plane variable mags
486 photoCalMags = np.zeros_like(photoCalMeanCalMags)
487 # This is the magnitude with the FGCM (central-ccd) zeropoint
488 zptMeanCalMags = np.zeros_like(photoCalMeanCalMags)
490 for i, rec in enumerate(src[gdSrc]):
491 photoCalMeanCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'])
492 photoCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'],
493 rec.getCentroid())
494 zptMeanCalMags[i] = fgcmZpt - 2.5*np.log10(rec['slot_CalibFlux_instFlux'])
496 # These should be very close but some tiny differences because the fgcm value
497 # is defined at the center of the bbox, and the photoCal is the mean over the box
498 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
499 zptMeanCalMags, rtol=1e-6)
500 # These should be roughly equal, but not precisely because of the focal-plane
501 # variation. However, this is a useful sanity check for something going totally
502 # wrong.
503 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
504 photoCalMags, rtol=1e-2)
506 # The next test compares the "FGCM standard magnitudes" (which are output
507 # from the fgcm code itself) to the "calibrated magnitudes" that are
508 # obtained from running photoCalib.calibrateCatalog() on the original
509 # src catalogs. This summary comparison ensures that using photoCalibs
510 # yields the same results as what FGCM is computing internally.
511 # Note that we additionally need to take into account the post-processing
512 # offsets used in the tests.
514 # For decent statistics, we are matching all the sources from one visit
515 # (multiple ccds)
516 whereClause = f"instrument='{instName:s}' and visit={testVisit:d}"
517 srcRefs = butler.registry.queryDatasets('src', dimensions=['visit'],
518 collections='%s/testdata' % (instName),
519 where=whereClause,
520 findFirst=True)
521 photoCals = []
522 for srcRef in srcRefs:
523 photoCals.append(photoCalibDict[(testVisit, srcRef.dataId['detector'])])
525 matchMag, matchDelta = self._getMatchedVisitCat(butler, srcRefs, photoCals,
526 rawStars, testBandIndex, offsets)
528 st = np.argsort(matchMag)
529 # Compare the brightest 25% of stars. No matter the setting of
530 # deltaMagBkgOffsetPercentile, we want to ensure that these stars
531 # match on average.
532 brightest, = np.where(matchMag < matchMag[st[int(0.25*st.size)]])
533 self.assertFloatsAlmostEqual(np.median(matchDelta[brightest]), 0.0, atol=0.002)
535 # And the photoCal error is just the zeropoint gray error
536 self.assertFloatsAlmostEqual(testCal.getCalibrationErr(),
537 (np.log(10.0)/2.5)*testCal.getCalibrationMean()*fgcmZptGrayErr)
539 # Test the transmission output
540 visitCatalog = butler.get('fgcmVisitCatalog', collections=[inputCollection],
541 instrument=instName)
542 lutCat = butler.get('fgcmLookUpTable', collections=[inputCollection],
543 instrument=instName)
545 testTrans = butler.get('transmission_atmosphere_fgcm',
546 visit=visitCatalog[0]['visit'],
547 collections=[outputCollection], instrument=instName)
548 testResp = testTrans.sampleAt(position=geom.Point2D(0, 0),
549 wavelengths=lutCat[0]['atmLambda'])
551 # The test fit is performed with the atmosphere parameters frozen
552 # (freezeStdAtmosphere = True). Thus the only difference between
553 # these output atmospheres and the standard is the different
554 # airmass. Furthermore, this is a very rough comparison because
555 # the look-up table is computed with very coarse sampling for faster
556 # testing.
558 # To account for overall throughput changes, we scale by the median ratio,
559 # we only care about the shape
560 ratio = np.median(testResp/lutCat[0]['atmStdTrans'])
561 self.assertFloatsAlmostEqual(testResp/ratio, lutCat[0]['atmStdTrans'], atol=0.04)
563 # The second should be close to the first, but there is the airmass
564 # difference so they aren't identical.
565 testTrans2 = butler.get('transmission_atmosphere_fgcm',
566 visit=visitCatalog[1]['visit'],
567 collections=[outputCollection], instrument=instName)
568 testResp2 = testTrans2.sampleAt(position=geom.Point2D(0, 0),
569 wavelengths=lutCat[0]['atmLambda'])
571 # As above, we scale by the ratio to compare the shape of the curve.
572 ratio = np.median(testResp/testResp2)
573 self.assertFloatsAlmostEqual(testResp/ratio, testResp2, atol=0.04)
575 def _testFgcmMultiFit(self, instName, testName, queryString, visits, zpOffsets):
576 """Test running the full pipeline with multiple fit cycles.
578 Parameters
579 ----------
580 instName : `str`
581 Short name of the instrument
582 testName : `str`
583 Base name of the test collection
584 queryString : `str`
585 Query to send to the pipetask.
586 visits : `list`
587 List of visits to calibrate
588 zpOffsets : `np.ndarray`
589 Zeropoint offsets expected
590 """
591 instCamel = instName.title()
593 configFiles = {'fgcmBuildStarsTable': [os.path.join(ROOT,
594 'config',
595 f'fgcmBuildStarsTable{instCamel}.py')],
596 'fgcmFitCycle': [os.path.join(ROOT,
597 'config',
598 f'fgcmFitCycle{instCamel}.py')],
599 'fgcmOutputProducts': [os.path.join(ROOT,
600 'config',
601 f'fgcmOutputProducts{instCamel}.py')]}
602 outputCollection = f'{instName}/{testName}/unified'
604 cwd = os.getcwd()
605 runDir = os.path.join(self.testDir, testName)
606 os.makedirs(runDir)
607 os.chdir(runDir)
609 self._runPipeline(self.repo,
610 os.path.join(ROOT,
611 'pipelines',
612 f'fgcmFullPipeline{instCamel}.yaml'),
613 configFiles=configFiles,
614 inputCollections=[f'{instName}/{testName}/lut',
615 'refcats/gen2'],
616 outputCollection=outputCollection,
617 queryString=queryString,
618 registerDatasetTypes=True)
620 os.chdir(cwd)
622 butler = dafButler.Butler(self.repo)
624 offsetCat = butler.get('fgcmReferenceCalibrationOffsets',
625 collections=[outputCollection], instrument=instName)
626 offsets = offsetCat['offset'][:]
627 self.assertFloatsAlmostEqual(offsets, zpOffsets, atol=1e-6)
629 def _getMatchedVisitCat(self, butler, srcHandles, photoCals,
630 rawStars, bandIndex, offsets):
631 """
632 Get a list of matched magnitudes and deltas from calibrated src catalogs.
634 Parameters
635 ----------
636 butler : `lsst.daf.butler.Butler`
637 srcHandles : `list`
638 handles of source catalogs
639 photoCals : `list`
640 photoCalib objects, matched to srcHandles.
641 rawStars : `lsst.afw.table.SourceCatalog`
642 Fgcm standard stars
643 bandIndex : `int`
644 Index of the band for the source catalogs
645 offsets : `np.ndarray`
646 Testing calibration offsets to apply to rawStars
648 Returns
649 -------
650 matchMag : `np.ndarray`
651 Array of matched magnitudes
652 matchDelta : `np.ndarray`
653 Array of matched deltas between src and standard stars.
654 """
655 matcher = esutil.htm.Matcher(11, np.rad2deg(rawStars['coord_ra']),
656 np.rad2deg(rawStars['coord_dec']))
658 matchDelta = None
659 for srcHandle, photoCal in zip(srcHandles, photoCals):
660 src = butler.getDirect(srcHandle)
661 src = photoCal.calibrateCatalog(src)
663 gdSrc, = np.where(np.nan_to_num(src['slot_CalibFlux_flux']) > 0.0)
665 matches = matcher.match(np.rad2deg(src['coord_ra'][gdSrc]),
666 np.rad2deg(src['coord_dec'][gdSrc]),
667 1./3600., maxmatch=1)
669 srcMag = src['slot_CalibFlux_mag'][gdSrc][matches[0]]
670 # Apply offset here to the catalog mag
671 catMag = rawStars['mag_std_noabs'][matches[1]][:, bandIndex] + offsets[bandIndex]
672 delta = srcMag - catMag
673 if matchDelta is None:
674 matchDelta = delta
675 matchMag = catMag
676 else:
677 matchDelta = np.append(matchDelta, delta)
678 matchMag = np.append(matchMag, catMag)
680 return matchMag, matchDelta
682 def _testFgcmCalibrateTract(self, instName, testName, visits, tract, skymapName,
683 rawRepeatability, filterNCalibMap):
684 """Test running of FgcmCalibrateTractTask
686 Parameters
687 ----------
688 instName : `str`
689 Short name of the instrument
690 testName : `str`
691 Base name of the test collection
692 visits : `list`
693 List of visits to calibrate
694 tract : `int`
695 Tract number
696 skymapName : `str`
697 Name of the sky map
698 rawRepeatability : `np.array`
699 Expected raw repeatability after convergence.
700 Length should be number of bands.
701 filterNCalibMap : `dict`
702 Mapping from filter name to number of photoCalibs created.
703 """
704 instCamel = instName.title()
706 configFiles = {'fgcmCalibrateTractTable':
707 [os.path.join(ROOT,
708 'config',
709 f'fgcmCalibrateTractTable{instCamel}.py')]}
711 outputCollection = f'{instName}/{testName}/tract'
713 inputCollections = [f'{instName}/{testName}/lut',
714 'refcats/gen2']
716 queryString = f"tract={tract:d} and skymap='{skymapName:s}'"
718 self._runPipeline(self.repo,
719 os.path.join(ROOT,
720 'pipelines',
721 f'fgcmCalibrateTractTable{instCamel:s}.yaml'),
722 queryString=queryString,
723 configFiles=configFiles,
724 inputCollections=inputCollections,
725 outputCollection=outputCollection,
726 registerDatasetTypes=True)
728 butler = dafButler.Butler(self.repo)
730 whereClause = f"instrument='{instName:s}' and tract={tract:d} and skymap='{skymapName:s}'"
732 repRefs = butler.registry.queryDatasets('fgcmRawRepeatability',
733 dimensions=['tract'],
734 collections=outputCollection,
735 where=whereClause)
737 repeatabilityCat = butler.getDirect(list(repRefs)[0])
738 repeatability = repeatabilityCat['rawRepeatability'][:]
739 self.assertFloatsAlmostEqual(repeatability, rawRepeatability, atol=4e-6)
741 # Check that the number of photoCalib objects in each filter are what we expect
742 for filterName in filterNCalibMap.keys():
743 whereClause = (f"instrument='{instName:s}' and tract={tract:d} and "
744 f"physical_filter='{filterName:s}' and skymap='{skymapName:s}'")
746 refs = butler.registry.queryDatasets('fgcmPhotoCalibTractCatalog',
747 dimensions=['tract', 'physical_filter'],
748 collections=outputCollection,
749 where=whereClause)
751 count = 0
752 for ref in set(refs):
753 expCat = butler.getDirect(ref)
754 test, = np.where((expCat['visit'] > 0) & (expCat['id'] >= 0))
755 count += test.size
757 self.assertEqual(count, filterNCalibMap[filterName])
759 # Check that every visit got a transmission
760 for visit in visits:
761 whereClause = (f"instrument='{instName:s}' and tract={tract:d} and "
762 f"visit={visit:d} and skymap='{skymapName:s}'")
763 refs = butler.registry.queryDatasets('transmission_atmosphere_fgcm_tract',
764 dimensions=['tract', 'visit'],
765 collections=outputCollection,
766 where=whereClause)
767 self.assertEqual(len(set(refs)), 1)
769 @classmethod
770 def tearDownClass(cls):
771 """Tear down and clear directories
772 """
773 if os.path.exists(cls.testDir):
774 shutil.rmtree(cls.testDir, True)