Coverage for python/lsst/fgcmcal/utilities.py : 10%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of fgcmcal.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21"""Utility functions for fgcmcal.
23This file contains utility functions that are used by more than one task,
24and do not need to be part of a task.
25"""
27import numpy as np
28import os
29import re
31from lsst.daf.base import PropertyList
32import lsst.daf.persistence as dafPersist
33import lsst.afw.cameraGeom as afwCameraGeom
34import lsst.afw.table as afwTable
35import lsst.afw.image as afwImage
36import lsst.afw.math as afwMath
37import lsst.geom as geom
38from lsst.obs.base import createInitialSkyWcs
39from lsst.obs.base import Instrument
41import fgcm
44FGCM_EXP_FIELD = 'VISIT'
45FGCM_CCD_FIELD = 'DETECTOR'
48def makeConfigDict(config, log, camera, maxIter,
49 resetFitParameters, outputZeropoints,
50 lutFilterNames, tract=None):
51 """
52 Make the FGCM fit cycle configuration dict
54 Parameters
55 ----------
56 config: `lsst.fgcmcal.FgcmFitCycleConfig`
57 Configuration object
58 log: `lsst.log.Log`
59 LSST log object
60 camera: `lsst.afw.cameraGeom.Camera`
61 Camera from the butler
62 maxIter: `int`
63 Maximum number of iterations
64 resetFitParameters: `bool`
65 Reset fit parameters before fitting?
66 outputZeropoints: `bool`
67 Compute zeropoints for output?
68 lutFilterNames : array-like, `str`
69 Array of physical filter names in the LUT.
70 tract: `int`, optional
71 Tract number for extending the output file name for debugging.
72 Default is None.
74 Returns
75 -------
76 configDict: `dict`
77 Configuration dictionary for fgcm
78 """
79 # Extract the bands that are _not_ being fit for fgcm configuration
80 notFitBands = [b for b in config.bands if b not in config.fitBands]
82 # process the starColorCuts
83 starColorCutList = []
84 for ccut in config.starColorCuts:
85 parts = ccut.split(',')
86 starColorCutList.append([parts[0], parts[1], float(parts[2]), float(parts[3])])
88 # TODO: Having direct access to the mirror area from the camera would be
89 # useful. See DM-16489.
90 # Mirror area in cm**2
91 mirrorArea = np.pi*(camera.telescopeDiameter*100./2.)**2.
93 # Get approximate average camera gain:
94 gains = [amp.getGain() for detector in camera for amp in detector.getAmplifiers()]
95 cameraGain = float(np.median(gains))
97 # Cut down the filter map to those that are in the LUT
98 filterToBand = {filterName: config.physicalFilterMap[filterName] for
99 filterName in lutFilterNames}
101 if tract is None:
102 outfileBase = config.outfileBase
103 else:
104 outfileBase = '%s-%06d' % (config.outfileBase, tract)
106 # create a configuration dictionary for fgcmFitCycle
107 configDict = {'outfileBase': outfileBase,
108 'logger': log,
109 'exposureFile': None,
110 'obsFile': None,
111 'indexFile': None,
112 'lutFile': None,
113 'mirrorArea': mirrorArea,
114 'cameraGain': cameraGain,
115 'ccdStartIndex': camera[0].getId(),
116 'expField': FGCM_EXP_FIELD,
117 'ccdField': FGCM_CCD_FIELD,
118 'seeingField': 'DELTA_APER',
119 'fwhmField': 'PSFSIGMA',
120 'skyBrightnessField': 'SKYBACKGROUND',
121 'deepFlag': 'DEEPFLAG', # unused
122 'bands': list(config.bands),
123 'fitBands': list(config.fitBands),
124 'notFitBands': notFitBands,
125 'requiredBands': list(config.requiredBands),
126 'filterToBand': filterToBand,
127 'logLevel': 'INFO',
128 'nCore': config.nCore,
129 'nStarPerRun': config.nStarPerRun,
130 'nExpPerRun': config.nExpPerRun,
131 'reserveFraction': config.reserveFraction,
132 'freezeStdAtmosphere': config.freezeStdAtmosphere,
133 'precomputeSuperStarInitialCycle': config.precomputeSuperStarInitialCycle,
134 'superStarSubCCDDict': dict(config.superStarSubCcdDict),
135 'superStarSubCCDChebyshevOrder': config.superStarSubCcdChebyshevOrder,
136 'superStarSubCCDTriangular': config.superStarSubCcdTriangular,
137 'superStarSigmaClip': config.superStarSigmaClip,
138 'focalPlaneSigmaClip': config.focalPlaneSigmaClip,
139 'ccdGraySubCCDDict': dict(config.ccdGraySubCcdDict),
140 'ccdGraySubCCDChebyshevOrder': config.ccdGraySubCcdChebyshevOrder,
141 'ccdGraySubCCDTriangular': config.ccdGraySubCcdTriangular,
142 'ccdGrayFocalPlaneDict': dict(config.ccdGrayFocalPlaneDict),
143 'ccdGrayFocalPlaneChebyshevOrder': config.ccdGrayFocalPlaneChebyshevOrder,
144 'ccdGrayFocalPlaneFitMinCcd': config.ccdGrayFocalPlaneFitMinCcd,
145 'cycleNumber': config.cycleNumber,
146 'maxIter': maxIter,
147 'deltaMagBkgOffsetPercentile': config.deltaMagBkgOffsetPercentile,
148 'deltaMagBkgPerCcd': config.deltaMagBkgPerCcd,
149 'UTBoundary': config.utBoundary,
150 'washMJDs': config.washMjds,
151 'epochMJDs': config.epochMjds,
152 'coatingMJDs': config.coatingMjds,
153 'minObsPerBand': config.minObsPerBand,
154 'latitude': config.latitude,
155 'brightObsGrayMax': config.brightObsGrayMax,
156 'minStarPerCCD': config.minStarPerCcd,
157 'minCCDPerExp': config.minCcdPerExp,
158 'maxCCDGrayErr': config.maxCcdGrayErr,
159 'minStarPerExp': config.minStarPerExp,
160 'minExpPerNight': config.minExpPerNight,
161 'expGrayInitialCut': config.expGrayInitialCut,
162 'expGrayPhotometricCutDict': dict(config.expGrayPhotometricCutDict),
163 'expGrayHighCutDict': dict(config.expGrayHighCutDict),
164 'expGrayRecoverCut': config.expGrayRecoverCut,
165 'expVarGrayPhotometricCutDict': dict(config.expVarGrayPhotometricCutDict),
166 'expGrayErrRecoverCut': config.expGrayErrRecoverCut,
167 'refStarSnMin': config.refStarSnMin,
168 'refStarOutlierNSig': config.refStarOutlierNSig,
169 'applyRefStarColorCuts': config.applyRefStarColorCuts,
170 'illegalValue': -9999.0, # internally used by fgcm.
171 'starColorCuts': starColorCutList,
172 'aperCorrFitNBins': config.aperCorrFitNBins,
173 'aperCorrInputSlopeDict': dict(config.aperCorrInputSlopeDict),
174 'sedBoundaryTermDict': config.sedboundaryterms.toDict()['data'],
175 'sedTermDict': config.sedterms.toDict()['data'],
176 'colorSplitBands': list(config.colorSplitBands),
177 'sigFgcmMaxErr': config.sigFgcmMaxErr,
178 'sigFgcmMaxEGrayDict': dict(config.sigFgcmMaxEGrayDict),
179 'ccdGrayMaxStarErr': config.ccdGrayMaxStarErr,
180 'approxThroughputDict': dict(config.approxThroughputDict),
181 'sigmaCalRange': list(config.sigmaCalRange),
182 'sigmaCalFitPercentile': list(config.sigmaCalFitPercentile),
183 'sigmaCalPlotPercentile': list(config.sigmaCalPlotPercentile),
184 'sigma0Phot': config.sigma0Phot,
185 'mapLongitudeRef': config.mapLongitudeRef,
186 'mapNSide': config.mapNSide,
187 'varNSig': 100.0, # Turn off 'variable star selection' which doesn't work yet
188 'varMinBand': 2,
189 'useRetrievedPwv': False,
190 'useNightlyRetrievedPwv': False,
191 'pwvRetrievalSmoothBlock': 25,
192 'useQuadraticPwv': config.useQuadraticPwv,
193 'useRetrievedTauInit': False,
194 'tauRetrievalMinCCDPerNight': 500,
195 'modelMagErrors': config.modelMagErrors,
196 'instrumentParsPerBand': config.instrumentParsPerBand,
197 'instrumentSlopeMinDeltaT': config.instrumentSlopeMinDeltaT,
198 'fitMirrorChromaticity': config.fitMirrorChromaticity,
199 'useRepeatabilityForExpGrayCutsDict': dict(config.useRepeatabilityForExpGrayCutsDict),
200 'autoPhotometricCutNSig': config.autoPhotometricCutNSig,
201 'autoHighCutNSig': config.autoHighCutNSig,
202 'printOnly': False,
203 'quietMode': config.quietMode,
204 'randomSeed': config.randomSeed,
205 'outputStars': False,
206 'outputPath': os.path.abspath('.'),
207 'clobber': True,
208 'useSedLUT': False,
209 'resetParameters': resetFitParameters,
210 'doPlots': config.doPlots,
211 'outputFgcmcalZpts': True, # when outputting zpts, use fgcmcal format
212 'outputZeropoints': outputZeropoints}
214 return configDict
217def translateFgcmLut(lutCat, physicalFilterMap):
218 """
219 Translate the FGCM look-up-table into an fgcm-compatible object
221 Parameters
222 ----------
223 lutCat: `lsst.afw.table.BaseCatalog`
224 Catalog describing the FGCM look-up table
225 physicalFilterMap: `dict`
226 Physical filter to band mapping
228 Returns
229 -------
230 fgcmLut: `lsst.fgcm.FgcmLut`
231 Lookup table for FGCM
232 lutIndexVals: `numpy.ndarray`
233 Numpy array with LUT index information for FGCM
234 lutStd: `numpy.ndarray`
235 Numpy array with LUT standard throughput values for FGCM
237 Notes
238 -----
239 After running this code, it is wise to `del lutCat` to clear the memory.
240 """
242 # first we need the lutIndexVals
243 lutFilterNames = np.array(lutCat[0]['physicalFilters'].split(','), dtype='U')
244 lutStdFilterNames = np.array(lutCat[0]['stdPhysicalFilters'].split(','), dtype='U')
246 # Note that any discrepancies between config values will raise relevant
247 # exceptions in the FGCM code.
249 lutIndexVals = np.zeros(1, dtype=[('FILTERNAMES', lutFilterNames.dtype.str,
250 lutFilterNames.size),
251 ('STDFILTERNAMES', lutStdFilterNames.dtype.str,
252 lutStdFilterNames.size),
253 ('PMB', 'f8', lutCat[0]['pmb'].size),
254 ('PMBFACTOR', 'f8', lutCat[0]['pmbFactor'].size),
255 ('PMBELEVATION', 'f8'),
256 ('LAMBDANORM', 'f8'),
257 ('PWV', 'f8', lutCat[0]['pwv'].size),
258 ('O3', 'f8', lutCat[0]['o3'].size),
259 ('TAU', 'f8', lutCat[0]['tau'].size),
260 ('ALPHA', 'f8', lutCat[0]['alpha'].size),
261 ('ZENITH', 'f8', lutCat[0]['zenith'].size),
262 ('NCCD', 'i4')])
264 lutIndexVals['FILTERNAMES'][:] = lutFilterNames
265 lutIndexVals['STDFILTERNAMES'][:] = lutStdFilterNames
266 lutIndexVals['PMB'][:] = lutCat[0]['pmb']
267 lutIndexVals['PMBFACTOR'][:] = lutCat[0]['pmbFactor']
268 lutIndexVals['PMBELEVATION'] = lutCat[0]['pmbElevation']
269 lutIndexVals['LAMBDANORM'] = lutCat[0]['lambdaNorm']
270 lutIndexVals['PWV'][:] = lutCat[0]['pwv']
271 lutIndexVals['O3'][:] = lutCat[0]['o3']
272 lutIndexVals['TAU'][:] = lutCat[0]['tau']
273 lutIndexVals['ALPHA'][:] = lutCat[0]['alpha']
274 lutIndexVals['ZENITH'][:] = lutCat[0]['zenith']
275 lutIndexVals['NCCD'] = lutCat[0]['nCcd']
277 # now we need the Standard Values
278 lutStd = np.zeros(1, dtype=[('PMBSTD', 'f8'),
279 ('PWVSTD', 'f8'),
280 ('O3STD', 'f8'),
281 ('TAUSTD', 'f8'),
282 ('ALPHASTD', 'f8'),
283 ('ZENITHSTD', 'f8'),
284 ('LAMBDARANGE', 'f8', 2),
285 ('LAMBDASTEP', 'f8'),
286 ('LAMBDASTD', 'f8', lutFilterNames.size),
287 ('LAMBDASTDFILTER', 'f8', lutStdFilterNames.size),
288 ('I0STD', 'f8', lutFilterNames.size),
289 ('I1STD', 'f8', lutFilterNames.size),
290 ('I10STD', 'f8', lutFilterNames.size),
291 ('I2STD', 'f8', lutFilterNames.size),
292 ('LAMBDAB', 'f8', lutFilterNames.size),
293 ('ATMLAMBDA', 'f8', lutCat[0]['atmLambda'].size),
294 ('ATMSTDTRANS', 'f8', lutCat[0]['atmStdTrans'].size)])
295 lutStd['PMBSTD'] = lutCat[0]['pmbStd']
296 lutStd['PWVSTD'] = lutCat[0]['pwvStd']
297 lutStd['O3STD'] = lutCat[0]['o3Std']
298 lutStd['TAUSTD'] = lutCat[0]['tauStd']
299 lutStd['ALPHASTD'] = lutCat[0]['alphaStd']
300 lutStd['ZENITHSTD'] = lutCat[0]['zenithStd']
301 lutStd['LAMBDARANGE'][:] = lutCat[0]['lambdaRange'][:]
302 lutStd['LAMBDASTEP'] = lutCat[0]['lambdaStep']
303 lutStd['LAMBDASTD'][:] = lutCat[0]['lambdaStd']
304 lutStd['LAMBDASTDFILTER'][:] = lutCat[0]['lambdaStdFilter']
305 lutStd['I0STD'][:] = lutCat[0]['i0Std']
306 lutStd['I1STD'][:] = lutCat[0]['i1Std']
307 lutStd['I10STD'][:] = lutCat[0]['i10Std']
308 lutStd['I2STD'][:] = lutCat[0]['i2Std']
309 lutStd['LAMBDAB'][:] = lutCat[0]['lambdaB']
310 lutStd['ATMLAMBDA'][:] = lutCat[0]['atmLambda'][:]
311 lutStd['ATMSTDTRANS'][:] = lutCat[0]['atmStdTrans'][:]
313 lutTypes = [row['luttype'] for row in lutCat]
315 # And the flattened look-up-table
316 lutFlat = np.zeros(lutCat[0]['lut'].size, dtype=[('I0', 'f4'),
317 ('I1', 'f4')])
319 lutFlat['I0'][:] = lutCat[lutTypes.index('I0')]['lut'][:]
320 lutFlat['I1'][:] = lutCat[lutTypes.index('I1')]['lut'][:]
322 lutDerivFlat = np.zeros(lutCat[0]['lut'].size, dtype=[('D_LNPWV', 'f4'),
323 ('D_O3', 'f4'),
324 ('D_LNTAU', 'f4'),
325 ('D_ALPHA', 'f4'),
326 ('D_SECZENITH', 'f4'),
327 ('D_LNPWV_I1', 'f4'),
328 ('D_O3_I1', 'f4'),
329 ('D_LNTAU_I1', 'f4'),
330 ('D_ALPHA_I1', 'f4'),
331 ('D_SECZENITH_I1', 'f4')])
333 for name in lutDerivFlat.dtype.names:
334 lutDerivFlat[name][:] = lutCat[lutTypes.index(name)]['lut'][:]
336 # The fgcm.FgcmLUT() class copies all the LUT information into special
337 # shared memory objects that will not blow up the memory usage when used
338 # with python multiprocessing. Once all the numbers are copied, the
339 # references to the temporary objects (lutCat, lutFlat, lutDerivFlat)
340 # will fall out of scope and can be cleaned up by the garbage collector.
341 fgcmLut = fgcm.FgcmLUT(lutIndexVals, lutFlat, lutDerivFlat, lutStd,
342 filterToBand=physicalFilterMap)
344 return fgcmLut, lutIndexVals, lutStd
347def translateVisitCatalog(visitCat):
348 """
349 Translate the FGCM visit catalog to an fgcm-compatible object
351 Parameters
352 ----------
353 visitCat: `lsst.afw.table.BaseCatalog`
354 FGCM visitCat from `lsst.fgcmcal.FgcmBuildStarsTask`
356 Returns
357 -------
358 fgcmExpInfo: `numpy.ndarray`
359 Numpy array for visit information for FGCM
361 Notes
362 -----
363 After running this code, it is wise to `del visitCat` to clear the memory.
364 """
366 fgcmExpInfo = np.zeros(len(visitCat), dtype=[('VISIT', 'i8'),
367 ('MJD', 'f8'),
368 ('EXPTIME', 'f8'),
369 ('PSFSIGMA', 'f8'),
370 ('DELTA_APER', 'f8'),
371 ('SKYBACKGROUND', 'f8'),
372 ('DEEPFLAG', 'i2'),
373 ('TELHA', 'f8'),
374 ('TELRA', 'f8'),
375 ('TELDEC', 'f8'),
376 ('TELROT', 'f8'),
377 ('PMB', 'f8'),
378 ('FILTERNAME', 'a50')])
379 fgcmExpInfo['VISIT'][:] = visitCat['visit']
380 fgcmExpInfo['MJD'][:] = visitCat['mjd']
381 fgcmExpInfo['EXPTIME'][:] = visitCat['exptime']
382 fgcmExpInfo['DEEPFLAG'][:] = visitCat['deepFlag']
383 fgcmExpInfo['TELHA'][:] = visitCat['telha']
384 fgcmExpInfo['TELRA'][:] = visitCat['telra']
385 fgcmExpInfo['TELDEC'][:] = visitCat['teldec']
386 fgcmExpInfo['TELROT'][:] = visitCat['telrot']
387 fgcmExpInfo['PMB'][:] = visitCat['pmb']
388 fgcmExpInfo['PSFSIGMA'][:] = visitCat['psfSigma']
389 fgcmExpInfo['DELTA_APER'][:] = visitCat['deltaAper']
390 fgcmExpInfo['SKYBACKGROUND'][:] = visitCat['skyBackground']
391 # Note that we have to go through asAstropy() to get a string
392 # array out of an afwTable. This is faster than a row-by-row loop.
393 fgcmExpInfo['FILTERNAME'][:] = visitCat.asAstropy()['physicalFilter']
395 return fgcmExpInfo
398def computeCcdOffsets(camera, defaultOrientation):
399 """
400 Compute the CCD offsets in ra/dec and x/y space
402 Parameters
403 ----------
404 camera: `lsst.afw.cameraGeom.Camera`
405 defaultOrientation: `float`
406 Default camera orientation (degrees)
408 Returns
409 -------
410 ccdOffsets: `numpy.ndarray`
411 Numpy array with ccd offset information for input to FGCM.
412 Angular units are degrees, and x/y units are pixels.
413 """
414 # TODO: DM-21215 will fully generalize to arbitrary camera orientations
416 # and we need to know the ccd offsets from the camera geometry
417 ccdOffsets = np.zeros(len(camera), dtype=[('CCDNUM', 'i4'),
418 ('DELTA_RA', 'f8'),
419 ('DELTA_DEC', 'f8'),
420 ('RA_SIZE', 'f8'),
421 ('DEC_SIZE', 'f8'),
422 ('X_SIZE', 'i4'),
423 ('Y_SIZE', 'i4')])
425 # Generate fake WCSs centered at 180/0 to avoid the RA=0/360 problem,
426 # since we are looking for relative positions
427 boresight = geom.SpherePoint(180.0*geom.degrees, 0.0*geom.degrees)
429 # TODO: DM-17597 will update testdata_jointcal so that the test data
430 # does not have nan as the boresight angle for HSC data. For the
431 # time being, there is this ungainly hack.
432 if camera.getName() == 'HSC' and np.isnan(defaultOrientation):
433 orientation = 270*geom.degrees
434 else:
435 orientation = defaultOrientation*geom.degrees
436 flipX = False
438 # Create a temporary visitInfo for input to createInitialSkyWcs
439 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight,
440 boresightRotAngle=orientation,
441 rotType=afwImage.RotType.SKY)
443 for i, detector in enumerate(camera):
444 ccdOffsets['CCDNUM'][i] = detector.getId()
446 wcs = createInitialSkyWcs(visitInfo, detector, flipX)
448 detCenter = wcs.pixelToSky(detector.getCenter(afwCameraGeom.PIXELS))
449 ccdOffsets['DELTA_RA'][i] = (detCenter.getRa() - boresight.getRa()).asDegrees()
450 ccdOffsets['DELTA_DEC'][i] = (detCenter.getDec() - boresight.getDec()).asDegrees()
452 bbox = detector.getBBox()
454 detCorner1 = wcs.pixelToSky(geom.Point2D(bbox.getMin()))
455 detCorner2 = wcs.pixelToSky(geom.Point2D(bbox.getMax()))
457 ccdOffsets['RA_SIZE'][i] = np.abs((detCorner2.getRa() - detCorner1.getRa()).asDegrees())
458 ccdOffsets['DEC_SIZE'][i] = np.abs((detCorner2.getDec() - detCorner1.getDec()).asDegrees())
460 ccdOffsets['X_SIZE'][i] = bbox.getMaxX()
461 ccdOffsets['Y_SIZE'][i] = bbox.getMaxY()
463 return ccdOffsets
466def computeReferencePixelScale(camera):
467 """
468 Compute the median pixel scale in the camera
470 Returns
471 -------
472 pixelScale: `float`
473 Average pixel scale (arcsecond) over the camera
474 """
476 boresight = geom.SpherePoint(180.0*geom.degrees, 0.0*geom.degrees)
477 orientation = 0.0*geom.degrees
478 flipX = False
480 # Create a temporary visitInfo for input to createInitialSkyWcs
481 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight,
482 boresightRotAngle=orientation,
483 rotType=afwImage.RotType.SKY)
485 pixelScales = np.zeros(len(camera))
486 for i, detector in enumerate(camera):
487 wcs = createInitialSkyWcs(visitInfo, detector, flipX)
488 pixelScales[i] = wcs.getPixelScale().asArcseconds()
490 ok, = np.where(pixelScales > 0.0)
491 return np.median(pixelScales[ok])
494def computeApproxPixelAreaFields(camera):
495 """
496 Compute the approximate pixel area bounded fields from the camera
497 geometry.
499 Parameters
500 ----------
501 camera: `lsst.afw.cameraGeom.Camera`
503 Returns
504 -------
505 approxPixelAreaFields: `dict`
506 Dictionary of approximate area fields, keyed with detector ID
507 """
509 areaScaling = 1. / computeReferencePixelScale(camera)**2.
511 # Generate fake WCSs centered at 180/0 to avoid the RA=0/360 problem,
512 # since we are looking for relative scales
513 boresight = geom.SpherePoint(180.0*geom.degrees, 0.0*geom.degrees)
515 flipX = False
516 # Create a temporary visitInfo for input to createInitialSkyWcs
517 # The orientation does not matter for the area computation
518 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight,
519 boresightRotAngle=0.0*geom.degrees,
520 rotType=afwImage.RotType.SKY)
522 approxPixelAreaFields = {}
524 for i, detector in enumerate(camera):
525 key = detector.getId()
527 wcs = createInitialSkyWcs(visitInfo, detector, flipX)
528 bbox = detector.getBBox()
530 areaField = afwMath.PixelAreaBoundedField(bbox, wcs,
531 unit=geom.arcseconds, scaling=areaScaling)
532 approxAreaField = afwMath.ChebyshevBoundedField.approximate(areaField)
534 approxPixelAreaFields[key] = approxAreaField
536 return approxPixelAreaFields
539def makeZptSchema(superStarChebyshevSize, zptChebyshevSize):
540 """
541 Make the zeropoint schema
543 Parameters
544 ----------
545 superStarChebyshevSize: `int`
546 Length of the superstar chebyshev array
547 zptChebyshevSize: `int`
548 Length of the zeropoint chebyshev array
550 Returns
551 -------
552 zptSchema: `lsst.afw.table.schema`
553 """
555 zptSchema = afwTable.Schema()
557 zptSchema.addField('visit', type=np.int32, doc='Visit number')
558 zptSchema.addField('detector', type=np.int32, doc='Detector ID number')
559 zptSchema.addField('fgcmFlag', type=np.int32, doc=('FGCM flag value: '
560 '1: Photometric, used in fit; '
561 '2: Photometric, not used in fit; '
562 '4: Non-photometric, on partly photometric night; '
563 '8: Non-photometric, on non-photometric night; '
564 '16: No zeropoint could be determined; '
565 '32: Too few stars for reliable gray computation'))
566 zptSchema.addField('fgcmZpt', type=np.float64, doc='FGCM zeropoint (center of CCD)')
567 zptSchema.addField('fgcmZptErr', type=np.float64,
568 doc='Error on zeropoint, estimated from repeatability + number of obs')
569 zptSchema.addField('fgcmfZptChebXyMax', type='ArrayD', size=2,
570 doc='maximum x/maximum y to scale to apply chebyshev parameters')
571 zptSchema.addField('fgcmfZptCheb', type='ArrayD',
572 size=zptChebyshevSize,
573 doc='Chebyshev parameters (flattened) for zeropoint')
574 zptSchema.addField('fgcmfZptSstarCheb', type='ArrayD',
575 size=superStarChebyshevSize,
576 doc='Chebyshev parameters (flattened) for superStarFlat')
577 zptSchema.addField('fgcmI0', type=np.float64, doc='Integral of the passband')
578 zptSchema.addField('fgcmI10', type=np.float64, doc='Normalized chromatic integral')
579 zptSchema.addField('fgcmR0', type=np.float64,
580 doc='Retrieved i0 integral, estimated from stars (only for flag 1)')
581 zptSchema.addField('fgcmR10', type=np.float64,
582 doc='Retrieved i10 integral, estimated from stars (only for flag 1)')
583 zptSchema.addField('fgcmGry', type=np.float64,
584 doc='Estimated gray extinction relative to atmospheric solution; '
585 'only for fgcmFlag <= 4 (see fgcmFlag) ')
586 zptSchema.addField('fgcmDeltaChrom', type=np.float64,
587 doc='Mean chromatic correction for stars in this ccd; '
588 'only for fgcmFlag <= 4 (see fgcmFlag)')
589 zptSchema.addField('fgcmZptVar', type=np.float64, doc='Variance of zeropoint over ccd')
590 zptSchema.addField('fgcmTilings', type=np.float64,
591 doc='Number of photometric tilings used for solution for ccd')
592 zptSchema.addField('fgcmFpGry', type=np.float64,
593 doc='Average gray extinction over the full focal plane '
594 '(same for all ccds in a visit)')
595 zptSchema.addField('fgcmFpGryBlue', type=np.float64,
596 doc='Average gray extinction over the full focal plane '
597 'for 25% bluest stars')
598 zptSchema.addField('fgcmFpGryBlueErr', type=np.float64,
599 doc='Error on Average gray extinction over the full focal plane '
600 'for 25% bluest stars')
601 zptSchema.addField('fgcmFpGryRed', type=np.float64,
602 doc='Average gray extinction over the full focal plane '
603 'for 25% reddest stars')
604 zptSchema.addField('fgcmFpGryRedErr', type=np.float64,
605 doc='Error on Average gray extinction over the full focal plane '
606 'for 25% reddest stars')
607 zptSchema.addField('fgcmFpVar', type=np.float64,
608 doc='Variance of gray extinction over the full focal plane '
609 '(same for all ccds in a visit)')
610 zptSchema.addField('fgcmDust', type=np.float64,
611 doc='Gray dust extinction from the primary/corrector'
612 'at the time of the exposure')
613 zptSchema.addField('fgcmFlat', type=np.float64, doc='Superstarflat illumination correction')
614 zptSchema.addField('fgcmAperCorr', type=np.float64, doc='Aperture correction estimated by fgcm')
615 zptSchema.addField('fgcmDeltaMagBkg', type=np.float64,
616 doc=('Local background correction from brightest percentile '
617 '(value set by deltaMagBkgOffsetPercentile) calibration '
618 'stars.'))
619 zptSchema.addField('exptime', type=np.float32, doc='Exposure time')
620 zptSchema.addField('filtername', type=str, size=10, doc='Filter name')
622 return zptSchema
625def makeZptCat(zptSchema, zpStruct):
626 """
627 Make the zeropoint catalog for persistence
629 Parameters
630 ----------
631 zptSchema: `lsst.afw.table.Schema`
632 Zeropoint catalog schema
633 zpStruct: `numpy.ndarray`
634 Zeropoint structure from fgcm
636 Returns
637 -------
638 zptCat: `afwTable.BaseCatalog`
639 Zeropoint catalog for persistence
640 """
642 zptCat = afwTable.BaseCatalog(zptSchema)
643 zptCat.reserve(zpStruct.size)
645 for filterName in zpStruct['FILTERNAME']:
646 rec = zptCat.addNew()
647 rec['filtername'] = filterName.decode('utf-8')
649 zptCat['visit'][:] = zpStruct[FGCM_EXP_FIELD]
650 zptCat['detector'][:] = zpStruct[FGCM_CCD_FIELD]
651 zptCat['fgcmFlag'][:] = zpStruct['FGCM_FLAG']
652 zptCat['fgcmZpt'][:] = zpStruct['FGCM_ZPT']
653 zptCat['fgcmZptErr'][:] = zpStruct['FGCM_ZPTERR']
654 zptCat['fgcmfZptChebXyMax'][:, :] = zpStruct['FGCM_FZPT_XYMAX']
655 zptCat['fgcmfZptCheb'][:, :] = zpStruct['FGCM_FZPT_CHEB']
656 zptCat['fgcmfZptSstarCheb'][:, :] = zpStruct['FGCM_FZPT_SSTAR_CHEB']
657 zptCat['fgcmI0'][:] = zpStruct['FGCM_I0']
658 zptCat['fgcmI10'][:] = zpStruct['FGCM_I10']
659 zptCat['fgcmR0'][:] = zpStruct['FGCM_R0']
660 zptCat['fgcmR10'][:] = zpStruct['FGCM_R10']
661 zptCat['fgcmGry'][:] = zpStruct['FGCM_GRY']
662 zptCat['fgcmDeltaChrom'][:] = zpStruct['FGCM_DELTACHROM']
663 zptCat['fgcmZptVar'][:] = zpStruct['FGCM_ZPTVAR']
664 zptCat['fgcmTilings'][:] = zpStruct['FGCM_TILINGS']
665 zptCat['fgcmFpGry'][:] = zpStruct['FGCM_FPGRY']
666 zptCat['fgcmFpGryBlue'][:] = zpStruct['FGCM_FPGRY_CSPLIT'][:, 0]
667 zptCat['fgcmFpGryBlueErr'][:] = zpStruct['FGCM_FPGRY_CSPLITERR'][:, 0]
668 zptCat['fgcmFpGryRed'][:] = zpStruct['FGCM_FPGRY_CSPLIT'][:, 2]
669 zptCat['fgcmFpGryRedErr'][:] = zpStruct['FGCM_FPGRY_CSPLITERR'][:, 2]
670 zptCat['fgcmFpVar'][:] = zpStruct['FGCM_FPVAR']
671 zptCat['fgcmDust'][:] = zpStruct['FGCM_DUST']
672 zptCat['fgcmFlat'][:] = zpStruct['FGCM_FLAT']
673 zptCat['fgcmAperCorr'][:] = zpStruct['FGCM_APERCORR']
674 zptCat['fgcmDeltaMagBkg'][:] = zpStruct['FGCM_DELTAMAGBKG']
675 zptCat['exptime'][:] = zpStruct['EXPTIME']
677 return zptCat
680def makeAtmSchema():
681 """
682 Make the atmosphere schema
684 Returns
685 -------
686 atmSchema: `lsst.afw.table.Schema`
687 """
689 atmSchema = afwTable.Schema()
691 atmSchema.addField('visit', type=np.int32, doc='Visit number')
692 atmSchema.addField('pmb', type=np.float64, doc='Barometric pressure (mb)')
693 atmSchema.addField('pwv', type=np.float64, doc='Water vapor (mm)')
694 atmSchema.addField('tau', type=np.float64, doc='Aerosol optical depth')
695 atmSchema.addField('alpha', type=np.float64, doc='Aerosol slope')
696 atmSchema.addField('o3', type=np.float64, doc='Ozone (dobson)')
697 atmSchema.addField('secZenith', type=np.float64, doc='Secant(zenith) (~ airmass)')
698 atmSchema.addField('cTrans', type=np.float64, doc='Transmission correction factor')
699 atmSchema.addField('lamStd', type=np.float64, doc='Wavelength for transmission correction')
701 return atmSchema
704def makeAtmCat(atmSchema, atmStruct):
705 """
706 Make the atmosphere catalog for persistence
708 Parameters
709 ----------
710 atmSchema: `lsst.afw.table.Schema`
711 Atmosphere catalog schema
712 atmStruct: `numpy.ndarray`
713 Atmosphere structure from fgcm
715 Returns
716 -------
717 atmCat: `lsst.afw.table.BaseCatalog`
718 Atmosphere catalog for persistence
719 """
721 atmCat = afwTable.BaseCatalog(atmSchema)
722 atmCat.resize(atmStruct.size)
724 atmCat['visit'][:] = atmStruct['VISIT']
725 atmCat['pmb'][:] = atmStruct['PMB']
726 atmCat['pwv'][:] = atmStruct['PWV']
727 atmCat['tau'][:] = atmStruct['TAU']
728 atmCat['alpha'][:] = atmStruct['ALPHA']
729 atmCat['o3'][:] = atmStruct['O3']
730 atmCat['secZenith'][:] = atmStruct['SECZENITH']
731 atmCat['cTrans'][:] = atmStruct['CTRANS']
732 atmCat['lamStd'][:] = atmStruct['LAMSTD']
734 return atmCat
737def makeStdSchema(nBands):
738 """
739 Make the standard star schema
741 Parameters
742 ----------
743 nBands: `int`
744 Number of bands in standard star catalog
746 Returns
747 -------
748 stdSchema: `lsst.afw.table.Schema`
749 """
751 stdSchema = afwTable.SimpleTable.makeMinimalSchema()
752 stdSchema.addField('ngood', type='ArrayI', doc='Number of good observations',
753 size=nBands)
754 stdSchema.addField('ntotal', type='ArrayI', doc='Number of total observations',
755 size=nBands)
756 stdSchema.addField('mag_std_noabs', type='ArrayF',
757 doc='Standard magnitude (no absolute calibration)',
758 size=nBands)
759 stdSchema.addField('magErr_std', type='ArrayF',
760 doc='Standard magnitude error',
761 size=nBands)
762 stdSchema.addField('npsfcand', type='ArrayI',
763 doc='Number of observations flagged as psf candidates',
764 size=nBands)
766 return stdSchema
769def makeStdCat(stdSchema, stdStruct, goodBands):
770 """
771 Make the standard star catalog for persistence
773 Parameters
774 ----------
775 stdSchema: `lsst.afw.table.Schema`
776 Standard star catalog schema
777 stdStruct: `numpy.ndarray`
778 Standard star structure in FGCM format
779 goodBands: `list`
780 List of good band names used in stdStruct
782 Returns
783 -------
784 stdCat: `lsst.afw.table.BaseCatalog`
785 Standard star catalog for persistence
786 """
788 stdCat = afwTable.SimpleCatalog(stdSchema)
789 stdCat.resize(stdStruct.size)
791 stdCat['id'][:] = stdStruct['FGCM_ID']
792 stdCat['coord_ra'][:] = stdStruct['RA'] * geom.degrees
793 stdCat['coord_dec'][:] = stdStruct['DEC'] * geom.degrees
794 stdCat['ngood'][:, :] = stdStruct['NGOOD'][:, :]
795 stdCat['ntotal'][:, :] = stdStruct['NTOTAL'][:, :]
796 stdCat['mag_std_noabs'][:, :] = stdStruct['MAG_STD'][:, :]
797 stdCat['magErr_std'][:, :] = stdStruct['MAGERR_STD'][:, :]
798 stdCat['npsfcand'][:, :] = stdStruct['NPSFCAND'][:, :]
800 md = PropertyList()
801 md.set("BANDS", list(goodBands))
802 stdCat.setMetadata(md)
804 return stdCat
807def computeApertureRadiusFromDataRef(dataRef, fluxField):
808 """
809 Compute the radius associated with a CircularApertureFlux field or
810 associated slot.
812 Parameters
813 ----------
814 dataRef : `lsst.daf.persistence.ButlerDataRef` or
815 `lsst.daf.butler.DeferredDatasetHandle`
816 fluxField : `str`
817 CircularApertureFlux or associated slot.
819 Returns
820 -------
821 apertureRadius : `float`
822 Radius of the aperture field, in pixels.
824 Raises
825 ------
826 RuntimeError: Raised if flux field is not a CircularApertureFlux, ApFlux,
827 or associated slot.
828 """
829 # TODO: Move this method to more general stack method in DM-25775
830 if isinstance(dataRef, dafPersist.ButlerDataRef):
831 # Gen2 dataRef
832 datasetType = dataRef.butlerSubset.datasetType
833 else:
834 # Gen3 dataRef
835 datasetType = dataRef.ref.datasetType.name
837 if datasetType == 'src':
838 schema = dataRef.get(datasetType='src_schema').schema
839 try:
840 fluxFieldName = schema[fluxField].asField().getName()
841 except LookupError:
842 raise RuntimeError("Could not find %s or associated slot in schema." % (fluxField))
843 # This may also raise a RuntimeError
844 apertureRadius = computeApertureRadiusFromName(fluxFieldName)
845 else:
846 # This is a sourceTable_visit
847 apertureRadius = computeApertureRadiusFromName(fluxField)
849 return apertureRadius
852def computeApertureRadiusFromName(fluxField):
853 """
854 Compute the radius associated with a CircularApertureFlux or ApFlux field.
856 Parameters
857 ----------
858 fluxField : `str`
859 CircularApertureFlux or ApFlux
861 Returns
862 -------
863 apertureRadius : `float`
864 Radius of the aperture field, in pixels.
866 Raises
867 ------
868 RuntimeError: Raised if flux field is not a CircularApertureFlux
869 or ApFlux.
870 """
871 # TODO: Move this method to more general stack method in DM-25775
872 m = re.search(r'(CircularApertureFlux|ApFlux)_(\d+)_(\d+)_', fluxField)
874 if m is None:
875 raise RuntimeError(f"Flux field {fluxField} does not correspond to a CircularApertureFlux or ApFlux")
877 apertureRadius = float(m.groups()[1]) + float(m.groups()[2])/10.
879 return apertureRadius
882def extractReferenceMags(refStars, bands, filterMap):
883 """
884 Extract reference magnitudes from refStars for given bands and
885 associated filterMap.
887 Parameters
888 ----------
889 refStars : `lsst.afw.table.BaseCatalog`
890 FGCM reference star catalog
891 bands : `list`
892 List of bands for calibration
893 filterMap: `dict`
894 FGCM mapping of filter to band
896 Returns
897 -------
898 refMag : `np.ndarray`
899 nstar x nband array of reference magnitudes
900 refMagErr : `np.ndarray`
901 nstar x nband array of reference magnitude errors
902 """
903 # After DM-23331 fgcm reference catalogs have FILTERNAMES to prevent
904 # against index errors and allow more flexibility in fitting after
905 # the build stars step.
907 md = refStars.getMetadata()
908 if 'FILTERNAMES' in md:
909 filternames = md.getArray('FILTERNAMES')
911 # The reference catalog that fgcm wants has one entry per band
912 # in the config file
913 refMag = np.zeros((len(refStars), len(bands)),
914 dtype=refStars['refMag'].dtype) + 99.0
915 refMagErr = np.zeros_like(refMag) + 99.0
916 for i, filtername in enumerate(filternames):
917 # We are allowed to run the fit configured so that we do not
918 # use every column in the reference catalog.
919 try:
920 band = filterMap[filtername]
921 except KeyError:
922 continue
923 try:
924 ind = bands.index(band)
925 except ValueError:
926 continue
928 refMag[:, ind] = refStars['refMag'][:, i]
929 refMagErr[:, ind] = refStars['refMagErr'][:, i]
931 else:
932 # Continue to use old catalogs as before.
933 refMag = refStars['refMag'][:, :]
934 refMagErr = refStars['refMagErr'][:, :]
936 return refMag, refMagErr
939def lookupStaticCalibrations(datasetType, registry, quantumDataId, collections):
940 instrument = Instrument.fromName(quantumDataId["instrument"], registry)
941 unboundedCollection = instrument.makeUnboundedCalibrationRunName()
943 return registry.queryDatasets(datasetType,
944 dataId=quantumDataId,
945 collections=[unboundedCollection])