Coverage for tests/fgcmcalTestBase.py: 9%
237 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-11 12:25 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-11 12: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)
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 # Check that the standard atmosphere was output and non-zero.
213 atmStd = butler.get('fgcm_standard_atmosphere',
214 collections=[outputCollection],
215 instrument=instName)
216 bounds = atmStd.getWavelengthBounds()
217 lambdas = np.linspace(bounds[0], bounds[1], 1000)
218 tputs = atmStd.sampleAt(position=geom.Point2D(0.0, 0.0), wavelengths=lambdas)
219 self.assertGreater(np.min(tputs), 0.0)
221 # Check that the standard passbands were output and non-zero.
222 for physical_filter in fgcmLut.filterNames:
223 passband = butler.get('fgcm_standard_passband',
224 collections=[outputCollection],
225 instrument=instName,
226 physical_filter=physical_filter)
227 tputs = passband.sampleAt(position=geom.Point2D(0.0, 0.0), wavelengths=lambdas)
228 self.assertEqual(np.min(tputs), 0.0)
229 self.assertGreater(np.max(tputs), 0.0)
231 def _testFgcmBuildStarsTable(self, instName, testName, queryString, visits, nStar, nObs):
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 """
249 instCamel = instName.title()
251 configFiles = {'fgcmBuildStarsTable': [os.path.join(ROOT,
252 'config',
253 f'fgcmBuildStarsTable{instCamel}.py')]}
254 outputCollection = f'{instName}/{testName}/buildstars'
256 self._runPipeline(self.repo,
257 os.path.join(ROOT,
258 'pipelines',
259 'fgcmBuildStarsTable%s.yaml' % (instCamel)),
260 configFiles=configFiles,
261 inputCollections=[f'{instName}/{testName}/lut',
262 'refcats/gen2'],
263 outputCollection=outputCollection,
264 queryString=queryString,
265 registerDatasetTypes=True)
267 butler = dafButler.Butler(self.repo)
269 visitCat = butler.get('fgcmVisitCatalog', collections=[outputCollection],
270 instrument=instName)
271 self.assertEqual(len(visits), len(visitCat))
273 starIds = butler.get('fgcmStarIds', collections=[outputCollection],
274 instrument=instName)
275 self.assertEqual(len(starIds), nStar)
277 starObs = butler.get('fgcmStarObservations', collections=[outputCollection],
278 instrument=instName)
279 self.assertEqual(len(starObs), nObs)
281 def _testFgcmFitCycle(self, instName, testName, cycleNumber,
282 nZp, nGoodZp, nOkZp, nBadZp, nStdStars, nPlots,
283 skipChecks=False, extraConfig=None):
284 """Test running of FgcmFitCycleTask
286 Parameters
287 ----------
288 instName : `str`
289 Short name of the instrument
290 testName : `str`
291 Base name of the test collection
292 cycleNumber : `int`
293 Fit cycle number.
294 nZp : `int`
295 Number of zeropoints created by the task
296 nGoodZp : `int`
297 Number of good (photometric) zeropoints created
298 nOkZp : `int`
299 Number of constrained zeropoints (photometric or not)
300 nBadZp : `int`
301 Number of unconstrained (bad) zeropoints
302 nStdStars : `int`
303 Number of standard stars produced
304 nPlots : `int`
305 Number of plots produced
306 skipChecks : `bool`, optional
307 Skip number checks, when running less-than-final cycle.
308 extraConfig : `str`, optional
309 Name of an extra config file to apply.
310 """
311 instCamel = instName.title()
313 configFiles = {'fgcmFitCycle': [os.path.join(ROOT,
314 'config',
315 f'fgcmFitCycle{instCamel}.py')]}
316 if extraConfig is not None:
317 configFiles['fgcmFitCycle'].append(extraConfig)
319 outputCollection = f'{instName}/{testName}/fit'
321 if cycleNumber == 0:
322 inputCollections = [f'{instName}/{testName}/buildstars']
323 else:
324 # In these tests we are running the fit cycle task multiple
325 # times into the same output collection. This code allows
326 # us to find the correct chained input collections to use
327 # so that we can both read from previous runs in the output
328 # collection and write to a new run in the output collection.
329 # Note that this behavior is handled automatically by the
330 # pipetask command-line interface, but not by the python
331 # API.
332 butler = dafButler.Butler(self.repo)
333 inputCollections = list(butler.registry.getCollectionChain(outputCollection))
335 cwd = os.getcwd()
336 runDir = os.path.join(self.testDir, testName)
337 os.makedirs(runDir, exist_ok=True)
338 os.chdir(runDir)
340 configOptions = {'fgcmFitCycle':
341 {'cycleNumber': f'{cycleNumber}',
342 'connections.previousCycleNumber': f'{cycleNumber - 1}',
343 'connections.cycleNumber': f'{cycleNumber}'}}
344 self._runPipeline(self.repo,
345 os.path.join(ROOT,
346 'pipelines',
347 f'fgcmFitCycle{instCamel}.yaml'),
348 configFiles=configFiles,
349 inputCollections=inputCollections,
350 outputCollection=outputCollection,
351 configOptions=configOptions,
352 registerDatasetTypes=True)
354 os.chdir(cwd)
356 if skipChecks:
357 return
359 butler = dafButler.Butler(self.repo)
361 config = butler.get('fgcmFitCycle_config', collections=[outputCollection])
363 # Check that the expected number of plots are there.
364 plots = glob.glob(os.path.join(runDir, config.outfileBase
365 + '_cycle%02d_plots/' % (cycleNumber)
366 + '*.png'))
367 self.assertEqual(len(plots), nPlots)
369 zps = butler.get('fgcmZeropoints%d' % (cycleNumber),
370 collections=[outputCollection],
371 instrument=instName)
372 self.assertEqual(len(zps), nZp)
374 gd, = np.where(zps['fgcmFlag'] == 1)
375 self.assertEqual(len(gd), nGoodZp)
377 ok, = np.where(zps['fgcmFlag'] < 16)
378 self.assertEqual(len(ok), nOkZp)
380 bd, = np.where(zps['fgcmFlag'] >= 16)
381 self.assertEqual(len(bd), nBadZp)
383 # Check that there are no illegal values with the ok zeropoints
384 test, = np.where(zps['fgcmZpt'][gd] < -9000.0)
385 self.assertEqual(len(test), 0)
387 stds = butler.get('fgcmStandardStars%d' % (cycleNumber),
388 collections=[outputCollection],
389 instrument=instName)
391 self.assertEqual(len(stds), nStdStars)
393 def _testFgcmOutputProducts(self, instName, testName,
394 zpOffsets, testVisit, testCcd, testFilter, testBandIndex):
395 """Test running of FgcmOutputProductsTask.
397 Parameters
398 ----------
399 instName : `str`
400 Short name of the instrument
401 testName : `str`
402 Base name of the test collection
403 zpOffsets : `np.ndarray`
404 Zeropoint offsets expected
405 testVisit : `int`
406 Visit id to check for round-trip computations
407 testCcd : `int`
408 Ccd id to check for round-trip computations
409 testFilter : `str`
410 Filtername for testVisit/testCcd
411 testBandIndex : `int`
412 Band index for testVisit/testCcd
413 """
414 instCamel = instName.title()
416 configFiles = {'fgcmOutputProducts': [os.path.join(ROOT,
417 'config',
418 f'fgcmOutputProducts{instCamel}.py')]}
419 inputCollection = f'{instName}/{testName}/fit'
420 outputCollection = f'{instName}/{testName}/fit/output'
422 self._runPipeline(self.repo,
423 os.path.join(ROOT,
424 'pipelines',
425 'fgcmOutputProducts%s.yaml' % (instCamel)),
426 configFiles=configFiles,
427 inputCollections=[inputCollection],
428 outputCollection=outputCollection,
429 registerDatasetTypes=True)
431 butler = dafButler.Butler(self.repo)
432 offsetCat = butler.get('fgcmReferenceCalibrationOffsets',
433 collections=[outputCollection], instrument=instName)
434 offsets = offsetCat['offset'][:]
435 self.assertFloatsAlmostEqual(offsets, zpOffsets, atol=1e-6)
437 config = butler.get('fgcmOutputProducts_config',
438 collections=[outputCollection], instrument=instName)
440 rawStars = butler.get('fgcmStandardStars' + config.connections.cycleNumber,
441 collections=[inputCollection], instrument=instName)
443 candRatio = (rawStars['npsfcand'][:, 0].astype(np.float64)
444 / rawStars['ntotal'][:, 0].astype(np.float64))
445 self.assertFloatsAlmostEqual(candRatio.min(), 0.0)
446 self.assertFloatsAlmostEqual(candRatio.max(), 1.0)
448 # Test the fgcm_photoCalib output
449 zptCat = butler.get('fgcmZeropoints' + config.connections.cycleNumber,
450 collections=[inputCollection], instrument=instName)
452 good = (zptCat['fgcmFlag'] < 16)
453 bad = (zptCat['fgcmFlag'] >= 16)
455 # Read in all the calibrations, these should all be there
456 # This test is simply to ensure that all the photoCalib files exist
457 visits = np.unique(zptCat['visit'][good])
458 photoCalibDict = {}
459 for visit in visits:
460 expCat = butler.get('fgcmPhotoCalibCatalog',
461 visit=visit,
462 collections=[outputCollection], instrument=instName)
463 for row in expCat:
464 if row['visit'] == visit:
465 photoCalibDict[(visit, row['id'])] = row.getPhotoCalib()
467 # Check that all of the good photocalibs are there.
468 for rec in zptCat[good]:
469 self.assertTrue((rec['visit'], rec['detector']) in photoCalibDict)
471 # Check that none of the bad photocalibs are there.
472 for rec in zptCat[bad]:
473 self.assertFalse((rec['visit'], rec['detector']) in photoCalibDict)
475 # We do round-trip value checking on just the final one (chosen arbitrarily)
476 testCal = photoCalibDict[(testVisit, testCcd)]
478 src = butler.get('src', visit=int(testVisit), detector=int(testCcd),
479 collections=[outputCollection], instrument=instName)
481 # Only test sources with positive flux
482 gdSrc = (src['slot_CalibFlux_instFlux'] > 0.0)
484 # We need to apply the calibration offset to the fgcmzpt (which is internal
485 # and doesn't know about that yet)
486 testZpInd, = np.where((zptCat['visit'] == testVisit)
487 & (zptCat['detector'] == testCcd))
488 fgcmZpt = (zptCat['fgcmZpt'][testZpInd] + offsets[testBandIndex]
489 + zptCat['fgcmDeltaChrom'][testZpInd])
490 fgcmZptGrayErr = np.sqrt(zptCat['fgcmZptVar'][testZpInd])
492 if config.doComposeWcsJacobian:
493 # The raw zeropoint needs to be modified to know about the wcs jacobian
494 refs = butler.registry.queryDatasets('camera', dimensions=['instrument'],
495 collections=...)
496 camera = butler.getDirect(list(refs)[0])
497 approxPixelAreaFields = fgcmcal.utilities.computeApproxPixelAreaFields(camera)
498 center = approxPixelAreaFields[testCcd].getBBox().getCenter()
499 pixAreaCorr = approxPixelAreaFields[testCcd].evaluate(center)
500 fgcmZpt += -2.5*np.log10(pixAreaCorr)
502 # This is the magnitude through the mean calibration
503 photoCalMeanCalMags = np.zeros(gdSrc.sum())
504 # This is the magnitude through the full focal-plane variable mags
505 photoCalMags = np.zeros_like(photoCalMeanCalMags)
506 # This is the magnitude with the FGCM (central-ccd) zeropoint
507 zptMeanCalMags = np.zeros_like(photoCalMeanCalMags)
509 for i, rec in enumerate(src[gdSrc]):
510 photoCalMeanCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'])
511 photoCalMags[i] = testCal.instFluxToMagnitude(rec['slot_CalibFlux_instFlux'],
512 rec.getCentroid())
513 zptMeanCalMags[i] = fgcmZpt - 2.5*np.log10(rec['slot_CalibFlux_instFlux'])
515 # These should be very close but some tiny differences because the fgcm value
516 # is defined at the center of the bbox, and the photoCal is the mean over the box
517 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
518 zptMeanCalMags, rtol=1e-6)
519 # These should be roughly equal, but not precisely because of the focal-plane
520 # variation. However, this is a useful sanity check for something going totally
521 # wrong.
522 self.assertFloatsAlmostEqual(photoCalMeanCalMags,
523 photoCalMags, rtol=1e-2)
525 # The next test compares the "FGCM standard magnitudes" (which are output
526 # from the fgcm code itself) to the "calibrated magnitudes" that are
527 # obtained from running photoCalib.calibrateCatalog() on the original
528 # src catalogs. This summary comparison ensures that using photoCalibs
529 # yields the same results as what FGCM is computing internally.
530 # Note that we additionally need to take into account the post-processing
531 # offsets used in the tests.
533 # For decent statistics, we are matching all the sources from one visit
534 # (multiple ccds)
535 whereClause = f"instrument='{instName:s}' and visit={testVisit:d}"
536 srcRefs = butler.registry.queryDatasets('src', dimensions=['visit'],
537 collections='%s/testdata' % (instName),
538 where=whereClause,
539 findFirst=True)
540 photoCals = []
541 for srcRef in srcRefs:
542 photoCals.append(photoCalibDict[(testVisit, srcRef.dataId['detector'])])
544 matchMag, matchDelta = self._getMatchedVisitCat(butler, srcRefs, photoCals,
545 rawStars, testBandIndex, offsets)
547 st = np.argsort(matchMag)
548 # Compare the brightest 25% of stars. No matter the setting of
549 # deltaMagBkgOffsetPercentile, we want to ensure that these stars
550 # match on average.
551 brightest, = np.where(matchMag < matchMag[st[int(0.25*st.size)]])
552 self.assertFloatsAlmostEqual(np.median(matchDelta[brightest]), 0.0, atol=0.002)
554 # And the photoCal error is just the zeropoint gray error
555 self.assertFloatsAlmostEqual(testCal.getCalibrationErr(),
556 (np.log(10.0)/2.5)*testCal.getCalibrationMean()*fgcmZptGrayErr)
558 # Test the transmission output
559 visitCatalog = butler.get('fgcmVisitCatalog', collections=[inputCollection],
560 instrument=instName)
561 lutCat = butler.get('fgcmLookUpTable', collections=[inputCollection],
562 instrument=instName)
564 testTrans = butler.get('transmission_atmosphere_fgcm',
565 visit=visitCatalog[0]['visit'],
566 collections=[outputCollection], instrument=instName)
567 testResp = testTrans.sampleAt(position=geom.Point2D(0, 0),
568 wavelengths=lutCat[0]['atmLambda'])
570 # The test fit is performed with the atmosphere parameters frozen
571 # (freezeStdAtmosphere = True). Thus the only difference between
572 # these output atmospheres and the standard is the different
573 # airmass. Furthermore, this is a very rough comparison because
574 # the look-up table is computed with very coarse sampling for faster
575 # testing.
577 # To account for overall throughput changes, we scale by the median ratio,
578 # we only care about the shape
579 ratio = np.median(testResp/lutCat[0]['atmStdTrans'])
580 self.assertFloatsAlmostEqual(testResp/ratio, lutCat[0]['atmStdTrans'], atol=0.04)
582 # The second should be close to the first, but there is the airmass
583 # difference so they aren't identical.
584 testTrans2 = butler.get('transmission_atmosphere_fgcm',
585 visit=visitCatalog[1]['visit'],
586 collections=[outputCollection], instrument=instName)
587 testResp2 = testTrans2.sampleAt(position=geom.Point2D(0, 0),
588 wavelengths=lutCat[0]['atmLambda'])
590 # As above, we scale by the ratio to compare the shape of the curve.
591 ratio = np.median(testResp/testResp2)
592 self.assertFloatsAlmostEqual(testResp/ratio, testResp2, atol=0.04)
594 def _testFgcmMultiFit(self, instName, testName, queryString, visits, zpOffsets):
595 """Test running the full pipeline with multiple fit cycles.
597 Parameters
598 ----------
599 instName : `str`
600 Short name of the instrument
601 testName : `str`
602 Base name of the test collection
603 queryString : `str`
604 Query to send to the pipetask.
605 visits : `list`
606 List of visits to calibrate
607 zpOffsets : `np.ndarray`
608 Zeropoint offsets expected
609 """
610 instCamel = instName.title()
612 configFiles = {'fgcmBuildStarsTable': [os.path.join(ROOT,
613 'config',
614 f'fgcmBuildStarsTable{instCamel}.py')],
615 'fgcmFitCycle': [os.path.join(ROOT,
616 'config',
617 f'fgcmFitCycle{instCamel}.py')],
618 'fgcmOutputProducts': [os.path.join(ROOT,
619 'config',
620 f'fgcmOutputProducts{instCamel}.py')]}
621 outputCollection = f'{instName}/{testName}/unified'
623 cwd = os.getcwd()
624 runDir = os.path.join(self.testDir, testName)
625 os.makedirs(runDir)
626 os.chdir(runDir)
628 self._runPipeline(self.repo,
629 os.path.join(ROOT,
630 'pipelines',
631 f'fgcmFullPipeline{instCamel}.yaml'),
632 configFiles=configFiles,
633 inputCollections=[f'{instName}/{testName}/lut',
634 'refcats/gen2'],
635 outputCollection=outputCollection,
636 queryString=queryString,
637 registerDatasetTypes=True)
639 os.chdir(cwd)
641 butler = dafButler.Butler(self.repo)
643 offsetCat = butler.get('fgcmReferenceCalibrationOffsets',
644 collections=[outputCollection], instrument=instName)
645 offsets = offsetCat['offset'][:]
646 self.assertFloatsAlmostEqual(offsets, zpOffsets, atol=1e-6)
648 def _getMatchedVisitCat(self, butler, srcHandles, photoCals,
649 rawStars, bandIndex, offsets):
650 """
651 Get a list of matched magnitudes and deltas from calibrated src catalogs.
653 Parameters
654 ----------
655 butler : `lsst.daf.butler.Butler`
656 srcHandles : `list`
657 handles of source catalogs
658 photoCals : `list`
659 photoCalib objects, matched to srcHandles.
660 rawStars : `lsst.afw.table.SourceCatalog`
661 Fgcm standard stars
662 bandIndex : `int`
663 Index of the band for the source catalogs
664 offsets : `np.ndarray`
665 Testing calibration offsets to apply to rawStars
667 Returns
668 -------
669 matchMag : `np.ndarray`
670 Array of matched magnitudes
671 matchDelta : `np.ndarray`
672 Array of matched deltas between src and standard stars.
673 """
674 matcher = esutil.htm.Matcher(11, np.rad2deg(rawStars['coord_ra']),
675 np.rad2deg(rawStars['coord_dec']))
677 matchDelta = None
678 for srcHandle, photoCal in zip(srcHandles, photoCals):
679 src = butler.getDirect(srcHandle)
680 src = photoCal.calibrateCatalog(src)
682 gdSrc, = np.where(np.nan_to_num(src['slot_CalibFlux_flux']) > 0.0)
684 matches = matcher.match(np.rad2deg(src['coord_ra'][gdSrc]),
685 np.rad2deg(src['coord_dec'][gdSrc]),
686 1./3600., maxmatch=1)
688 srcMag = src['slot_CalibFlux_mag'][gdSrc][matches[0]]
689 # Apply offset here to the catalog mag
690 catMag = rawStars['mag_std_noabs'][matches[1]][:, bandIndex] + offsets[bandIndex]
691 delta = srcMag - catMag
692 if matchDelta is None:
693 matchDelta = delta
694 matchMag = catMag
695 else:
696 matchDelta = np.append(matchDelta, delta)
697 matchMag = np.append(matchMag, catMag)
699 return matchMag, matchDelta
701 def _testFgcmCalibrateTract(self, instName, testName, visits, tract, skymapName,
702 rawRepeatability, filterNCalibMap):
703 """Test running of FgcmCalibrateTractTask
705 Parameters
706 ----------
707 instName : `str`
708 Short name of the instrument
709 testName : `str`
710 Base name of the test collection
711 visits : `list`
712 List of visits to calibrate
713 tract : `int`
714 Tract number
715 skymapName : `str`
716 Name of the sky map
717 rawRepeatability : `np.array`
718 Expected raw repeatability after convergence.
719 Length should be number of bands.
720 filterNCalibMap : `dict`
721 Mapping from filter name to number of photoCalibs created.
722 """
723 instCamel = instName.title()
725 configFiles = {'fgcmCalibrateTractTable':
726 [os.path.join(ROOT,
727 'config',
728 f'fgcmCalibrateTractTable{instCamel}.py')]}
730 outputCollection = f'{instName}/{testName}/tract'
732 inputCollections = [f'{instName}/{testName}/lut',
733 'refcats/gen2']
735 queryString = f"tract={tract:d} and skymap='{skymapName:s}'"
737 self._runPipeline(self.repo,
738 os.path.join(ROOT,
739 'pipelines',
740 f'fgcmCalibrateTractTable{instCamel:s}.yaml'),
741 queryString=queryString,
742 configFiles=configFiles,
743 inputCollections=inputCollections,
744 outputCollection=outputCollection,
745 registerDatasetTypes=True)
747 butler = dafButler.Butler(self.repo)
749 whereClause = f"instrument='{instName:s}' and tract={tract:d} and skymap='{skymapName:s}'"
751 repRefs = butler.registry.queryDatasets('fgcmRawRepeatability',
752 dimensions=['tract'],
753 collections=outputCollection,
754 where=whereClause)
756 repeatabilityCat = butler.getDirect(list(repRefs)[0])
757 repeatability = repeatabilityCat['rawRepeatability'][:]
758 self.assertFloatsAlmostEqual(repeatability, rawRepeatability, atol=4e-6)
760 # Check that the number of photoCalib objects in each filter are what we expect
761 for filterName in filterNCalibMap.keys():
762 whereClause = (f"instrument='{instName:s}' and tract={tract:d} and "
763 f"physical_filter='{filterName:s}' and skymap='{skymapName:s}'")
765 refs = butler.registry.queryDatasets('fgcmPhotoCalibTractCatalog',
766 dimensions=['tract', 'physical_filter'],
767 collections=outputCollection,
768 where=whereClause)
770 count = 0
771 for ref in set(refs):
772 expCat = butler.getDirect(ref)
773 test, = np.where((expCat['visit'] > 0) & (expCat['id'] >= 0))
774 count += test.size
776 self.assertEqual(count, filterNCalibMap[filterName])
778 # Check that every visit got a transmission
779 for visit in visits:
780 whereClause = (f"instrument='{instName:s}' and tract={tract:d} and "
781 f"visit={visit:d} and skymap='{skymapName:s}'")
782 refs = butler.registry.queryDatasets('transmission_atmosphere_fgcm_tract',
783 dimensions=['tract', 'visit'],
784 collections=outputCollection,
785 where=whereClause)
786 self.assertEqual(len(set(refs)), 1)
788 @classmethod
789 def tearDownClass(cls):
790 """Tear down and clear directories
791 """
792 if os.path.exists(cls.testDir):
793 shutil.rmtree(cls.testDir, True)