Coverage for python/lsst/fgcmcal/utilities.py: 11%
Shortcuts 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
Shortcuts 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
30from deprecated.sphinx import deprecated
32from lsst.daf.base import PropertyList
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.pipe.base import Instrument
41import fgcm
44FGCM_EXP_FIELD = 'VISIT'
45FGCM_CCD_FIELD = 'DETECTOR'
46FGCM_ILLEGAL_VALUE = -9999.0
49def makeConfigDict(config, log, camera, maxIter,
50 resetFitParameters, outputZeropoints,
51 lutFilterNames, tract=None):
52 """
53 Make the FGCM fit cycle configuration dict
55 Parameters
56 ----------
57 config: `lsst.fgcmcal.FgcmFitCycleConfig`
58 Configuration object
59 log: `lsst.log.Log`
60 LSST log object
61 camera: `lsst.afw.cameraGeom.Camera`
62 Camera from the butler
63 maxIter: `int`
64 Maximum number of iterations
65 resetFitParameters: `bool`
66 Reset fit parameters before fitting?
67 outputZeropoints: `bool`
68 Compute zeropoints for output?
69 lutFilterNames : array-like, `str`
70 Array of physical filter names in the LUT.
71 tract: `int`, optional
72 Tract number for extending the output file name for debugging.
73 Default is None.
75 Returns
76 -------
77 configDict: `dict`
78 Configuration dictionary for fgcm
79 """
80 # Extract the bands that are _not_ being fit for fgcm configuration
81 notFitBands = [b for b in config.bands if b not in config.fitBands]
83 # process the starColorCuts
84 starColorCutList = []
85 for ccut in config.starColorCuts:
86 parts = ccut.split(',')
87 starColorCutList.append([parts[0], parts[1], float(parts[2]), float(parts[3])])
89 # TODO: Having direct access to the mirror area from the camera would be
90 # useful. See DM-16489.
91 # Mirror area in cm**2
92 mirrorArea = np.pi*(camera.telescopeDiameter*100./2.)**2.
94 # Get approximate average camera gain:
95 gains = [amp.getGain() for detector in camera for amp in detector.getAmplifiers()]
96 cameraGain = float(np.median(gains))
98 # Cut down the filter map to those that are in the LUT
99 filterToBand = {filterName: config.physicalFilterMap[filterName] for
100 filterName in lutFilterNames}
102 if tract is None:
103 outfileBase = config.outfileBase
104 else:
105 outfileBase = '%s-%06d' % (config.outfileBase, tract)
107 # create a configuration dictionary for fgcmFitCycle
108 configDict = {'outfileBase': outfileBase,
109 'logger': log,
110 'exposureFile': None,
111 'obsFile': None,
112 'indexFile': None,
113 'lutFile': None,
114 'mirrorArea': mirrorArea,
115 'cameraGain': cameraGain,
116 'ccdStartIndex': camera[0].getId(),
117 'expField': FGCM_EXP_FIELD,
118 'ccdField': FGCM_CCD_FIELD,
119 'seeingField': 'DELTA_APER',
120 'fwhmField': 'PSFSIGMA',
121 'skyBrightnessField': 'SKYBACKGROUND',
122 'deepFlag': 'DEEPFLAG', # unused
123 'bands': list(config.bands),
124 'fitBands': list(config.fitBands),
125 'notFitBands': notFitBands,
126 'requiredBands': list(config.requiredBands),
127 'filterToBand': filterToBand,
128 'logLevel': 'INFO',
129 'nCore': config.nCore,
130 'nStarPerRun': config.nStarPerRun,
131 'nExpPerRun': config.nExpPerRun,
132 'reserveFraction': config.reserveFraction,
133 'freezeStdAtmosphere': config.freezeStdAtmosphere,
134 'precomputeSuperStarInitialCycle': config.precomputeSuperStarInitialCycle,
135 'superStarSubCCDDict': dict(config.superStarSubCcdDict),
136 'superStarSubCCDChebyshevOrder': config.superStarSubCcdChebyshevOrder,
137 'superStarSubCCDTriangular': config.superStarSubCcdTriangular,
138 'superStarSigmaClip': config.superStarSigmaClip,
139 'focalPlaneSigmaClip': config.focalPlaneSigmaClip,
140 'ccdGraySubCCDDict': dict(config.ccdGraySubCcdDict),
141 'ccdGraySubCCDChebyshevOrder': config.ccdGraySubCcdChebyshevOrder,
142 'ccdGraySubCCDTriangular': config.ccdGraySubCcdTriangular,
143 'ccdGrayFocalPlaneDict': dict(config.ccdGrayFocalPlaneDict),
144 'ccdGrayFocalPlaneChebyshevOrder': config.ccdGrayFocalPlaneChebyshevOrder,
145 'ccdGrayFocalPlaneFitMinCcd': config.ccdGrayFocalPlaneFitMinCcd,
146 'cycleNumber': config.cycleNumber,
147 'maxIter': maxIter,
148 'deltaMagBkgOffsetPercentile': config.deltaMagBkgOffsetPercentile,
149 'deltaMagBkgPerCcd': config.deltaMagBkgPerCcd,
150 'UTBoundary': config.utBoundary,
151 'washMJDs': config.washMjds,
152 'epochMJDs': config.epochMjds,
153 'coatingMJDs': config.coatingMjds,
154 'minObsPerBand': config.minObsPerBand,
155 'latitude': config.latitude,
156 'defaultCameraOrientation': config.defaultCameraOrientation,
157 'brightObsGrayMax': config.brightObsGrayMax,
158 'minStarPerCCD': config.minStarPerCcd,
159 'minCCDPerExp': config.minCcdPerExp,
160 'maxCCDGrayErr': config.maxCcdGrayErr,
161 'minStarPerExp': config.minStarPerExp,
162 'minExpPerNight': config.minExpPerNight,
163 'expGrayInitialCut': config.expGrayInitialCut,
164 'expGrayPhotometricCutDict': dict(config.expGrayPhotometricCutDict),
165 'expGrayHighCutDict': dict(config.expGrayHighCutDict),
166 'expGrayRecoverCut': config.expGrayRecoverCut,
167 'expVarGrayPhotometricCutDict': dict(config.expVarGrayPhotometricCutDict),
168 'expGrayErrRecoverCut': config.expGrayErrRecoverCut,
169 'refStarSnMin': config.refStarSnMin,
170 'refStarOutlierNSig': config.refStarOutlierNSig,
171 'applyRefStarColorCuts': config.applyRefStarColorCuts,
172 'illegalValue': FGCM_ILLEGAL_VALUE, # internally used by fgcm.
173 'starColorCuts': starColorCutList,
174 'aperCorrFitNBins': config.aperCorrFitNBins,
175 'aperCorrInputSlopeDict': dict(config.aperCorrInputSlopeDict),
176 'sedBoundaryTermDict': config.sedboundaryterms.toDict()['data'],
177 'sedTermDict': config.sedterms.toDict()['data'],
178 'colorSplitBands': list(config.colorSplitBands),
179 'sigFgcmMaxErr': config.sigFgcmMaxErr,
180 'sigFgcmMaxEGrayDict': dict(config.sigFgcmMaxEGrayDict),
181 'ccdGrayMaxStarErr': config.ccdGrayMaxStarErr,
182 'approxThroughputDict': dict(config.approxThroughputDict),
183 'sigmaCalRange': list(config.sigmaCalRange),
184 'sigmaCalFitPercentile': list(config.sigmaCalFitPercentile),
185 'sigmaCalPlotPercentile': list(config.sigmaCalPlotPercentile),
186 'sigma0Phot': config.sigma0Phot,
187 'mapLongitudeRef': config.mapLongitudeRef,
188 'mapNSide': config.mapNSide,
189 'varNSig': 100.0, # Turn off 'variable star selection' which doesn't work yet
190 'varMinBand': 2,
191 'useRetrievedPwv': False,
192 'useNightlyRetrievedPwv': False,
193 'pwvRetrievalSmoothBlock': 25,
194 'useQuadraticPwv': config.useQuadraticPwv,
195 'useRetrievedTauInit': False,
196 'tauRetrievalMinCCDPerNight': 500,
197 'modelMagErrors': config.modelMagErrors,
198 'instrumentParsPerBand': config.instrumentParsPerBand,
199 'instrumentSlopeMinDeltaT': config.instrumentSlopeMinDeltaT,
200 'fitMirrorChromaticity': config.fitMirrorChromaticity,
201 'useRepeatabilityForExpGrayCutsDict': dict(config.useRepeatabilityForExpGrayCutsDict),
202 'autoPhotometricCutNSig': config.autoPhotometricCutNSig,
203 'autoHighCutNSig': config.autoHighCutNSig,
204 'deltaAperInnerRadiusArcsec': config.deltaAperInnerRadiusArcsec,
205 'deltaAperOuterRadiusArcsec': config.deltaAperOuterRadiusArcsec,
206 'deltaAperFitMinNgoodObs': config.deltaAperFitMinNgoodObs,
207 'deltaAperFitPerCcdNx': config.deltaAperFitPerCcdNx,
208 'deltaAperFitPerCcdNy': config.deltaAperFitPerCcdNy,
209 'deltaAperFitSpatialNside': config.deltaAperFitSpatialNside,
210 'doComputeDeltaAperExposures': config.doComputeDeltaAperPerVisit,
211 'doComputeDeltaAperStars': config.doComputeDeltaAperPerStar,
212 'doComputeDeltaAperMap': config.doComputeDeltaAperMap,
213 'doComputeDeltaAperPerCcd': config.doComputeDeltaAperPerCcd,
214 'printOnly': False,
215 'quietMode': config.quietMode,
216 'randomSeed': config.randomSeed,
217 'outputStars': False,
218 'outputPath': os.path.abspath('.'),
219 'clobber': True,
220 'useSedLUT': False,
221 'resetParameters': resetFitParameters,
222 'doPlots': config.doPlots,
223 'outputFgcmcalZpts': True, # when outputting zpts, use fgcmcal format
224 'outputZeropoints': outputZeropoints}
226 return configDict
229def translateFgcmLut(lutCat, physicalFilterMap):
230 """
231 Translate the FGCM look-up-table into an fgcm-compatible object
233 Parameters
234 ----------
235 lutCat: `lsst.afw.table.BaseCatalog`
236 Catalog describing the FGCM look-up table
237 physicalFilterMap: `dict`
238 Physical filter to band mapping
240 Returns
241 -------
242 fgcmLut: `lsst.fgcm.FgcmLut`
243 Lookup table for FGCM
244 lutIndexVals: `numpy.ndarray`
245 Numpy array with LUT index information for FGCM
246 lutStd: `numpy.ndarray`
247 Numpy array with LUT standard throughput values for FGCM
249 Notes
250 -----
251 After running this code, it is wise to `del lutCat` to clear the memory.
252 """
254 # first we need the lutIndexVals
255 lutFilterNames = np.array(lutCat[0]['physicalFilters'].split(','), dtype='U')
256 lutStdFilterNames = np.array(lutCat[0]['stdPhysicalFilters'].split(','), dtype='U')
258 # Note that any discrepancies between config values will raise relevant
259 # exceptions in the FGCM code.
261 lutIndexVals = np.zeros(1, dtype=[('FILTERNAMES', lutFilterNames.dtype.str,
262 lutFilterNames.size),
263 ('STDFILTERNAMES', lutStdFilterNames.dtype.str,
264 lutStdFilterNames.size),
265 ('PMB', 'f8', lutCat[0]['pmb'].size),
266 ('PMBFACTOR', 'f8', lutCat[0]['pmbFactor'].size),
267 ('PMBELEVATION', 'f8'),
268 ('LAMBDANORM', 'f8'),
269 ('PWV', 'f8', lutCat[0]['pwv'].size),
270 ('O3', 'f8', lutCat[0]['o3'].size),
271 ('TAU', 'f8', lutCat[0]['tau'].size),
272 ('ALPHA', 'f8', lutCat[0]['alpha'].size),
273 ('ZENITH', 'f8', lutCat[0]['zenith'].size),
274 ('NCCD', 'i4')])
276 lutIndexVals['FILTERNAMES'][:] = lutFilterNames
277 lutIndexVals['STDFILTERNAMES'][:] = lutStdFilterNames
278 lutIndexVals['PMB'][:] = lutCat[0]['pmb']
279 lutIndexVals['PMBFACTOR'][:] = lutCat[0]['pmbFactor']
280 lutIndexVals['PMBELEVATION'] = lutCat[0]['pmbElevation']
281 lutIndexVals['LAMBDANORM'] = lutCat[0]['lambdaNorm']
282 lutIndexVals['PWV'][:] = lutCat[0]['pwv']
283 lutIndexVals['O3'][:] = lutCat[0]['o3']
284 lutIndexVals['TAU'][:] = lutCat[0]['tau']
285 lutIndexVals['ALPHA'][:] = lutCat[0]['alpha']
286 lutIndexVals['ZENITH'][:] = lutCat[0]['zenith']
287 lutIndexVals['NCCD'] = lutCat[0]['nCcd']
289 # now we need the Standard Values
290 lutStd = np.zeros(1, dtype=[('PMBSTD', 'f8'),
291 ('PWVSTD', 'f8'),
292 ('O3STD', 'f8'),
293 ('TAUSTD', 'f8'),
294 ('ALPHASTD', 'f8'),
295 ('ZENITHSTD', 'f8'),
296 ('LAMBDARANGE', 'f8', 2),
297 ('LAMBDASTEP', 'f8'),
298 ('LAMBDASTD', 'f8', lutFilterNames.size),
299 ('LAMBDASTDFILTER', 'f8', lutStdFilterNames.size),
300 ('I0STD', 'f8', lutFilterNames.size),
301 ('I1STD', 'f8', lutFilterNames.size),
302 ('I10STD', 'f8', lutFilterNames.size),
303 ('I2STD', 'f8', lutFilterNames.size),
304 ('LAMBDAB', 'f8', lutFilterNames.size),
305 ('ATMLAMBDA', 'f8', lutCat[0]['atmLambda'].size),
306 ('ATMSTDTRANS', 'f8', lutCat[0]['atmStdTrans'].size)])
307 lutStd['PMBSTD'] = lutCat[0]['pmbStd']
308 lutStd['PWVSTD'] = lutCat[0]['pwvStd']
309 lutStd['O3STD'] = lutCat[0]['o3Std']
310 lutStd['TAUSTD'] = lutCat[0]['tauStd']
311 lutStd['ALPHASTD'] = lutCat[0]['alphaStd']
312 lutStd['ZENITHSTD'] = lutCat[0]['zenithStd']
313 lutStd['LAMBDARANGE'][:] = lutCat[0]['lambdaRange'][:]
314 lutStd['LAMBDASTEP'] = lutCat[0]['lambdaStep']
315 lutStd['LAMBDASTD'][:] = lutCat[0]['lambdaStd']
316 lutStd['LAMBDASTDFILTER'][:] = lutCat[0]['lambdaStdFilter']
317 lutStd['I0STD'][:] = lutCat[0]['i0Std']
318 lutStd['I1STD'][:] = lutCat[0]['i1Std']
319 lutStd['I10STD'][:] = lutCat[0]['i10Std']
320 lutStd['I2STD'][:] = lutCat[0]['i2Std']
321 lutStd['LAMBDAB'][:] = lutCat[0]['lambdaB']
322 lutStd['ATMLAMBDA'][:] = lutCat[0]['atmLambda'][:]
323 lutStd['ATMSTDTRANS'][:] = lutCat[0]['atmStdTrans'][:]
325 lutTypes = [row['luttype'] for row in lutCat]
327 # And the flattened look-up-table
328 lutFlat = np.zeros(lutCat[0]['lut'].size, dtype=[('I0', 'f4'),
329 ('I1', 'f4')])
331 lutFlat['I0'][:] = lutCat[lutTypes.index('I0')]['lut'][:]
332 lutFlat['I1'][:] = lutCat[lutTypes.index('I1')]['lut'][:]
334 lutDerivFlat = np.zeros(lutCat[0]['lut'].size, dtype=[('D_LNPWV', 'f4'),
335 ('D_O3', 'f4'),
336 ('D_LNTAU', 'f4'),
337 ('D_ALPHA', 'f4'),
338 ('D_SECZENITH', 'f4'),
339 ('D_LNPWV_I1', 'f4'),
340 ('D_O3_I1', 'f4'),
341 ('D_LNTAU_I1', 'f4'),
342 ('D_ALPHA_I1', 'f4'),
343 ('D_SECZENITH_I1', 'f4')])
345 for name in lutDerivFlat.dtype.names:
346 lutDerivFlat[name][:] = lutCat[lutTypes.index(name)]['lut'][:]
348 # The fgcm.FgcmLUT() class copies all the LUT information into special
349 # shared memory objects that will not blow up the memory usage when used
350 # with python multiprocessing. Once all the numbers are copied, the
351 # references to the temporary objects (lutCat, lutFlat, lutDerivFlat)
352 # will fall out of scope and can be cleaned up by the garbage collector.
353 fgcmLut = fgcm.FgcmLUT(lutIndexVals, lutFlat, lutDerivFlat, lutStd,
354 filterToBand=physicalFilterMap)
356 return fgcmLut, lutIndexVals, lutStd
359def translateVisitCatalog(visitCat):
360 """
361 Translate the FGCM visit catalog to an fgcm-compatible object
363 Parameters
364 ----------
365 visitCat: `lsst.afw.table.BaseCatalog`
366 FGCM visitCat from `lsst.fgcmcal.FgcmBuildStarsTask`
368 Returns
369 -------
370 fgcmExpInfo: `numpy.ndarray`
371 Numpy array for visit information for FGCM
373 Notes
374 -----
375 After running this code, it is wise to `del visitCat` to clear the memory.
376 """
378 fgcmExpInfo = np.zeros(len(visitCat), dtype=[('VISIT', 'i8'),
379 ('MJD', 'f8'),
380 ('EXPTIME', 'f8'),
381 ('PSFSIGMA', 'f8'),
382 ('DELTA_APER', 'f8'),
383 ('SKYBACKGROUND', 'f8'),
384 ('DEEPFLAG', 'i2'),
385 ('TELHA', 'f8'),
386 ('TELRA', 'f8'),
387 ('TELDEC', 'f8'),
388 ('TELROT', 'f8'),
389 ('PMB', 'f8'),
390 ('FILTERNAME', 'a50')])
391 fgcmExpInfo['VISIT'][:] = visitCat['visit']
392 fgcmExpInfo['MJD'][:] = visitCat['mjd']
393 fgcmExpInfo['EXPTIME'][:] = visitCat['exptime']
394 fgcmExpInfo['DEEPFLAG'][:] = visitCat['deepFlag']
395 fgcmExpInfo['TELHA'][:] = visitCat['telha']
396 fgcmExpInfo['TELRA'][:] = visitCat['telra']
397 fgcmExpInfo['TELDEC'][:] = visitCat['teldec']
398 fgcmExpInfo['TELROT'][:] = visitCat['telrot']
399 fgcmExpInfo['PMB'][:] = visitCat['pmb']
400 fgcmExpInfo['PSFSIGMA'][:] = visitCat['psfSigma']
401 fgcmExpInfo['DELTA_APER'][:] = visitCat['deltaAper']
402 fgcmExpInfo['SKYBACKGROUND'][:] = visitCat['skyBackground']
403 # Note that we have to go through asAstropy() to get a string
404 # array out of an afwTable. This is faster than a row-by-row loop.
405 fgcmExpInfo['FILTERNAME'][:] = visitCat.asAstropy()['physicalFilter']
407 return fgcmExpInfo
410@deprecated(reason="This method is no longer used in fgcmcal. It will be removed after v23.",
411 version="v23.0", category=FutureWarning)
412def computeCcdOffsets(camera, defaultOrientation):
413 """
414 Compute the CCD offsets in ra/dec and x/y space
416 Parameters
417 ----------
418 camera: `lsst.afw.cameraGeom.Camera`
419 defaultOrientation: `float`
420 Default camera orientation (degrees)
422 Returns
423 -------
424 ccdOffsets: `numpy.ndarray`
425 Numpy array with ccd offset information for input to FGCM.
426 Angular units are degrees, and x/y units are pixels.
427 """
428 # TODO: DM-21215 will fully generalize to arbitrary camera orientations
430 # and we need to know the ccd offsets from the camera geometry
431 ccdOffsets = np.zeros(len(camera), dtype=[('CCDNUM', 'i4'),
432 ('DELTA_RA', 'f8'),
433 ('DELTA_DEC', 'f8'),
434 ('RA_SIZE', 'f8'),
435 ('DEC_SIZE', 'f8'),
436 ('X_SIZE', 'i4'),
437 ('Y_SIZE', 'i4')])
439 # Generate fake WCSs centered at 180/0 to avoid the RA=0/360 problem,
440 # since we are looking for relative positions
441 boresight = geom.SpherePoint(180.0*geom.degrees, 0.0*geom.degrees)
443 # TODO: DM-17597 will update testdata_jointcal so that the test data
444 # does not have nan as the boresight angle for HSC data. For the
445 # time being, there is this ungainly hack.
446 if camera.getName() == 'HSC' and np.isnan(defaultOrientation):
447 orientation = 270*geom.degrees
448 else:
449 orientation = defaultOrientation*geom.degrees
450 flipX = False
452 # Create a temporary visitInfo for input to createInitialSkyWcs
453 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight,
454 boresightRotAngle=orientation,
455 rotType=afwImage.RotType.SKY)
457 for i, detector in enumerate(camera):
458 ccdOffsets['CCDNUM'][i] = detector.getId()
460 wcs = createInitialSkyWcs(visitInfo, detector, flipX)
462 detCenter = wcs.pixelToSky(detector.getCenter(afwCameraGeom.PIXELS))
463 ccdOffsets['DELTA_RA'][i] = (detCenter.getRa() - boresight.getRa()).asDegrees()
464 ccdOffsets['DELTA_DEC'][i] = (detCenter.getDec() - boresight.getDec()).asDegrees()
466 bbox = detector.getBBox()
468 detCorner1 = wcs.pixelToSky(geom.Point2D(bbox.getMin()))
469 detCorner2 = wcs.pixelToSky(geom.Point2D(bbox.getMax()))
471 ccdOffsets['RA_SIZE'][i] = np.abs((detCorner2.getRa() - detCorner1.getRa()).asDegrees())
472 ccdOffsets['DEC_SIZE'][i] = np.abs((detCorner2.getDec() - detCorner1.getDec()).asDegrees())
474 ccdOffsets['X_SIZE'][i] = bbox.getMaxX()
475 ccdOffsets['Y_SIZE'][i] = bbox.getMaxY()
477 return ccdOffsets
480def computeReferencePixelScale(camera):
481 """
482 Compute the median pixel scale in the camera
484 Returns
485 -------
486 pixelScale: `float`
487 Average pixel scale (arcsecond) over the camera
488 """
490 boresight = geom.SpherePoint(180.0*geom.degrees, 0.0*geom.degrees)
491 orientation = 0.0*geom.degrees
492 flipX = False
494 # Create a temporary visitInfo for input to createInitialSkyWcs
495 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight,
496 boresightRotAngle=orientation,
497 rotType=afwImage.RotType.SKY)
499 pixelScales = np.zeros(len(camera))
500 for i, detector in enumerate(camera):
501 wcs = createInitialSkyWcs(visitInfo, detector, flipX)
502 pixelScales[i] = wcs.getPixelScale().asArcseconds()
504 ok, = np.where(pixelScales > 0.0)
505 return np.median(pixelScales[ok])
508def computeApproxPixelAreaFields(camera):
509 """
510 Compute the approximate pixel area bounded fields from the camera
511 geometry.
513 Parameters
514 ----------
515 camera: `lsst.afw.cameraGeom.Camera`
517 Returns
518 -------
519 approxPixelAreaFields: `dict`
520 Dictionary of approximate area fields, keyed with detector ID
521 """
523 areaScaling = 1. / computeReferencePixelScale(camera)**2.
525 # Generate fake WCSs centered at 180/0 to avoid the RA=0/360 problem,
526 # since we are looking for relative scales
527 boresight = geom.SpherePoint(180.0*geom.degrees, 0.0*geom.degrees)
529 flipX = False
530 # Create a temporary visitInfo for input to createInitialSkyWcs
531 # The orientation does not matter for the area computation
532 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight,
533 boresightRotAngle=0.0*geom.degrees,
534 rotType=afwImage.RotType.SKY)
536 approxPixelAreaFields = {}
538 for i, detector in enumerate(camera):
539 key = detector.getId()
541 wcs = createInitialSkyWcs(visitInfo, detector, flipX)
542 bbox = detector.getBBox()
544 areaField = afwMath.PixelAreaBoundedField(bbox, wcs,
545 unit=geom.arcseconds, scaling=areaScaling)
546 approxAreaField = afwMath.ChebyshevBoundedField.approximate(areaField)
548 approxPixelAreaFields[key] = approxAreaField
550 return approxPixelAreaFields
553def makeZptSchema(superStarChebyshevSize, zptChebyshevSize):
554 """
555 Make the zeropoint schema
557 Parameters
558 ----------
559 superStarChebyshevSize: `int`
560 Length of the superstar chebyshev array
561 zptChebyshevSize: `int`
562 Length of the zeropoint chebyshev array
564 Returns
565 -------
566 zptSchema: `lsst.afw.table.schema`
567 """
569 zptSchema = afwTable.Schema()
571 zptSchema.addField('visit', type=np.int32, doc='Visit number')
572 zptSchema.addField('detector', type=np.int32, doc='Detector ID number')
573 zptSchema.addField('fgcmFlag', type=np.int32, doc=('FGCM flag value: '
574 '1: Photometric, used in fit; '
575 '2: Photometric, not used in fit; '
576 '4: Non-photometric, on partly photometric night; '
577 '8: Non-photometric, on non-photometric night; '
578 '16: No zeropoint could be determined; '
579 '32: Too few stars for reliable gray computation'))
580 zptSchema.addField('fgcmZpt', type=np.float64, doc='FGCM zeropoint (center of CCD)')
581 zptSchema.addField('fgcmZptErr', type=np.float64,
582 doc='Error on zeropoint, estimated from repeatability + number of obs')
583 zptSchema.addField('fgcmfZptChebXyMax', type='ArrayD', size=2,
584 doc='maximum x/maximum y to scale to apply chebyshev parameters')
585 zptSchema.addField('fgcmfZptCheb', type='ArrayD',
586 size=zptChebyshevSize,
587 doc='Chebyshev parameters (flattened) for zeropoint')
588 zptSchema.addField('fgcmfZptSstarCheb', type='ArrayD',
589 size=superStarChebyshevSize,
590 doc='Chebyshev parameters (flattened) for superStarFlat')
591 zptSchema.addField('fgcmI0', type=np.float64, doc='Integral of the passband')
592 zptSchema.addField('fgcmI10', type=np.float64, doc='Normalized chromatic integral')
593 zptSchema.addField('fgcmR0', type=np.float64,
594 doc='Retrieved i0 integral, estimated from stars (only for flag 1)')
595 zptSchema.addField('fgcmR10', type=np.float64,
596 doc='Retrieved i10 integral, estimated from stars (only for flag 1)')
597 zptSchema.addField('fgcmGry', type=np.float64,
598 doc='Estimated gray extinction relative to atmospheric solution; '
599 'only for fgcmFlag <= 4 (see fgcmFlag) ')
600 zptSchema.addField('fgcmDeltaChrom', type=np.float64,
601 doc='Mean chromatic correction for stars in this ccd; '
602 'only for fgcmFlag <= 4 (see fgcmFlag)')
603 zptSchema.addField('fgcmZptVar', type=np.float64, doc='Variance of zeropoint over ccd')
604 zptSchema.addField('fgcmTilings', type=np.float64,
605 doc='Number of photometric tilings used for solution for ccd')
606 zptSchema.addField('fgcmFpGry', type=np.float64,
607 doc='Average gray extinction over the full focal plane '
608 '(same for all ccds in a visit)')
609 zptSchema.addField('fgcmFpGryBlue', type=np.float64,
610 doc='Average gray extinction over the full focal plane '
611 'for 25% bluest stars')
612 zptSchema.addField('fgcmFpGryBlueErr', type=np.float64,
613 doc='Error on Average gray extinction over the full focal plane '
614 'for 25% bluest stars')
615 zptSchema.addField('fgcmFpGryRed', type=np.float64,
616 doc='Average gray extinction over the full focal plane '
617 'for 25% reddest stars')
618 zptSchema.addField('fgcmFpGryRedErr', type=np.float64,
619 doc='Error on Average gray extinction over the full focal plane '
620 'for 25% reddest stars')
621 zptSchema.addField('fgcmFpVar', type=np.float64,
622 doc='Variance of gray extinction over the full focal plane '
623 '(same for all ccds in a visit)')
624 zptSchema.addField('fgcmDust', type=np.float64,
625 doc='Gray dust extinction from the primary/corrector'
626 'at the time of the exposure')
627 zptSchema.addField('fgcmFlat', type=np.float64, doc='Superstarflat illumination correction')
628 zptSchema.addField('fgcmAperCorr', type=np.float64, doc='Aperture correction estimated by fgcm')
629 zptSchema.addField('fgcmDeltaMagBkg', type=np.float64,
630 doc=('Local background correction from brightest percentile '
631 '(value set by deltaMagBkgOffsetPercentile) calibration '
632 'stars.'))
633 zptSchema.addField('exptime', type=np.float32, doc='Exposure time')
634 zptSchema.addField('filtername', type=str, size=10, doc='Filter name')
636 return zptSchema
639def makeZptCat(zptSchema, zpStruct):
640 """
641 Make the zeropoint catalog for persistence
643 Parameters
644 ----------
645 zptSchema: `lsst.afw.table.Schema`
646 Zeropoint catalog schema
647 zpStruct: `numpy.ndarray`
648 Zeropoint structure from fgcm
650 Returns
651 -------
652 zptCat: `afwTable.BaseCatalog`
653 Zeropoint catalog for persistence
654 """
656 zptCat = afwTable.BaseCatalog(zptSchema)
657 zptCat.reserve(zpStruct.size)
659 for filterName in zpStruct['FILTERNAME']:
660 rec = zptCat.addNew()
661 rec['filtername'] = filterName.decode('utf-8')
663 zptCat['visit'][:] = zpStruct[FGCM_EXP_FIELD]
664 zptCat['detector'][:] = zpStruct[FGCM_CCD_FIELD]
665 zptCat['fgcmFlag'][:] = zpStruct['FGCM_FLAG']
666 zptCat['fgcmZpt'][:] = zpStruct['FGCM_ZPT']
667 zptCat['fgcmZptErr'][:] = zpStruct['FGCM_ZPTERR']
668 zptCat['fgcmfZptChebXyMax'][:, :] = zpStruct['FGCM_FZPT_XYMAX']
669 zptCat['fgcmfZptCheb'][:, :] = zpStruct['FGCM_FZPT_CHEB']
670 zptCat['fgcmfZptSstarCheb'][:, :] = zpStruct['FGCM_FZPT_SSTAR_CHEB']
671 zptCat['fgcmI0'][:] = zpStruct['FGCM_I0']
672 zptCat['fgcmI10'][:] = zpStruct['FGCM_I10']
673 zptCat['fgcmR0'][:] = zpStruct['FGCM_R0']
674 zptCat['fgcmR10'][:] = zpStruct['FGCM_R10']
675 zptCat['fgcmGry'][:] = zpStruct['FGCM_GRY']
676 zptCat['fgcmDeltaChrom'][:] = zpStruct['FGCM_DELTACHROM']
677 zptCat['fgcmZptVar'][:] = zpStruct['FGCM_ZPTVAR']
678 zptCat['fgcmTilings'][:] = zpStruct['FGCM_TILINGS']
679 zptCat['fgcmFpGry'][:] = zpStruct['FGCM_FPGRY']
680 zptCat['fgcmFpGryBlue'][:] = zpStruct['FGCM_FPGRY_CSPLIT'][:, 0]
681 zptCat['fgcmFpGryBlueErr'][:] = zpStruct['FGCM_FPGRY_CSPLITERR'][:, 0]
682 zptCat['fgcmFpGryRed'][:] = zpStruct['FGCM_FPGRY_CSPLIT'][:, 2]
683 zptCat['fgcmFpGryRedErr'][:] = zpStruct['FGCM_FPGRY_CSPLITERR'][:, 2]
684 zptCat['fgcmFpVar'][:] = zpStruct['FGCM_FPVAR']
685 zptCat['fgcmDust'][:] = zpStruct['FGCM_DUST']
686 zptCat['fgcmFlat'][:] = zpStruct['FGCM_FLAT']
687 zptCat['fgcmAperCorr'][:] = zpStruct['FGCM_APERCORR']
688 zptCat['fgcmDeltaMagBkg'][:] = zpStruct['FGCM_DELTAMAGBKG']
689 zptCat['exptime'][:] = zpStruct['EXPTIME']
691 return zptCat
694def makeAtmSchema():
695 """
696 Make the atmosphere schema
698 Returns
699 -------
700 atmSchema: `lsst.afw.table.Schema`
701 """
703 atmSchema = afwTable.Schema()
705 atmSchema.addField('visit', type=np.int32, doc='Visit number')
706 atmSchema.addField('pmb', type=np.float64, doc='Barometric pressure (mb)')
707 atmSchema.addField('pwv', type=np.float64, doc='Water vapor (mm)')
708 atmSchema.addField('tau', type=np.float64, doc='Aerosol optical depth')
709 atmSchema.addField('alpha', type=np.float64, doc='Aerosol slope')
710 atmSchema.addField('o3', type=np.float64, doc='Ozone (dobson)')
711 atmSchema.addField('secZenith', type=np.float64, doc='Secant(zenith) (~ airmass)')
712 atmSchema.addField('cTrans', type=np.float64, doc='Transmission correction factor')
713 atmSchema.addField('lamStd', type=np.float64, doc='Wavelength for transmission correction')
715 return atmSchema
718def makeAtmCat(atmSchema, atmStruct):
719 """
720 Make the atmosphere catalog for persistence
722 Parameters
723 ----------
724 atmSchema: `lsst.afw.table.Schema`
725 Atmosphere catalog schema
726 atmStruct: `numpy.ndarray`
727 Atmosphere structure from fgcm
729 Returns
730 -------
731 atmCat: `lsst.afw.table.BaseCatalog`
732 Atmosphere catalog for persistence
733 """
735 atmCat = afwTable.BaseCatalog(atmSchema)
736 atmCat.resize(atmStruct.size)
738 atmCat['visit'][:] = atmStruct['VISIT']
739 atmCat['pmb'][:] = atmStruct['PMB']
740 atmCat['pwv'][:] = atmStruct['PWV']
741 atmCat['tau'][:] = atmStruct['TAU']
742 atmCat['alpha'][:] = atmStruct['ALPHA']
743 atmCat['o3'][:] = atmStruct['O3']
744 atmCat['secZenith'][:] = atmStruct['SECZENITH']
745 atmCat['cTrans'][:] = atmStruct['CTRANS']
746 atmCat['lamStd'][:] = atmStruct['LAMSTD']
748 return atmCat
751def makeStdSchema(nBands):
752 """
753 Make the standard star schema
755 Parameters
756 ----------
757 nBands: `int`
758 Number of bands in standard star catalog
760 Returns
761 -------
762 stdSchema: `lsst.afw.table.Schema`
763 """
765 stdSchema = afwTable.SimpleTable.makeMinimalSchema()
766 stdSchema.addField('ngood', type='ArrayI', doc='Number of good observations',
767 size=nBands)
768 stdSchema.addField('ntotal', type='ArrayI', doc='Number of total observations',
769 size=nBands)
770 stdSchema.addField('mag_std_noabs', type='ArrayF',
771 doc='Standard magnitude (no absolute calibration)',
772 size=nBands)
773 stdSchema.addField('magErr_std', type='ArrayF',
774 doc='Standard magnitude error',
775 size=nBands)
776 stdSchema.addField('npsfcand', type='ArrayI',
777 doc='Number of observations flagged as psf candidates',
778 size=nBands)
779 stdSchema.addField('delta_aper', type='ArrayF',
780 doc='Delta mag (small - large aperture)',
781 size=nBands)
783 return stdSchema
786def makeStdCat(stdSchema, stdStruct, goodBands):
787 """
788 Make the standard star catalog for persistence
790 Parameters
791 ----------
792 stdSchema: `lsst.afw.table.Schema`
793 Standard star catalog schema
794 stdStruct: `numpy.ndarray`
795 Standard star structure in FGCM format
796 goodBands: `list`
797 List of good band names used in stdStruct
799 Returns
800 -------
801 stdCat: `lsst.afw.table.BaseCatalog`
802 Standard star catalog for persistence
803 """
805 stdCat = afwTable.SimpleCatalog(stdSchema)
806 stdCat.resize(stdStruct.size)
808 stdCat['id'][:] = stdStruct['FGCM_ID']
809 stdCat['coord_ra'][:] = stdStruct['RA'] * geom.degrees
810 stdCat['coord_dec'][:] = stdStruct['DEC'] * geom.degrees
811 stdCat['ngood'][:, :] = stdStruct['NGOOD'][:, :]
812 stdCat['ntotal'][:, :] = stdStruct['NTOTAL'][:, :]
813 stdCat['mag_std_noabs'][:, :] = stdStruct['MAG_STD'][:, :]
814 stdCat['magErr_std'][:, :] = stdStruct['MAGERR_STD'][:, :]
815 stdCat['npsfcand'][:, :] = stdStruct['NPSFCAND'][:, :]
816 stdCat['delta_aper'][:, :] = stdStruct['DELTA_APER'][:, :]
818 md = PropertyList()
819 md.set("BANDS", list(goodBands))
820 stdCat.setMetadata(md)
822 return stdCat
825def computeApertureRadiusFromName(fluxField):
826 """
827 Compute the radius associated with a CircularApertureFlux or ApFlux field.
829 Parameters
830 ----------
831 fluxField : `str`
832 CircularApertureFlux or ApFlux
834 Returns
835 -------
836 apertureRadius : `float`
837 Radius of the aperture field, in pixels.
839 Raises
840 ------
841 RuntimeError: Raised if flux field is not a CircularApertureFlux,
842 ApFlux, or apFlux.
843 """
844 # TODO: Move this method to more general stack method in DM-25775
845 m = re.search(r'(CircularApertureFlux|ApFlux|apFlux)_(\d+)_(\d+)_', fluxField)
847 if m is None:
848 raise RuntimeError(f"Flux field {fluxField} does not correspond to a CircularApertureFlux or ApFlux")
850 apertureRadius = float(m.groups()[1]) + float(m.groups()[2])/10.
852 return apertureRadius
855def extractReferenceMags(refStars, bands, filterMap):
856 """
857 Extract reference magnitudes from refStars for given bands and
858 associated filterMap.
860 Parameters
861 ----------
862 refStars : `lsst.afw.table.BaseCatalog`
863 FGCM reference star catalog
864 bands : `list`
865 List of bands for calibration
866 filterMap: `dict`
867 FGCM mapping of filter to band
869 Returns
870 -------
871 refMag : `np.ndarray`
872 nstar x nband array of reference magnitudes
873 refMagErr : `np.ndarray`
874 nstar x nband array of reference magnitude errors
875 """
876 # After DM-23331 fgcm reference catalogs have FILTERNAMES to prevent
877 # against index errors and allow more flexibility in fitting after
878 # the build stars step.
880 md = refStars.getMetadata()
881 if 'FILTERNAMES' in md:
882 filternames = md.getArray('FILTERNAMES')
884 # The reference catalog that fgcm wants has one entry per band
885 # in the config file
886 refMag = np.zeros((len(refStars), len(bands)),
887 dtype=refStars['refMag'].dtype) + 99.0
888 refMagErr = np.zeros_like(refMag) + 99.0
889 for i, filtername in enumerate(filternames):
890 # We are allowed to run the fit configured so that we do not
891 # use every column in the reference catalog.
892 try:
893 band = filterMap[filtername]
894 except KeyError:
895 continue
896 try:
897 ind = bands.index(band)
898 except ValueError:
899 continue
901 refMag[:, ind] = refStars['refMag'][:, i]
902 refMagErr[:, ind] = refStars['refMagErr'][:, i]
904 else:
905 # Continue to use old catalogs as before.
906 refMag = refStars['refMag'][:, :]
907 refMagErr = refStars['refMagErr'][:, :]
909 return refMag, refMagErr
912def lookupStaticCalibrations(datasetType, registry, quantumDataId, collections):
913 instrument = Instrument.fromName(quantumDataId["instrument"], registry)
914 unboundedCollection = instrument.makeUnboundedCalibrationRunName()
916 return registry.queryDatasets(datasetType,
917 dataId=quantumDataId,
918 collections=[unboundedCollection])