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