Coverage for tests/fgcmcalTestBase.py: 9%
246 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-13 09:25 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-13 09:25 +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 """Test running of FgcmBuildStarsTableTask
233 Parameters
234 ----------
235 instName : `str`
236 Short name of the instrument.
237 testName : `str`
238 Base name of the test collection.
239 queryString : `str`
240 Query to send to the pipetask.
241 visits : `list`
242 List of visits to calibrate.
243 nStar : `int`
244 Number of stars expected.
245 nObs : `int`
246 Number of observations of stars expected.
247 """
248 instCamel = instName.title()
250 configFiles = {'fgcmBuildStarsTable': [os.path.join(ROOT,
251 'config',
252 f'fgcmBuildStarsTable{instCamel}.py')]}
253 outputCollection = f'{instName}/{testName}/buildstars'
255 self._runPipeline(self.repo,
256 os.path.join(ROOT,
257 'pipelines',
258 'fgcmBuildStarsTable%s.yaml' % (instCamel)),
259 configFiles=configFiles,
260 inputCollections=[f'{instName}/{testName}/lut',
261 'refcats/gen2'],
262 outputCollection=outputCollection,
263 queryString=queryString,
264 registerDatasetTypes=True)
266 butler = dafButler.Butler(self.repo)
268 visitCat = butler.get('fgcmVisitCatalog', collections=[outputCollection],
269 instrument=instName)
270 self.assertEqual(len(visits), len(visitCat))
272 starIds = butler.get('fgcmStarIds', collections=[outputCollection],
273 instrument=instName)
274 self.assertEqual(len(starIds), nStar)
276 starObs = butler.get('fgcmStarObservations', collections=[outputCollection],
277 instrument=instName)
278 self.assertEqual(len(starObs), nObs)
280 def _testFgcmBuildFromIsolatedStars(self, instName, testName, queryString, visits, nStar, nObs):
281 """Test running of FgcmBuildFromIsolatedStarsTask.
283 Parameters
284 ----------
285 instName : `str`
286 Short name of the instrument.
287 testName : `str`
288 Base name of the test collection.
289 queryString : `str`
290 Query to send to the pipetask.
291 visits : `list`
292 List of visits to calibrate.
293 nStar : `int`
294 Number of stars expected.
295 nObs : `int`
296 Number of observations of stars expected.
297 """
298 instCamel = instName.title()
300 configFiles = {'fgcmBuildFromIsolatedStars': [
301 os.path.join(ROOT,
302 'config',
303 f'fgcmBuildFromIsolatedStars{instCamel}.py')
304 ]}
305 outputCollection = f'{instName}/{testName}/buildstars'
307 self._runPipeline(self.repo,
308 os.path.join(ROOT,
309 'pipelines',
310 'fgcmBuildFromIsolatedStars%s.yaml' % (instCamel)),
311 configFiles=configFiles,
312 inputCollections=[f'{instName}/{testName}/lut',
313 'refcats/gen2'],
314 outputCollection=outputCollection,
315 queryString=queryString,
316 registerDatasetTypes=True)
318 butler = dafButler.Butler(self.repo)
320 visitCat = butler.get('fgcmVisitCatalog', collections=[outputCollection],
321 instrument=instName)
322 self.assertEqual(len(visits), len(visitCat))
324 starIds = butler.get('fgcm_star_ids', collections=[outputCollection],
325 instrument=instName)
326 self.assertEqual(len(starIds), nStar)
328 starObs = butler.get('fgcm_star_observations', collections=[outputCollection],
329 instrument=instName)
330 self.assertEqual(len(starObs), nObs)
332 def _testFgcmFitCycle(self, instName, testName, cycleNumber,
333 nZp, nGoodZp, nOkZp, nBadZp, nStdStars, nPlots,
334 skipChecks=False, extraConfig=None):
335 """Test running of FgcmFitCycleTask
337 Parameters
338 ----------
339 instName : `str`
340 Short name of the instrument.
341 testName : `str`
342 Base name of the test collection.
343 cycleNumber : `int`
344 Fit cycle number.
345 nZp : `int`
346 Number of zeropoints created by the task.
347 nGoodZp : `int`
348 Number of good (photometric) zeropoints created.
349 nOkZp : `int`
350 Number of constrained zeropoints (photometric or not).
351 nBadZp : `int`
352 Number of unconstrained (bad) zeropoints.
353 nStdStars : `int`
354 Number of standard stars produced.
355 nPlots : `int`
356 Number of plots produced.
357 skipChecks : `bool`, optional
358 Skip number checks, when running less-than-final cycle.
359 extraConfig : `str`, optional
360 Name of an extra config file to apply.
361 """
362 instCamel = instName.title()
364 configFiles = {'fgcmFitCycle': [os.path.join(ROOT,
365 'config',
366 f'fgcmFitCycle{instCamel}.py')]}
367 if extraConfig is not None:
368 configFiles['fgcmFitCycle'].append(extraConfig)
370 outputCollection = f'{instName}/{testName}/fit'
372 if cycleNumber == 0:
373 inputCollections = [f'{instName}/{testName}/buildstars']
374 else:
375 # In these tests we are running the fit cycle task multiple
376 # times into the same output collection. This code allows
377 # us to find the correct chained input collections to use
378 # so that we can both read from previous runs in the output
379 # collection and write to a new run in the output collection.
380 # Note that this behavior is handled automatically by the
381 # pipetask command-line interface, but not by the python
382 # API.
383 butler = dafButler.Butler(self.repo)
384 inputCollections = list(butler.registry.getCollectionChain(outputCollection))
386 cwd = os.getcwd()
387 runDir = os.path.join(self.testDir, testName)
388 os.makedirs(runDir, exist_ok=True)
389 os.chdir(runDir)
391 configOptions = {'fgcmFitCycle':
392 {'cycleNumber': f'{cycleNumber}',
393 'connections.previousCycleNumber': f'{cycleNumber - 1}',
394 'connections.cycleNumber': f'{cycleNumber}'}}
395 self._runPipeline(self.repo,
396 os.path.join(ROOT,
397 'pipelines',
398 f'fgcmFitCycle{instCamel}.yaml'),
399 configFiles=configFiles,
400 inputCollections=inputCollections,
401 outputCollection=outputCollection,
402 configOptions=configOptions,
403 registerDatasetTypes=True)
405 os.chdir(cwd)
407 if skipChecks:
408 return
410 butler = dafButler.Butler(self.repo)
412 config = butler.get('fgcmFitCycle_config', collections=[outputCollection])
414 # Check that the expected number of plots are there.
415 plots = glob.glob(os.path.join(runDir, config.outfileBase
416 + '_cycle%02d_plots/' % (cycleNumber)
417 + '*.png'))
418 self.assertEqual(len(plots), nPlots)
420 zps = butler.get('fgcmZeropoints%d' % (cycleNumber),
421 collections=[outputCollection],
422 instrument=instName)
423 self.assertEqual(len(zps), nZp)
425 gd, = np.where(zps['fgcmFlag'] == 1)
426 self.assertEqual(len(gd), nGoodZp)
428 ok, = np.where(zps['fgcmFlag'] < 16)
429 self.assertEqual(len(ok), nOkZp)
431 bd, = np.where(zps['fgcmFlag'] >= 16)
432 self.assertEqual(len(bd), nBadZp)
434 # Check that there are no illegal values with the ok zeropoints
435 test, = np.where(zps['fgcmZpt'][gd] < -9000.0)
436 self.assertEqual(len(test), 0)
438 stds = butler.get('fgcmStandardStars%d' % (cycleNumber),
439 collections=[outputCollection],
440 instrument=instName)
442 self.assertEqual(len(stds), nStdStars)
444 def _testFgcmOutputProducts(self, instName, testName,
445 zpOffsets, testVisit, testCcd, testFilter, testBandIndex):
446 """Test running of FgcmOutputProductsTask.
448 Parameters
449 ----------
450 instName : `str`
451 Short name of the instrument.
452 testName : `str`
453 Base name of the test collection.
454 zpOffsets : `np.ndarray`
455 Zeropoint offsets expected.
456 testVisit : `int`
457 Visit id to check for round-trip computations.
458 testCcd : `int`
459 Ccd id to check for round-trip computations.
460 testFilter : `str`
461 Filtername for testVisit/testCcd.
462 testBandIndex : `int`
463 Band index for testVisit/testCcd.
464 """
465 instCamel = instName.title()
467 configFiles = {'fgcmOutputProducts': [os.path.join(ROOT,
468 'config',
469 f'fgcmOutputProducts{instCamel}.py')]}
470 inputCollection = f'{instName}/{testName}/fit'
471 outputCollection = f'{instName}/{testName}/fit/output'
473 self._runPipeline(self.repo,
474 os.path.join(ROOT,
475 'pipelines',
476 'fgcmOutputProducts%s.yaml' % (instCamel)),
477 configFiles=configFiles,
478 inputCollections=[inputCollection],
479 outputCollection=outputCollection,
480 registerDatasetTypes=True)
482 butler = dafButler.Butler(self.repo)
483 offsetCat = butler.get('fgcmReferenceCalibrationOffsets',
484 collections=[outputCollection], instrument=instName)
485 offsets = offsetCat['offset'][:]
486 self.assertFloatsAlmostEqual(offsets, zpOffsets, atol=1e-6)
488 config = butler.get('fgcmOutputProducts_config',
489 collections=[outputCollection], instrument=instName)
491 rawStars = butler.get('fgcmStandardStars' + config.connections.cycleNumber,
492 collections=[inputCollection], instrument=instName)
494 # Test the fgcm_photoCalib output
495 zptCat = butler.get('fgcmZeropoints' + config.connections.cycleNumber,
496 collections=[inputCollection], instrument=instName)
498 good = (zptCat['fgcmFlag'] < 16)
499 bad = (zptCat['fgcmFlag'] >= 16)
501 # Read in all the calibrations, these should all be there
502 # This test is simply to ensure that all the photoCalib files exist
503 visits = np.unique(zptCat['visit'][good])
504 photoCalibDict = {}
505 for visit in visits:
506 expCat = butler.get('fgcmPhotoCalibCatalog',
507 visit=visit,
508 collections=[outputCollection], instrument=instName)
509 for row in expCat:
510 if row['visit'] == visit:
511 photoCalibDict[(visit, row['id'])] = row.getPhotoCalib()
513 # Check that all of the good photocalibs are there.
514 for rec in zptCat[good]:
515 self.assertTrue((rec['visit'], rec['detector']) in photoCalibDict)
517 # Check that none of the bad photocalibs are there.
518 for rec in zptCat[bad]:
519 self.assertFalse((rec['visit'], rec['detector']) in photoCalibDict)
521 # We do round-trip value checking on just the final one (chosen arbitrarily)
522 testCal = photoCalibDict[(testVisit, testCcd)]
524 src = butler.get('src', visit=int(testVisit), detector=int(testCcd),
525 collections=[outputCollection], instrument=instName)
527 # Only test sources with positive flux
528 gdSrc = (src['slot_CalibFlux_instFlux'] > 0.0)
530 # We need to apply the calibration offset to the fgcmzpt (which is internal
531 # and doesn't know about that yet)
532 testZpInd, = np.where((zptCat['visit'] == testVisit)
533 & (zptCat['detector'] == testCcd))
534 fgcmZpt = (zptCat['fgcmZpt'][testZpInd] + offsets[testBandIndex]
535 + zptCat['fgcmDeltaChrom'][testZpInd])
536 fgcmZptGrayErr = np.sqrt(zptCat['fgcmZptVar'][testZpInd])
538 if config.doComposeWcsJacobian:
539 # The raw zeropoint needs to be modified to know about the wcs jacobian
540 refs = butler.registry.queryDatasets('camera', dimensions=['instrument'],
541 collections=...)
542 camera = butler.get(list(refs)[0])
543 approxPixelAreaFields = fgcmcal.utilities.computeApproxPixelAreaFields(camera)
544 center = approxPixelAreaFields[testCcd].getBBox().getCenter()
545 pixAreaCorr = approxPixelAreaFields[testCcd].evaluate(center)
546 fgcmZpt += -2.5*np.log10(pixAreaCorr)
548 # This is the magnitude through the mean calibration
549 photoCalMeanCalMags = np.zeros(gdSrc.sum())
550 # This is the magnitude through the full focal-plane variable mags
551 photoCalMags = np.zeros_like(photoCalMeanCalMags)
552 # This is the magnitude with the FGCM (central-ccd) zeropoint
553 zptMeanCalMags = np.zeros_like(photoCalMeanCalMags)
555 for i, rec in enumerate(src[gdSrc]):
556 photoCalMeanCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'])
557 photoCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'],
558 rec.getCentroid())
559 zptMeanCalMags[i] = fgcmZpt - 2.5*np.log10(rec['slot_CalibFlux_instFlux'])
561 # These should be very close but some tiny differences because the fgcm value
562 # is defined at the center of the bbox, and the photoCal is the mean over the box
563 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
564 zptMeanCalMags, rtol=1e-6)
565 # These should be roughly equal, but not precisely because of the focal-plane
566 # variation. However, this is a useful sanity check for something going totally
567 # wrong.
568 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
569 photoCalMags, rtol=1e-2)
571 # The next test compares the "FGCM standard magnitudes" (which are output
572 # from the fgcm code itself) to the "calibrated magnitudes" that are
573 # obtained from running photoCalib.calibrateCatalog() on the original
574 # src catalogs. This summary comparison ensures that using photoCalibs
575 # yields the same results as what FGCM is computing internally.
576 # Note that we additionally need to take into account the post-processing
577 # offsets used in the tests.
579 # For decent statistics, we are matching all the sources from one visit
580 # (multiple ccds)
581 whereClause = f"instrument='{instName:s}' and visit={testVisit:d}"
582 srcRefs = butler.registry.queryDatasets('src', dimensions=['visit'],
583 collections='%s/testdata' % (instName),
584 where=whereClause,
585 findFirst=True)
586 photoCals = []
587 for srcRef in srcRefs:
588 photoCals.append(photoCalibDict[(testVisit, srcRef.dataId['detector'])])
590 matchMag, matchDelta = self._getMatchedVisitCat(butler, srcRefs, photoCals,
591 rawStars, testBandIndex, offsets)
593 st = np.argsort(matchMag)
594 # Compare the brightest 25% of stars. No matter the setting of
595 # deltaMagBkgOffsetPercentile, we want to ensure that these stars
596 # match on average.
597 brightest, = np.where(matchMag < matchMag[st[int(0.25*st.size)]])
598 self.assertFloatsAlmostEqual(np.median(matchDelta[brightest]), 0.0, atol=0.002)
600 # And the photoCal error is just the zeropoint gray error
601 self.assertFloatsAlmostEqual(testCal.getCalibrationErr(),
602 (np.log(10.0)/2.5)*testCal.getCalibrationMean()*fgcmZptGrayErr)
604 # Test the transmission output
605 visitCatalog = butler.get('fgcmVisitCatalog', collections=[inputCollection],
606 instrument=instName)
607 lutCat = butler.get('fgcmLookUpTable', collections=[inputCollection],
608 instrument=instName)
610 testTrans = butler.get('transmission_atmosphere_fgcm',
611 visit=visitCatalog[0]['visit'],
612 collections=[outputCollection], instrument=instName)
613 testResp = testTrans.sampleAt(position=geom.Point2D(0, 0),
614 wavelengths=lutCat[0]['atmLambda'])
616 # The test fit is performed with the atmosphere parameters frozen
617 # (freezeStdAtmosphere = True). Thus the only difference between
618 # these output atmospheres and the standard is the different
619 # airmass. Furthermore, this is a very rough comparison because
620 # the look-up table is computed with very coarse sampling for faster
621 # testing.
623 # To account for overall throughput changes, we scale by the median ratio,
624 # we only care about the shape
625 ratio = np.median(testResp/lutCat[0]['atmStdTrans'])
626 self.assertFloatsAlmostEqual(testResp/ratio, lutCat[0]['atmStdTrans'], atol=0.04)
628 # The second should be close to the first, but there is the airmass
629 # difference so they aren't identical.
630 testTrans2 = butler.get('transmission_atmosphere_fgcm',
631 visit=visitCatalog[1]['visit'],
632 collections=[outputCollection], instrument=instName)
633 testResp2 = testTrans2.sampleAt(position=geom.Point2D(0, 0),
634 wavelengths=lutCat[0]['atmLambda'])
636 # As above, we scale by the ratio to compare the shape of the curve.
637 ratio = np.median(testResp/testResp2)
638 self.assertFloatsAlmostEqual(testResp/ratio, testResp2, atol=0.04)
640 def _testFgcmMultiFit(self, instName, testName, queryString, visits, zpOffsets):
641 """Test running the full pipeline with multiple fit cycles.
643 Parameters
644 ----------
645 instName : `str`
646 Short name of the instrument.
647 testName : `str`
648 Base name of the test collection.
649 queryString : `str`
650 Query to send to the pipetask.
651 visits : `list`
652 List of visits to calibrate.
653 zpOffsets : `np.ndarray`
654 Zeropoint offsets expected.
655 """
656 instCamel = instName.title()
658 configFiles = {'fgcmBuildFromIsolatedStars': [
659 os.path.join(ROOT,
660 'config',
661 f'fgcmBuildFromIsolatedStars{instCamel}.py'
662 )],
663 'fgcmFitCycle': [os.path.join(ROOT,
664 'config',
665 f'fgcmFitCycle{instCamel}.py')],
666 'fgcmOutputProducts': [os.path.join(ROOT,
667 'config',
668 f'fgcmOutputProducts{instCamel}.py')]}
669 outputCollection = f'{instName}/{testName}/unified'
671 cwd = os.getcwd()
672 runDir = os.path.join(self.testDir, testName)
673 os.makedirs(runDir)
674 os.chdir(runDir)
676 self._runPipeline(self.repo,
677 os.path.join(ROOT,
678 'pipelines',
679 f'fgcmFullPipeline{instCamel}.yaml'),
680 configFiles=configFiles,
681 inputCollections=[f'{instName}/{testName}/lut',
682 'refcats/gen2'],
683 outputCollection=outputCollection,
684 queryString=queryString,
685 registerDatasetTypes=True)
687 os.chdir(cwd)
689 butler = dafButler.Butler(self.repo)
691 offsetCat = butler.get('fgcmReferenceCalibrationOffsets',
692 collections=[outputCollection], instrument=instName)
693 offsets = offsetCat['offset'][:]
694 self.assertFloatsAlmostEqual(offsets, zpOffsets, atol=1e-6)
696 def _getMatchedVisitCat(self, butler, srcHandles, photoCals,
697 rawStars, bandIndex, offsets):
698 """
699 Get a list of matched magnitudes and deltas from calibrated src catalogs.
701 Parameters
702 ----------
703 butler : `lsst.daf.butler.Butler`
704 srcHandles : `list`
705 Handles of source catalogs.
706 photoCals : `list`
707 photoCalib objects, matched to srcHandles.
708 rawStars : `lsst.afw.table.SourceCatalog`
709 Fgcm standard stars.
710 bandIndex : `int`
711 Index of the band for the source catalogs.
712 offsets : `np.ndarray`
713 Testing calibration offsets to apply to rawStars.
715 Returns
716 -------
717 matchMag : `np.ndarray`
718 Array of matched magnitudes.
719 matchDelta : `np.ndarray`
720 Array of matched deltas between src and standard stars.
721 """
722 matcher = esutil.htm.Matcher(11, np.rad2deg(rawStars['coord_ra']),
723 np.rad2deg(rawStars['coord_dec']))
725 matchDelta = None
726 for srcHandle, photoCal in zip(srcHandles, photoCals):
727 src = butler.get(srcHandle)
728 src = photoCal.calibrateCatalog(src)
730 gdSrc, = np.where(np.nan_to_num(src['slot_CalibFlux_flux']) > 0.0)
732 matches = matcher.match(np.rad2deg(src['coord_ra'][gdSrc]),
733 np.rad2deg(src['coord_dec'][gdSrc]),
734 1./3600., maxmatch=1)
736 srcMag = src['slot_CalibFlux_mag'][gdSrc][matches[0]]
737 # Apply offset here to the catalog mag
738 catMag = rawStars['mag_std_noabs'][matches[1]][:, bandIndex] + offsets[bandIndex]
739 delta = srcMag - catMag
740 if matchDelta is None:
741 matchDelta = delta
742 matchMag = catMag
743 else:
744 matchDelta = np.append(matchDelta, delta)
745 matchMag = np.append(matchMag, catMag)
747 return matchMag, matchDelta
749 def _testFgcmCalibrateTract(self, instName, testName, visits, tract, skymapName,
750 rawRepeatability, filterNCalibMap):
751 """Test running of FgcmCalibrateTractTask
753 Parameters
754 ----------
755 instName : `str`
756 Short name of the instrument.
757 testName : `str`
758 Base name of the test collection.
759 visits : `list`
760 List of visits to calibrate.
761 tract : `int`
762 Tract number.
763 skymapName : `str`
764 Name of the sky map.
765 rawRepeatability : `np.array`
766 Expected raw repeatability after convergence.
767 Length should be number of bands.
768 filterNCalibMap : `dict`
769 Mapping from filter name to number of photoCalibs created.
770 """
771 instCamel = instName.title()
773 configFiles = {'fgcmCalibrateTractTable':
774 [os.path.join(ROOT,
775 'config',
776 f'fgcmCalibrateTractTable{instCamel}.py')]}
778 outputCollection = f'{instName}/{testName}/tract'
780 inputCollections = [f'{instName}/{testName}/lut',
781 'refcats/gen2']
783 queryString = f"tract={tract:d} and skymap='{skymapName:s}'"
785 self._runPipeline(self.repo,
786 os.path.join(ROOT,
787 'pipelines',
788 f'fgcmCalibrateTractTable{instCamel:s}.yaml'),
789 queryString=queryString,
790 configFiles=configFiles,
791 inputCollections=inputCollections,
792 outputCollection=outputCollection,
793 registerDatasetTypes=True)
795 butler = dafButler.Butler(self.repo)
797 whereClause = f"instrument='{instName:s}' and tract={tract:d} and skymap='{skymapName:s}'"
799 repRefs = butler.registry.queryDatasets('fgcmRawRepeatability',
800 dimensions=['tract'],
801 collections=outputCollection,
802 where=whereClause)
804 repeatabilityCat = butler.get(list(repRefs)[0])
805 repeatability = repeatabilityCat['rawRepeatability'][:]
806 self.assertFloatsAlmostEqual(repeatability, rawRepeatability, atol=4e-6)
808 # Check that the number of photoCalib objects in each filter are what we expect
809 for filterName in filterNCalibMap.keys():
810 whereClause = (f"instrument='{instName:s}' and tract={tract:d} and "
811 f"physical_filter='{filterName:s}' and skymap='{skymapName:s}'")
813 refs = butler.registry.queryDatasets('fgcmPhotoCalibTractCatalog',
814 dimensions=['tract', 'physical_filter'],
815 collections=outputCollection,
816 where=whereClause)
818 count = 0
819 for ref in set(refs):
820 expCat = butler.get(ref)
821 test, = np.where((expCat['visit'] > 0) & (expCat['id'] >= 0))
822 count += test.size
824 self.assertEqual(count, filterNCalibMap[filterName])
826 # Check that every visit got a transmission
827 for visit in visits:
828 whereClause = (f"instrument='{instName:s}' and tract={tract:d} and "
829 f"visit={visit:d} and skymap='{skymapName:s}'")
830 refs = butler.registry.queryDatasets('transmission_atmosphere_fgcm_tract',
831 dimensions=['tract', 'visit'],
832 collections=outputCollection,
833 where=whereClause)
834 self.assertEqual(len(set(refs)), 1)
836 @classmethod
837 def tearDownClass(cls):
838 """Tear down and clear directories.
839 """
840 if os.path.exists(cls.testDir):
841 shutil.rmtree(cls.testDir, True)