Coverage for tests/fgcmcalTestBase.py: 10%
248 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-19 11:04 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-19 11:04 +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, ExecutionResources
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)
122 pipeline = Pipeline.fromFile(pipelineFile)
123 for taskName, fileList in configFiles.items():
124 for fileName in fileList:
125 pipeline.addConfigFile(taskName, fileName)
126 for taskName, configDict in configOptions.items():
127 for option, value in configDict.items():
128 pipeline.addConfigOverride(taskName, option, value)
130 resources = ExecutionResources(num_cores=1)
132 executor = SimplePipelineExecutor.from_pipeline(pipeline,
133 where=queryString,
134 root=repo,
135 butler=butler,
136 resources=resources)
137 quanta = executor.run(register_dataset_types=registerDatasetTypes)
139 return len(quanta)
141 def _testFgcmMakeLut(self, instName, testName, nBand, i0Std, i0Recon, i10Std, i10Recon):
142 """Test running of FgcmMakeLutTask
144 Parameters
145 ----------
146 instName : `str`
147 Short name of the instrument.
148 testName : `str`
149 Base name of the test collection.
150 nBand : `int`
151 Number of bands tested.
152 i0Std : `np.ndarray'
153 Values of i0Std to compare to.
154 i10Std : `np.ndarray`
155 Values of i10Std to compare to.
156 i0Recon : `np.ndarray`
157 Values of reconstructed i0 to compare to.
158 i10Recon : `np.ndarray`
159 Values of reconsntructed i10 to compare to.
160 """
161 instCamel = instName.title()
163 configFiles = {'fgcmMakeLut': [os.path.join(ROOT,
164 'config',
165 f'fgcmMakeLut{instCamel}.py')]}
166 outputCollection = f'{instName}/{testName}/lut'
168 self._runPipeline(self.repo,
169 os.path.join(ROOT,
170 'pipelines',
171 f'fgcmMakeLut{instCamel}.yaml'),
172 configFiles=configFiles,
173 inputCollections=[f'{instName}/calib', f'{instName}/testdata'],
174 outputCollection=outputCollection,
175 registerDatasetTypes=True)
177 # Check output values
178 butler = dafButler.Butler(self.repo)
179 lutCat = butler.get('fgcmLookUpTable',
180 collections=[outputCollection],
181 instrument=instName)
182 fgcmLut, lutIndexVals, lutStd = fgcmcal.utilities.translateFgcmLut(lutCat, {})
184 self.assertEqual(nBand, len(lutIndexVals[0]['FILTERNAMES']))
186 indices = fgcmLut.getIndices(np.arange(nBand, dtype=np.int32),
187 np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']),
188 np.zeros(nBand) + lutStd[0]['O3STD'],
189 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']),
190 np.zeros(nBand) + lutStd[0]['ALPHASTD'],
191 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])),
192 np.zeros(nBand, dtype=np.int32),
193 np.zeros(nBand) + lutStd[0]['PMBSTD'])
194 i0 = fgcmLut.computeI0(np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']),
195 np.zeros(nBand) + lutStd[0]['O3STD'],
196 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']),
197 np.zeros(nBand) + lutStd[0]['ALPHASTD'],
198 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])),
199 np.zeros(nBand) + lutStd[0]['PMBSTD'],
200 indices)
202 self.assertFloatsAlmostEqual(i0Recon, i0, msg='i0Recon', rtol=1e-5)
204 i1 = fgcmLut.computeI1(np.zeros(nBand) + np.log(lutStd[0]['PWVSTD']),
205 np.zeros(nBand) + lutStd[0]['O3STD'],
206 np.zeros(nBand) + np.log(lutStd[0]['TAUSTD']),
207 np.zeros(nBand) + lutStd[0]['ALPHASTD'],
208 np.zeros(nBand) + 1./np.cos(np.radians(lutStd[0]['ZENITHSTD'])),
209 np.zeros(nBand) + lutStd[0]['PMBSTD'],
210 indices)
212 self.assertFloatsAlmostEqual(i10Recon, i1/i0, msg='i10Recon', rtol=1e-5)
214 # Check that the standard atmosphere was output and non-zero.
215 atmStd = butler.get('fgcm_standard_atmosphere',
216 collections=[outputCollection],
217 instrument=instName)
218 bounds = atmStd.getWavelengthBounds()
219 lambdas = np.linspace(bounds[0], bounds[1], 1000)
220 tputs = atmStd.sampleAt(position=geom.Point2D(0.0, 0.0), wavelengths=lambdas)
221 self.assertGreater(np.min(tputs), 0.0)
223 # Check that the standard passbands were output and non-zero.
224 for physical_filter in fgcmLut.filterNames:
225 passband = butler.get('fgcm_standard_passband',
226 collections=[outputCollection],
227 instrument=instName,
228 physical_filter=physical_filter)
229 tputs = passband.sampleAt(position=geom.Point2D(0.0, 0.0), wavelengths=lambdas)
230 self.assertEqual(np.min(tputs), 0.0)
231 self.assertGreater(np.max(tputs), 0.0)
233 def _testFgcmBuildStarsTable(self, instName, testName, queryString, visits, nStar, nObs,
234 refcatCollection="refcats/gen2"):
235 """Test running of FgcmBuildStarsTableTask
237 Parameters
238 ----------
239 instName : `str`
240 Short name of the instrument.
241 testName : `str`
242 Base name of the test collection.
243 queryString : `str`
244 Query to send to the pipetask.
245 visits : `list`
246 List of visits to calibrate.
247 nStar : `int`
248 Number of stars expected.
249 nObs : `int`
250 Number of observations of stars expected.
251 refcatCollection : `str`, optional
252 Name of reference catalog collection.
253 """
254 instCamel = instName.title()
256 configFiles = {'fgcmBuildStarsTable': [os.path.join(ROOT,
257 'config',
258 f'fgcmBuildStarsTable{instCamel}.py')]}
259 outputCollection = f'{instName}/{testName}/buildstars'
261 self._runPipeline(self.repo,
262 os.path.join(ROOT,
263 'pipelines',
264 'fgcmBuildStarsTable%s.yaml' % (instCamel)),
265 configFiles=configFiles,
266 inputCollections=[f'{instName}/{testName}/lut',
267 refcatCollection],
268 outputCollection=outputCollection,
269 queryString=queryString,
270 registerDatasetTypes=True)
272 butler = dafButler.Butler(self.repo)
274 visitCat = butler.get('fgcmVisitCatalog', collections=[outputCollection],
275 instrument=instName)
276 self.assertEqual(len(visits), len(visitCat))
278 starIds = butler.get('fgcmStarIds', collections=[outputCollection],
279 instrument=instName)
280 self.assertEqual(len(starIds), nStar)
282 starObs = butler.get('fgcmStarObservations', collections=[outputCollection],
283 instrument=instName)
284 self.assertEqual(len(starObs), nObs)
286 def _testFgcmBuildFromIsolatedStars(self, instName, testName, queryString, visits, nStar, nObs,
287 refcatCollection="refcats/gen2"):
288 """Test running of FgcmBuildFromIsolatedStarsTask.
290 Parameters
291 ----------
292 instName : `str`
293 Short name of the instrument.
294 testName : `str`
295 Base name of the test collection.
296 queryString : `str`
297 Query to send to the pipetask.
298 visits : `list`
299 List of visits to calibrate.
300 nStar : `int`
301 Number of stars expected.
302 nObs : `int`
303 Number of observations of stars expected.
304 refcatCollection : `str`, optional
305 Name of reference catalog collection.
306 """
307 instCamel = instName.title()
309 configFiles = {'fgcmBuildFromIsolatedStars': [
310 os.path.join(ROOT,
311 'config',
312 f'fgcmBuildFromIsolatedStars{instCamel}.py')
313 ]}
314 outputCollection = f'{instName}/{testName}/buildstars'
316 self._runPipeline(self.repo,
317 os.path.join(ROOT,
318 'pipelines',
319 'fgcmBuildFromIsolatedStars%s.yaml' % (instCamel)),
320 configFiles=configFiles,
321 inputCollections=[f'{instName}/{testName}/lut',
322 refcatCollection],
323 outputCollection=outputCollection,
324 queryString=queryString,
325 registerDatasetTypes=True)
327 butler = dafButler.Butler(self.repo)
329 visitCat = butler.get('fgcmVisitCatalog', collections=[outputCollection],
330 instrument=instName)
331 self.assertEqual(len(visits), len(visitCat))
333 starIds = butler.get('fgcm_star_ids', collections=[outputCollection],
334 instrument=instName)
335 self.assertEqual(len(starIds), nStar)
337 starObs = butler.get('fgcm_star_observations', collections=[outputCollection],
338 instrument=instName)
339 self.assertEqual(len(starObs), nObs)
341 def _testFgcmFitCycle(self, instName, testName, cycleNumber,
342 nZp, nGoodZp, nOkZp, nBadZp, nStdStars, nPlots,
343 skipChecks=False, extraConfig=None):
344 """Test running of FgcmFitCycleTask
346 Parameters
347 ----------
348 instName : `str`
349 Short name of the instrument.
350 testName : `str`
351 Base name of the test collection.
352 cycleNumber : `int`
353 Fit cycle number.
354 nZp : `int`
355 Number of zeropoints created by the task.
356 nGoodZp : `int`
357 Number of good (photometric) zeropoints created.
358 nOkZp : `int`
359 Number of constrained zeropoints (photometric or not).
360 nBadZp : `int`
361 Number of unconstrained (bad) zeropoints.
362 nStdStars : `int`
363 Number of standard stars produced.
364 nPlots : `int`
365 Number of plots produced.
366 skipChecks : `bool`, optional
367 Skip number checks, when running less-than-final cycle.
368 extraConfig : `str`, optional
369 Name of an extra config file to apply.
370 """
371 instCamel = instName.title()
373 configFiles = {'fgcmFitCycle': [os.path.join(ROOT,
374 'config',
375 f'fgcmFitCycle{instCamel}.py')]}
376 if extraConfig is not None:
377 configFiles['fgcmFitCycle'].append(extraConfig)
379 outputCollection = f'{instName}/{testName}/fit'
381 if cycleNumber == 0:
382 inputCollections = [f'{instName}/{testName}/buildstars']
383 else:
384 # In these tests we are running the fit cycle task multiple
385 # times into the same output collection. This code allows
386 # us to find the correct chained input collections to use
387 # so that we can both read from previous runs in the output
388 # collection and write to a new run in the output collection.
389 # Note that this behavior is handled automatically by the
390 # pipetask command-line interface, but not by the python
391 # API.
392 butler = dafButler.Butler(self.repo)
393 inputCollections = list(butler.registry.getCollectionChain(outputCollection))
395 cwd = os.getcwd()
396 runDir = os.path.join(self.testDir, testName)
397 os.makedirs(runDir, exist_ok=True)
398 os.chdir(runDir)
400 configOptions = {'fgcmFitCycle':
401 {'cycleNumber': f'{cycleNumber}',
402 'connections.previousCycleNumber': f'{cycleNumber - 1}',
403 'connections.cycleNumber': f'{cycleNumber}'}}
404 self._runPipeline(self.repo,
405 os.path.join(ROOT,
406 'pipelines',
407 f'fgcmFitCycle{instCamel}.yaml'),
408 configFiles=configFiles,
409 inputCollections=inputCollections,
410 outputCollection=outputCollection,
411 configOptions=configOptions,
412 registerDatasetTypes=True)
414 os.chdir(cwd)
416 if skipChecks:
417 return
419 butler = dafButler.Butler(self.repo)
421 config = butler.get('fgcmFitCycle_config', collections=[outputCollection])
423 # Check that the expected number of plots are there.
424 plots = glob.glob(os.path.join(runDir, config.outfileBase
425 + '_cycle%02d_plots/' % (cycleNumber)
426 + '*.png'))
427 self.assertEqual(len(plots), nPlots)
429 zps = butler.get('fgcmZeropoints%d' % (cycleNumber),
430 collections=[outputCollection],
431 instrument=instName)
432 self.assertEqual(len(zps), nZp)
434 gd, = np.where(zps['fgcmFlag'] == 1)
435 self.assertEqual(len(gd), nGoodZp)
437 ok, = np.where(zps['fgcmFlag'] < 16)
438 self.assertEqual(len(ok), nOkZp)
440 bd, = np.where(zps['fgcmFlag'] >= 16)
441 self.assertEqual(len(bd), nBadZp)
443 # Check that there are no illegal values with the ok zeropoints
444 test, = np.where(zps['fgcmZpt'][gd] < -9000.0)
445 self.assertEqual(len(test), 0)
447 stds = butler.get('fgcmStandardStars%d' % (cycleNumber),
448 collections=[outputCollection],
449 instrument=instName)
451 self.assertEqual(len(stds), nStdStars)
453 def _testFgcmOutputProducts(self, instName, testName,
454 zpOffsets, testVisit, testCcd, testFilter, testBandIndex,
455 testSrc=True):
456 """Test running of FgcmOutputProductsTask.
458 Parameters
459 ----------
460 instName : `str`
461 Short name of the instrument.
462 testName : `str`
463 Base name of the test collection.
464 zpOffsets : `np.ndarray`
465 Zeropoint offsets expected.
466 testVisit : `int`
467 Visit id to check for round-trip computations.
468 testCcd : `int`
469 Ccd id to check for round-trip computations.
470 testFilter : `str`
471 Filtername for testVisit/testCcd.
472 testBandIndex : `int`
473 Band index for testVisit/testCcd.
474 testSrc : `bool`, optional
475 Test the source catalogs? (Only if available in dataset.)
476 """
477 instCamel = instName.title()
479 configFiles = {'fgcmOutputProducts': [os.path.join(ROOT,
480 'config',
481 f'fgcmOutputProducts{instCamel}.py')]}
482 inputCollection = f'{instName}/{testName}/fit'
483 outputCollection = f'{instName}/{testName}/fit/output'
485 self._runPipeline(self.repo,
486 os.path.join(ROOT,
487 'pipelines',
488 'fgcmOutputProducts%s.yaml' % (instCamel)),
489 configFiles=configFiles,
490 inputCollections=[inputCollection],
491 outputCollection=outputCollection,
492 registerDatasetTypes=True)
494 butler = dafButler.Butler(self.repo)
495 offsetCat = butler.get('fgcmReferenceCalibrationOffsets',
496 collections=[outputCollection], instrument=instName)
497 offsets = offsetCat['offset'][:]
498 self.assertFloatsAlmostEqual(offsets, zpOffsets, atol=1e-6)
500 config = butler.get('fgcmOutputProducts_config',
501 collections=[outputCollection], instrument=instName)
503 rawStars = butler.get('fgcmStandardStars' + config.connections.cycleNumber,
504 collections=[inputCollection], instrument=instName)
506 # Test the fgcm_photoCalib output
507 zptCat = butler.get('fgcmZeropoints' + config.connections.cycleNumber,
508 collections=[inputCollection], instrument=instName)
510 good = (zptCat['fgcmFlag'] < 16)
511 bad = (zptCat['fgcmFlag'] >= 16)
513 # Read in all the calibrations, these should all be there
514 # This test is simply to ensure that all the photoCalib files exist
515 visits = np.unique(zptCat['visit'][good])
516 photoCalibDict = {}
517 for visit in visits:
518 expCat = butler.get('fgcmPhotoCalibCatalog',
519 visit=visit,
520 collections=[outputCollection], instrument=instName)
521 for row in expCat:
522 if row['visit'] == visit:
523 photoCalibDict[(visit, row['id'])] = row.getPhotoCalib()
525 # Check that all of the good photocalibs are there.
526 for rec in zptCat[good]:
527 self.assertTrue((rec['visit'], rec['detector']) in photoCalibDict)
529 # Check that none of the bad photocalibs are there.
530 for rec in zptCat[bad]:
531 self.assertFalse((rec['visit'], rec['detector']) in photoCalibDict)
533 # We do round-trip value checking on just the final one (chosen arbitrarily)
534 testCal = photoCalibDict[(testVisit, testCcd)]
536 if testSrc:
537 src = butler.get('src', visit=int(testVisit), detector=int(testCcd),
538 collections=[outputCollection], instrument=instName)
540 # Only test sources with positive flux
541 gdSrc = (src['slot_CalibFlux_instFlux'] > 0.0)
543 # We need to apply the calibration offset to the fgcmzpt (which is internal
544 # and doesn't know about that yet)
545 testZpInd, = np.where((zptCat['visit'] == testVisit)
546 & (zptCat['detector'] == testCcd))
547 fgcmZpt = (zptCat['fgcmZpt'][testZpInd] + offsets[testBandIndex]
548 + zptCat['fgcmDeltaChrom'][testZpInd])
549 fgcmZptGrayErr = np.sqrt(zptCat['fgcmZptVar'][testZpInd])
551 if config.doComposeWcsJacobian:
552 # The raw zeropoint needs to be modified to know about the wcs jacobian
553 refs = butler.registry.queryDatasets('camera', dimensions=['instrument'],
554 collections=...)
555 camera = butler.get(list(refs)[0])
556 approxPixelAreaFields = fgcmcal.utilities.computeApproxPixelAreaFields(camera)
557 center = approxPixelAreaFields[testCcd].getBBox().getCenter()
558 pixAreaCorr = approxPixelAreaFields[testCcd].evaluate(center)
559 fgcmZpt += -2.5*np.log10(pixAreaCorr)
561 # This is the magnitude through the mean calibration
562 photoCalMeanCalMags = np.zeros(gdSrc.sum())
563 # This is the magnitude through the full focal-plane variable mags
564 photoCalMags = np.zeros_like(photoCalMeanCalMags)
565 # This is the magnitude with the FGCM (central-ccd) zeropoint
566 zptMeanCalMags = np.zeros_like(photoCalMeanCalMags)
568 for i, rec in enumerate(src[gdSrc]):
569 photoCalMeanCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'])
570 photoCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'],
571 rec.getCentroid())
572 zptMeanCalMags[i] = fgcmZpt[0] - 2.5*np.log10(rec['slot_CalibFlux_instFlux'])
574 # These should be very close but some tiny differences because the fgcm value
575 # is defined at the center of the bbox, and the photoCal is the mean over the box
576 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
577 zptMeanCalMags, rtol=1e-6)
578 # These should be roughly equal, but not precisely because of the focal-plane
579 # variation. However, this is a useful sanity check for something going totally
580 # wrong.
581 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
582 photoCalMags, rtol=1e-2)
584 # The next test compares the "FGCM standard magnitudes" (which are output
585 # from the fgcm code itself) to the "calibrated magnitudes" that are
586 # obtained from running photoCalib.calibrateCatalog() on the original
587 # src catalogs. This summary comparison ensures that using photoCalibs
588 # yields the same results as what FGCM is computing internally.
589 # Note that we additionally need to take into account the post-processing
590 # offsets used in the tests.
592 # For decent statistics, we are matching all the sources from one visit
593 # (multiple ccds)
594 whereClause = f"instrument='{instName:s}' and visit={testVisit:d}"
595 srcRefs = butler.registry.queryDatasets('src', dimensions=['visit'],
596 collections='%s/testdata' % (instName),
597 where=whereClause,
598 findFirst=True)
599 photoCals = []
600 for srcRef in srcRefs:
601 photoCals.append(photoCalibDict[(testVisit, srcRef.dataId['detector'])])
603 matchMag, matchDelta = self._getMatchedVisitCat(butler, srcRefs, photoCals,
604 rawStars, testBandIndex, offsets)
606 st = np.argsort(matchMag)
607 # Compare the brightest 25% of stars. No matter the setting of
608 # deltaMagBkgOffsetPercentile, we want to ensure that these stars
609 # match on average.
610 brightest, = np.where(matchMag < matchMag[st[int(0.25*st.size)]])
611 self.assertFloatsAlmostEqual(np.median(matchDelta[brightest]), 0.0, atol=0.002)
613 # And the photoCal error is just the zeropoint gray error
614 self.assertFloatsAlmostEqual(testCal.getCalibrationErr(),
615 (np.log(10.0)/2.5)*testCal.getCalibrationMean()*fgcmZptGrayErr)
617 # Test the transmission output
618 visitCatalog = butler.get('fgcmVisitCatalog', collections=[inputCollection],
619 instrument=instName)
620 lutCat = butler.get('fgcmLookUpTable', collections=[inputCollection],
621 instrument=instName)
623 testTrans = butler.get('transmission_atmosphere_fgcm',
624 visit=visitCatalog[0]['visit'],
625 collections=[outputCollection], instrument=instName)
626 testResp = testTrans.sampleAt(position=geom.Point2D(0, 0),
627 wavelengths=lutCat[0]['atmLambda'])
629 # The test fit is performed with the atmosphere parameters frozen
630 # (freezeStdAtmosphere = True). Thus the only difference between
631 # these output atmospheres and the standard is the different
632 # airmass. Furthermore, this is a very rough comparison because
633 # the look-up table is computed with very coarse sampling for faster
634 # testing.
636 # To account for overall throughput changes, we scale by the median ratio,
637 # we only care about the shape
638 ratio = np.median(testResp/lutCat[0]['atmStdTrans'])
639 self.assertFloatsAlmostEqual(testResp/ratio, lutCat[0]['atmStdTrans'], atol=0.2)
641 # The second should be close to the first, but there is the airmass
642 # difference so they aren't identical.
643 testTrans2 = butler.get('transmission_atmosphere_fgcm',
644 visit=visitCatalog[1]['visit'],
645 collections=[outputCollection], instrument=instName)
646 testResp2 = testTrans2.sampleAt(position=geom.Point2D(0, 0),
647 wavelengths=lutCat[0]['atmLambda'])
649 # As above, we scale by the ratio to compare the shape of the curve.
650 ratio = np.median(testResp/testResp2)
651 self.assertFloatsAlmostEqual(testResp/ratio, testResp2, atol=0.04)
653 def _testFgcmMultiFit(self, instName, testName, queryString, visits, zpOffsets,
654 refcatCollection="refcats/gen2"):
655 """Test running the full pipeline with multiple fit cycles.
657 Parameters
658 ----------
659 instName : `str`
660 Short name of the instrument.
661 testName : `str`
662 Base name of the test collection.
663 queryString : `str`
664 Query to send to the pipetask.
665 visits : `list`
666 List of visits to calibrate.
667 zpOffsets : `np.ndarray`
668 Zeropoint offsets expected.
669 refcatCollection : `str`, optional
670 Name of reference catalog collection.
671 """
672 instCamel = instName.title()
674 configFiles = {'fgcmBuildFromIsolatedStars': [
675 os.path.join(ROOT,
676 'config',
677 f'fgcmBuildFromIsolatedStars{instCamel}.py'
678 )],
679 'fgcmFitCycle': [os.path.join(ROOT,
680 'config',
681 f'fgcmFitCycle{instCamel}.py')],
682 'fgcmOutputProducts': [os.path.join(ROOT,
683 'config',
684 f'fgcmOutputProducts{instCamel}.py')]}
685 outputCollection = f'{instName}/{testName}/unified'
687 cwd = os.getcwd()
688 runDir = os.path.join(self.testDir, testName)
689 os.makedirs(runDir)
690 os.chdir(runDir)
692 self._runPipeline(self.repo,
693 os.path.join(ROOT,
694 'pipelines',
695 f'fgcmFullPipeline{instCamel}.yaml'),
696 configFiles=configFiles,
697 inputCollections=[f'{instName}/{testName}/lut',
698 refcatCollection],
699 outputCollection=outputCollection,
700 queryString=queryString,
701 registerDatasetTypes=True)
703 os.chdir(cwd)
705 butler = dafButler.Butler(self.repo)
707 offsetCat = butler.get('fgcmReferenceCalibrationOffsets',
708 collections=[outputCollection], instrument=instName)
709 offsets = offsetCat['offset'][:]
710 self.assertFloatsAlmostEqual(offsets, zpOffsets, atol=1e-6)
712 def _getMatchedVisitCat(self, butler, srcHandles, photoCals,
713 rawStars, bandIndex, offsets):
714 """
715 Get a list of matched magnitudes and deltas from calibrated src catalogs.
717 Parameters
718 ----------
719 butler : `lsst.daf.butler.Butler`
720 srcHandles : `list`
721 Handles of source catalogs.
722 photoCals : `list`
723 photoCalib objects, matched to srcHandles.
724 rawStars : `lsst.afw.table.SourceCatalog`
725 Fgcm standard stars.
726 bandIndex : `int`
727 Index of the band for the source catalogs.
728 offsets : `np.ndarray`
729 Testing calibration offsets to apply to rawStars.
731 Returns
732 -------
733 matchMag : `np.ndarray`
734 Array of matched magnitudes.
735 matchDelta : `np.ndarray`
736 Array of matched deltas between src and standard stars.
737 """
738 matcher = esutil.htm.Matcher(11, np.rad2deg(rawStars['coord_ra']),
739 np.rad2deg(rawStars['coord_dec']))
741 matchDelta = None
742 for srcHandle, photoCal in zip(srcHandles, photoCals):
743 src = butler.get(srcHandle)
744 src = photoCal.calibrateCatalog(src)
746 gdSrc, = np.where(np.nan_to_num(src['slot_CalibFlux_flux']) > 0.0)
748 matches = matcher.match(np.rad2deg(src['coord_ra'][gdSrc]),
749 np.rad2deg(src['coord_dec'][gdSrc]),
750 1./3600., maxmatch=1)
752 srcMag = src['slot_CalibFlux_mag'][gdSrc][matches[0]]
753 # Apply offset here to the catalog mag
754 catMag = rawStars['mag_std_noabs'][matches[1]][:, bandIndex] + offsets[bandIndex]
755 delta = srcMag - catMag
756 if matchDelta is None:
757 matchDelta = delta
758 matchMag = catMag
759 else:
760 matchDelta = np.append(matchDelta, delta)
761 matchMag = np.append(matchMag, catMag)
763 return matchMag, matchDelta
765 def _testFgcmCalibrateTract(self, instName, testName, visits, tract, skymapName,
766 rawRepeatability, filterNCalibMap):
767 """Test running of FgcmCalibrateTractTask
769 Parameters
770 ----------
771 instName : `str`
772 Short name of the instrument.
773 testName : `str`
774 Base name of the test collection.
775 visits : `list`
776 List of visits to calibrate.
777 tract : `int`
778 Tract number.
779 skymapName : `str`
780 Name of the sky map.
781 rawRepeatability : `np.array`
782 Expected raw repeatability after convergence.
783 Length should be number of bands.
784 filterNCalibMap : `dict`
785 Mapping from filter name to number of photoCalibs created.
786 """
787 instCamel = instName.title()
789 configFiles = {'fgcmCalibrateTractTable':
790 [os.path.join(ROOT,
791 'config',
792 f'fgcmCalibrateTractTable{instCamel}.py')]}
794 outputCollection = f'{instName}/{testName}/tract'
796 inputCollections = [f'{instName}/{testName}/lut',
797 'refcats/gen2']
799 queryString = f"tract={tract:d} and skymap='{skymapName:s}'"
801 self._runPipeline(self.repo,
802 os.path.join(ROOT,
803 'pipelines',
804 f'fgcmCalibrateTractTable{instCamel:s}.yaml'),
805 queryString=queryString,
806 configFiles=configFiles,
807 inputCollections=inputCollections,
808 outputCollection=outputCollection,
809 registerDatasetTypes=True)
811 butler = dafButler.Butler(self.repo)
813 whereClause = f"instrument='{instName:s}' and tract={tract:d} and skymap='{skymapName:s}'"
815 repRefs = butler.registry.queryDatasets('fgcmRawRepeatability',
816 dimensions=['tract'],
817 collections=outputCollection,
818 where=whereClause)
820 repeatabilityCat = butler.get(list(repRefs)[0])
821 repeatability = repeatabilityCat['rawRepeatability'][:]
822 self.assertFloatsAlmostEqual(repeatability, rawRepeatability, atol=4e-6)
824 # Check that the number of photoCalib objects in each filter are what we expect
825 for filterName in filterNCalibMap.keys():
826 whereClause = (f"instrument='{instName:s}' and tract={tract:d} and "
827 f"physical_filter='{filterName:s}' and skymap='{skymapName:s}'")
829 refs = butler.registry.queryDatasets('fgcmPhotoCalibTractCatalog',
830 dimensions=['tract', 'physical_filter'],
831 collections=outputCollection,
832 where=whereClause)
834 count = 0
835 for ref in set(refs):
836 expCat = butler.get(ref)
837 test, = np.where((expCat['visit'] > 0) & (expCat['id'] >= 0))
838 count += test.size
840 self.assertEqual(count, filterNCalibMap[filterName])
842 # Check that every visit got a transmission
843 for visit in visits:
844 whereClause = (f"instrument='{instName:s}' and tract={tract:d} and "
845 f"visit={visit:d} and skymap='{skymapName:s}'")
846 refs = butler.registry.queryDatasets('transmission_atmosphere_fgcm_tract',
847 dimensions=['tract', 'visit'],
848 collections=outputCollection,
849 where=whereClause)
850 self.assertEqual(len(set(refs)), 1)
852 @classmethod
853 def tearDownClass(cls):
854 """Tear down and clear directories.
855 """
856 if os.path.exists(cls.testDir):
857 shutil.rmtree(cls.testDir, True)