Coverage for python/lsst/fgcmcal/utilities.py: 9%
306 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-25 01:30 -0800
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-25 01:30 -0800
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
32from lsst.daf.butler import Timespan
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
40import fgcm
43FGCM_EXP_FIELD = 'VISIT'
44FGCM_CCD_FIELD = 'DETECTOR'
45FGCM_ILLEGAL_VALUE = -9999.0
48def makeConfigDict(config, log, camera, maxIter,
49 resetFitParameters, outputZeropoints,
50 lutFilterNames, tract=None, nCore=1, doPlots=False):
51 """
52 Make the FGCM fit cycle configuration dict
54 Parameters
55 ----------
56 config : `lsst.fgcmcal.FgcmFitCycleConfig`
57 Configuration object
58 log : `logging.Logger`
59 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.
73 nCore : `int`, optional
74 Number of cores to use.
75 doPlots : `bool`, optional
76 Make FGCM QA plots?
78 Returns
79 -------
80 configDict : `dict`
81 Configuration dictionary for fgcm
82 """
83 # Extract the bands that are _not_ being fit for fgcm configuration
84 notFitBands = [b for b in config.bands if b not in config.fitBands]
86 # process the starColorCuts
87 starColorCutList = []
88 for ccut in config.starColorCuts:
89 if ccut == 'NO_DATA':
90 # No color cuts to apply.
91 break
92 parts = ccut.split(',')
93 starColorCutList.append([parts[0], parts[1], float(parts[2]), float(parts[3])])
95 # process the refStarColorCuts
96 refStarColorCutList = []
97 for ccut in config.refStarColorCuts:
98 if ccut == 'NO_DATA':
99 # No color cuts to apply.
100 break
101 parts = ccut.split(',')
102 refStarColorCutList.append([parts[0], parts[1], float(parts[2]), float(parts[3])])
104 # TODO: Having direct access to the mirror area from the camera would be
105 # useful. See DM-16489.
106 # Mirror area in cm**2
107 if config.mirrorArea is None:
108 mirrorArea = np.pi*(camera.telescopeDiameter*100./2.)**2.
109 else:
110 # Convert to square cm.
111 mirrorArea = config.mirrorArea * 100.**2.
113 if config.cameraGain is None:
114 # Get approximate average camera gain:
115 gains = [amp.getGain() for detector in camera for amp in detector.getAmplifiers()]
116 cameraGain = float(np.median(gains))
117 else:
118 cameraGain = config.cameraGain
120 # Cut down the filter map to those that are in the LUT
121 filterToBand = {filterName: config.physicalFilterMap[filterName] for
122 filterName in lutFilterNames}
124 if tract is None:
125 outfileBase = config.outfileBase
126 else:
127 outfileBase = '%s-%06d' % (config.outfileBase, tract)
129 if config.aperCorrPerCcd:
130 seeingField = 'DELTA_APER_DETECTOR'
131 else:
132 seeingField = 'DELTA_APER'
134 # create a configuration dictionary for fgcmFitCycle
135 configDict = {'outfileBase': outfileBase,
136 'logger': log,
137 'exposureFile': None,
138 'obsFile': None,
139 'indexFile': None,
140 'lutFile': None,
141 'mirrorArea': mirrorArea,
142 'cameraGain': cameraGain,
143 'ccdStartIndex': camera[0].getId(),
144 'expField': FGCM_EXP_FIELD,
145 'ccdField': FGCM_CCD_FIELD,
146 'seeingField': seeingField,
147 'fwhmField': 'PSFFWHM',
148 'skyBrightnessField': 'SKYBACKGROUND',
149 'deepFlag': 'DEEPFLAG', # unused
150 'bands': list(config.bands),
151 'fitBands': list(config.fitBands),
152 'notFitBands': notFitBands,
153 'requiredBands': list(config.requiredBands),
154 'filterToBand': filterToBand,
155 'logLevel': 'INFO',
156 'nCore': nCore,
157 'nStarPerRun': config.nStarPerRun,
158 'nExpPerRun': config.nExpPerRun,
159 'reserveFraction': config.reserveFraction,
160 'freezeStdAtmosphere': config.freezeStdAtmosphere,
161 'precomputeSuperStarInitialCycle': config.precomputeSuperStarInitialCycle,
162 'superStarSubCCDDict': dict(config.superStarSubCcdDict),
163 'superStarSubCCDChebyshevOrder': config.superStarSubCcdChebyshevOrder,
164 'superStarSubCCDTriangular': config.superStarSubCcdTriangular,
165 'superStarSigmaClip': config.superStarSigmaClip,
166 'superStarPlotCCDResiduals': config.superStarPlotCcdResiduals,
167 'superStarForceZeroMean': config.superStarForceZeroMean,
168 'focalPlaneSigmaClip': config.focalPlaneSigmaClip,
169 'ccdGraySubCCDDict': dict(config.ccdGraySubCcdDict),
170 'ccdGraySubCCDChebyshevOrder': config.ccdGraySubCcdChebyshevOrder,
171 'ccdGraySubCCDTriangular': config.ccdGraySubCcdTriangular,
172 'ccdGrayFocalPlaneDict': dict(config.ccdGrayFocalPlaneDict),
173 'ccdGrayFocalPlaneChebyshevOrder': config.ccdGrayFocalPlaneChebyshevOrder,
174 'ccdGrayFocalPlaneFitMinCcd': config.ccdGrayFocalPlaneFitMinCcd,
175 'ccdGrayFocalPlaneMaxStars': config.ccdGrayFocalPlaneMaxStars,
176 'cycleNumber': config.cycleNumber,
177 'maxIter': maxIter,
178 'deltaMagBkgOffsetPercentile': config.deltaMagBkgOffsetPercentile,
179 'deltaMagBkgPerCcd': config.deltaMagBkgPerCcd,
180 'UTBoundary': config.utBoundary,
181 'washMJDs': config.washMjds,
182 'epochMJDs': config.epochMjds,
183 'coatingMJDs': config.coatingMjds,
184 'minObsPerBand': config.minObsPerBand,
185 'latitude': config.latitude,
186 'defaultCameraOrientation': config.defaultCameraOrientation,
187 'brightObsGrayMax': config.brightObsGrayMax,
188 'minStarPerCCD': config.minStarPerCcd,
189 'minCCDPerExp': config.minCcdPerExp,
190 'maxCCDGrayErr': config.maxCcdGrayErr,
191 'minStarPerExp': config.minStarPerExp,
192 'minExpPerNight': config.minExpPerNight,
193 'expGrayInitialCut': config.expGrayInitialCut,
194 'expFwhmCutDict': dict(config.expFwhmCutDict),
195 'expGrayPhotometricCutDict': dict(config.expGrayPhotometricCutDict),
196 'expGrayHighCutDict': dict(config.expGrayHighCutDict),
197 'expGrayRecoverCut': config.expGrayRecoverCut,
198 'expVarGrayPhotometricCutDict': dict(config.expVarGrayPhotometricCutDict),
199 'expGrayErrRecoverCut': config.expGrayErrRecoverCut,
200 'refStarSnMin': config.refStarSnMin,
201 'refStarOutlierNSig': config.refStarOutlierNSig,
202 'applyRefStarColorCuts': config.applyRefStarColorCuts,
203 'refStarMaxFracUse': config.refStarMaxFracUse,
204 'useExposureReferenceOffset': config.useExposureReferenceOffset,
205 'illegalValue': FGCM_ILLEGAL_VALUE, # internally used by fgcm.
206 'starColorCuts': starColorCutList,
207 'refStarColorCuts': refStarColorCutList,
208 'aperCorrFitNBins': config.aperCorrFitNBins,
209 'aperCorrInputSlopeDict': dict(config.aperCorrInputSlopeDict),
210 'sedBoundaryTermDict': config.sedboundaryterms.toDict()['data'],
211 'sedTermDict': config.sedterms.toDict()['data'],
212 'colorSplitBands': list(config.colorSplitBands),
213 'sigFgcmMaxErr': config.sigFgcmMaxErr,
214 'sigFgcmMaxEGrayDict': dict(config.sigFgcmMaxEGrayDict),
215 'ccdGrayMaxStarErr': config.ccdGrayMaxStarErr,
216 'approxThroughputDict': dict(config.approxThroughputDict),
217 'sigmaCalRange': list(config.sigmaCalRange),
218 'sigmaCalFitPercentile': list(config.sigmaCalFitPercentile),
219 'sigmaCalPlotPercentile': list(config.sigmaCalPlotPercentile),
220 'sigma0Phot': config.sigma0Phot,
221 'mapLongitudeRef': config.mapLongitudeRef,
222 'mapNSide': config.mapNSide,
223 'varNSig': 100.0, # Turn off 'variable star selection' which doesn't work yet
224 'varMinBand': 2,
225 'useRetrievedPwv': False,
226 'useNightlyRetrievedPwv': False,
227 'pwvRetrievalSmoothBlock': 25,
228 'useQuadraticPwv': config.useQuadraticPwv,
229 'useRetrievedTauInit': False,
230 'tauRetrievalMinCCDPerNight': 500,
231 'modelMagErrors': config.modelMagErrors,
232 'instrumentParsPerBand': config.instrumentParsPerBand,
233 'instrumentSlopeMinDeltaT': config.instrumentSlopeMinDeltaT,
234 'fitMirrorChromaticity': config.fitMirrorChromaticity,
235 'fitCCDChromaticityDict': dict(config.fitCcdChromaticityDict),
236 'useRepeatabilityForExpGrayCutsDict': dict(config.useRepeatabilityForExpGrayCutsDict),
237 'autoPhotometricCutNSig': config.autoPhotometricCutNSig,
238 'autoHighCutNSig': config.autoHighCutNSig,
239 'deltaAperInnerRadiusArcsec': config.deltaAperInnerRadiusArcsec,
240 'deltaAperOuterRadiusArcsec': config.deltaAperOuterRadiusArcsec,
241 'deltaAperFitMinNgoodObs': config.deltaAperFitMinNgoodObs,
242 'deltaAperFitPerCcdNx': config.deltaAperFitPerCcdNx,
243 'deltaAperFitPerCcdNy': config.deltaAperFitPerCcdNy,
244 'deltaAperFitSpatialNside': config.deltaAperFitSpatialNside,
245 'doComputeDeltaAperExposures': config.doComputeDeltaAperPerVisit,
246 'doComputeDeltaAperStars': config.doComputeDeltaAperPerStar,
247 'doComputeDeltaAperMap': config.doComputeDeltaAperMap,
248 'doComputeDeltaAperPerCcd': config.doComputeDeltaAperPerCcd,
249 'printOnly': False,
250 'quietMode': config.quietMode,
251 'randomSeed': config.randomSeed,
252 'outputStars': False,
253 'outputPath': os.path.abspath('.'),
254 'clobber': True,
255 'useSedLUT': False,
256 'resetParameters': resetFitParameters,
257 'doPlots': doPlots,
258 'outputFgcmcalZpts': True, # when outputting zpts, use fgcmcal format
259 'outputZeropoints': outputZeropoints}
261 return configDict
264def translateFgcmLut(lutCat, physicalFilterMap):
265 """
266 Translate the FGCM look-up-table into an fgcm-compatible object
268 Parameters
269 ----------
270 lutCat: `lsst.afw.table.BaseCatalog`
271 Catalog describing the FGCM look-up table
272 physicalFilterMap: `dict`
273 Physical filter to band mapping
275 Returns
276 -------
277 fgcmLut: `lsst.fgcm.FgcmLut`
278 Lookup table for FGCM
279 lutIndexVals: `numpy.ndarray`
280 Numpy array with LUT index information for FGCM
281 lutStd: `numpy.ndarray`
282 Numpy array with LUT standard throughput values for FGCM
284 Notes
285 -----
286 After running this code, it is wise to `del lutCat` to clear the memory.
287 """
289 # first we need the lutIndexVals
290 lutFilterNames = np.array(lutCat[0]['physicalFilters'].split(','), dtype='U')
291 lutStdFilterNames = np.array(lutCat[0]['stdPhysicalFilters'].split(','), dtype='U')
293 # Note that any discrepancies between config values will raise relevant
294 # exceptions in the FGCM code.
296 lutIndexVals = np.zeros(1, dtype=[('FILTERNAMES', lutFilterNames.dtype.str,
297 lutFilterNames.size),
298 ('STDFILTERNAMES', lutStdFilterNames.dtype.str,
299 lutStdFilterNames.size),
300 ('PMB', 'f8', lutCat[0]['pmb'].size),
301 ('PMBFACTOR', 'f8', lutCat[0]['pmbFactor'].size),
302 ('PMBELEVATION', 'f8'),
303 ('LAMBDANORM', 'f8'),
304 ('PWV', 'f8', lutCat[0]['pwv'].size),
305 ('O3', 'f8', lutCat[0]['o3'].size),
306 ('TAU', 'f8', lutCat[0]['tau'].size),
307 ('ALPHA', 'f8', lutCat[0]['alpha'].size),
308 ('ZENITH', 'f8', lutCat[0]['zenith'].size),
309 ('NCCD', 'i4')])
311 lutIndexVals['FILTERNAMES'][:] = lutFilterNames
312 lutIndexVals['STDFILTERNAMES'][:] = lutStdFilterNames
313 lutIndexVals['PMB'][:] = lutCat[0]['pmb']
314 lutIndexVals['PMBFACTOR'][:] = lutCat[0]['pmbFactor']
315 lutIndexVals['PMBELEVATION'] = lutCat[0]['pmbElevation']
316 lutIndexVals['LAMBDANORM'] = lutCat[0]['lambdaNorm']
317 lutIndexVals['PWV'][:] = lutCat[0]['pwv']
318 lutIndexVals['O3'][:] = lutCat[0]['o3']
319 lutIndexVals['TAU'][:] = lutCat[0]['tau']
320 lutIndexVals['ALPHA'][:] = lutCat[0]['alpha']
321 lutIndexVals['ZENITH'][:] = lutCat[0]['zenith']
322 lutIndexVals['NCCD'] = lutCat[0]['nCcd']
324 # now we need the Standard Values
325 lutStd = np.zeros(1, dtype=[('PMBSTD', 'f8'),
326 ('PWVSTD', 'f8'),
327 ('O3STD', 'f8'),
328 ('TAUSTD', 'f8'),
329 ('ALPHASTD', 'f8'),
330 ('ZENITHSTD', 'f8'),
331 ('LAMBDARANGE', 'f8', 2),
332 ('LAMBDASTEP', 'f8'),
333 ('LAMBDASTD', 'f8', lutFilterNames.size),
334 ('LAMBDASTDFILTER', 'f8', lutStdFilterNames.size),
335 ('I0STD', 'f8', lutFilterNames.size),
336 ('I1STD', 'f8', lutFilterNames.size),
337 ('I10STD', 'f8', lutFilterNames.size),
338 ('I2STD', 'f8', lutFilterNames.size),
339 ('LAMBDAB', 'f8', lutFilterNames.size),
340 ('ATMLAMBDA', 'f8', lutCat[0]['atmLambda'].size),
341 ('ATMSTDTRANS', 'f8', lutCat[0]['atmStdTrans'].size)])
342 lutStd['PMBSTD'] = lutCat[0]['pmbStd']
343 lutStd['PWVSTD'] = lutCat[0]['pwvStd']
344 lutStd['O3STD'] = lutCat[0]['o3Std']
345 lutStd['TAUSTD'] = lutCat[0]['tauStd']
346 lutStd['ALPHASTD'] = lutCat[0]['alphaStd']
347 lutStd['ZENITHSTD'] = lutCat[0]['zenithStd']
348 lutStd['LAMBDARANGE'][:] = lutCat[0]['lambdaRange'][:]
349 lutStd['LAMBDASTEP'] = lutCat[0]['lambdaStep']
350 lutStd['LAMBDASTD'][:] = lutCat[0]['lambdaStd']
351 lutStd['LAMBDASTDFILTER'][:] = lutCat[0]['lambdaStdFilter']
352 lutStd['I0STD'][:] = lutCat[0]['i0Std']
353 lutStd['I1STD'][:] = lutCat[0]['i1Std']
354 lutStd['I10STD'][:] = lutCat[0]['i10Std']
355 lutStd['I2STD'][:] = lutCat[0]['i2Std']
356 lutStd['LAMBDAB'][:] = lutCat[0]['lambdaB']
357 lutStd['ATMLAMBDA'][:] = lutCat[0]['atmLambda'][:]
358 lutStd['ATMSTDTRANS'][:] = lutCat[0]['atmStdTrans'][:]
360 lutTypes = [row['luttype'] for row in lutCat]
362 # And the flattened look-up-table
363 lutFlat = np.zeros(lutCat[0]['lut'].size, dtype=[('I0', 'f4'),
364 ('I1', 'f4')])
366 lutFlat['I0'][:] = lutCat[lutTypes.index('I0')]['lut'][:]
367 lutFlat['I1'][:] = lutCat[lutTypes.index('I1')]['lut'][:]
369 lutDerivFlat = np.zeros(lutCat[0]['lut'].size, dtype=[('D_LNPWV', 'f4'),
370 ('D_O3', 'f4'),
371 ('D_LNTAU', 'f4'),
372 ('D_ALPHA', 'f4'),
373 ('D_SECZENITH', 'f4'),
374 ('D_LNPWV_I1', 'f4'),
375 ('D_O3_I1', 'f4'),
376 ('D_LNTAU_I1', 'f4'),
377 ('D_ALPHA_I1', 'f4'),
378 ('D_SECZENITH_I1', 'f4')])
380 for name in lutDerivFlat.dtype.names:
381 lutDerivFlat[name][:] = lutCat[lutTypes.index(name)]['lut'][:]
383 # The fgcm.FgcmLUT() class copies all the LUT information into special
384 # shared memory objects that will not blow up the memory usage when used
385 # with python multiprocessing. Once all the numbers are copied, the
386 # references to the temporary objects (lutCat, lutFlat, lutDerivFlat)
387 # will fall out of scope and can be cleaned up by the garbage collector.
388 fgcmLut = fgcm.FgcmLUT(lutIndexVals, lutFlat, lutDerivFlat, lutStd,
389 filterToBand=physicalFilterMap)
391 return fgcmLut, lutIndexVals, lutStd
394def translateVisitCatalog(visitCat):
395 """
396 Translate the FGCM visit catalog to an fgcm-compatible object
398 Parameters
399 ----------
400 visitCat: `lsst.afw.table.BaseCatalog`
401 FGCM visitCat from `lsst.fgcmcal.FgcmBuildStarsTask`
403 Returns
404 -------
405 fgcmExpInfo: `numpy.ndarray`
406 Numpy array for visit information for FGCM
408 Notes
409 -----
410 After running this code, it is wise to `del visitCat` to clear the memory.
411 """
413 nDetector = visitCat.schema["deltaAperDetector"].asKey().getSize()
415 fgcmExpInfo = np.zeros(len(visitCat), dtype=[('VISIT', 'i8'),
416 ('MJD', 'f8'),
417 ('EXPTIME', 'f8'),
418 ('PSFFWHM', 'f8'),
419 ('DELTA_APER', 'f8'),
420 ('DELTA_APER_DETECTOR', ('f8', nDetector)),
421 ('SKYBACKGROUND', 'f8'),
422 ('DEEPFLAG', 'i2'),
423 ('TELHA', 'f8'),
424 ('TELRA', 'f8'),
425 ('TELDEC', 'f8'),
426 ('TELROT', 'f8'),
427 ('PMB', 'f8'),
428 ('FILTERNAME', 'S50')])
429 fgcmExpInfo['VISIT'][:] = visitCat['visit']
430 fgcmExpInfo['MJD'][:] = visitCat['mjd']
431 fgcmExpInfo['EXPTIME'][:] = visitCat['exptime']
432 fgcmExpInfo['DEEPFLAG'][:] = visitCat['deepFlag']
433 fgcmExpInfo['TELHA'][:] = visitCat['telha']
434 fgcmExpInfo['TELRA'][:] = visitCat['telra']
435 fgcmExpInfo['TELDEC'][:] = visitCat['teldec']
436 fgcmExpInfo['TELROT'][:] = visitCat['telrot']
437 fgcmExpInfo['PMB'][:] = visitCat['pmb']
438 fgcmExpInfo['PSFFWHM'][:] = visitCat['psfFwhm']
439 fgcmExpInfo['DELTA_APER'][:] = visitCat['deltaAper']
440 fgcmExpInfo['DELTA_APER_DETECTOR'][:, :] = visitCat['deltaAperDetector']
441 fgcmExpInfo['SKYBACKGROUND'][:] = visitCat['skyBackground']
442 # Note that we have to go through asAstropy() to get a string
443 # array out of an afwTable. This is faster than a row-by-row loop.
444 fgcmExpInfo['FILTERNAME'][:] = visitCat.asAstropy()['physicalFilter']
446 return fgcmExpInfo
449def _computeDefaultVisitInfo():
450 """
451 Compute a default visitInfo for use with pixel scale / jacobians.
453 Returns
454 -------
455 visitInfo : `lsst.afw.image.VisitInfo`
456 The visit info object.
457 """
458 # Generate fake WCSs centered at 180/0 to avoid the RA=0/360 problem,
459 # since we are looking for relative scales
460 boresight = geom.SpherePoint(180.0*geom.degrees, 0.0*geom.degrees)
461 orientation = 0.0*geom.degrees
463 # Create a temporary visitInfo for input to createInitialSkyWcs
464 # The orientation does not matter for the area computation
465 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight,
466 boresightRotAngle=orientation,
467 rotType=afwImage.RotType.SKY)
468 return visitInfo
471def computeReferencePixelScale(camera, useScienceDetectors=False):
472 """
473 Compute the median pixel scale in the camera
475 Returns
476 -------
477 pixelScale : `float`
478 Average pixel scale (arcsecond) over the camera
479 useScienceDetectors : `bool`, optional
480 Limit to just science detectors?
481 """
482 visitInfo = _computeDefaultVisitInfo()
484 pixelScales = np.zeros(len(camera))
485 for i, detector in enumerate(camera):
486 if useScienceDetectors:
487 if not detector.getType() == afwCameraGeom.DetectorType.SCIENCE:
488 continue
489 wcs = createInitialSkyWcs(visitInfo, detector, False)
490 pixelScales[i] = wcs.getPixelScale(detector.getBBox().getCenter()).asArcseconds()
492 ok, = np.where(pixelScales > 0.0)
493 return np.median(pixelScales[ok])
496def computePixelAreaFieldDetector(detector, visitInfo=None, areaScaling=1.0, approximate=False):
497 """
498 Compute the reference pixel area field for a detector.
500 Parameters
501 ----------
502 visitInfo : `lsst.afw.image.VisitInfo`
503 The visit info object.
504 detector : `lsst.afw.cameraGeom.detector`
505 The detector object.
506 visitInfo : `lsst.afw.image.VisitInfo`, optional
507 If not supplied, will compute a default visitInfo.
508 areaScaling : `float`, optional
509 Area scaling factor.
510 approximate : `bool`, optional
511 Compute Chebyshev approximation to pixel area field?
513 Returns
514 -------
515 pixelAreaField : `lsst.afw.math.BoundedField`
516 Bounded field describing the pixel area from the detector model.
517 """
518 if visitInfo is None:
519 visitInfo = _computeDefaultVisitInfo()
521 wcs = createInitialSkyWcs(visitInfo, detector, False)
522 bbox = detector.getBBox()
524 pixelAreaField = afwMath.PixelAreaBoundedField(
525 bbox,
526 wcs,
527 unit=geom.arcseconds,
528 scaling=areaScaling,
529 )
531 if approximate:
532 pixelAreaField = afwMath.ChebyshevBoundedField.approximate(pixelAreaField)
534 return pixelAreaField
537def computeApproxPixelAreaFields(camera):
538 """
539 Compute the approximate pixel area bounded fields from the camera
540 geometry.
542 Parameters
543 ----------
544 camera: `lsst.afw.cameraGeom.Camera`
546 Returns
547 -------
548 approxPixelAreaFields: `dict`
549 Dictionary of approximate area fields, keyed with detector ID
550 """
552 areaScaling = 1. / computeReferencePixelScale(camera)**2.
554 visitInfo = _computeDefaultVisitInfo()
556 approxPixelAreaFields = {}
558 for i, detector in enumerate(camera):
559 key = detector.getId()
561 approxAreaField = computePixelAreaFieldDetector(
562 detector,
563 visitInfo=visitInfo,
564 areaScaling=areaScaling,
565 approximate=True,
566 )
568 approxPixelAreaFields[key] = approxAreaField
570 return approxPixelAreaFields
573def makeZptSchema(superStarChebyshevSize, zptChebyshevSize):
574 """
575 Make the zeropoint schema
577 Parameters
578 ----------
579 superStarChebyshevSize: `int`
580 Length of the superstar chebyshev array
581 zptChebyshevSize: `int`
582 Length of the zeropoint chebyshev array
584 Returns
585 -------
586 zptSchema: `lsst.afw.table.schema`
587 """
589 zptSchema = afwTable.Schema()
591 zptSchema.addField('visit', type=np.int64, doc='Visit number')
592 zptSchema.addField('detector', type=np.int32, doc='Detector ID number')
593 zptSchema.addField('fgcmFlag', type=np.int32, doc=('FGCM flag value: '
594 '1: Photometric, used in fit; '
595 '2: Photometric, not used in fit; '
596 '4: Non-photometric, on partly photometric night; '
597 '8: Non-photometric, on non-photometric night; '
598 '16: No zeropoint could be determined; '
599 '32: Too few stars for reliable gray computation'))
600 zptSchema.addField('fgcmZpt', type=np.float64, doc='FGCM zeropoint (center of CCD)')
601 zptSchema.addField('fgcmZptErr', type=np.float64,
602 doc='Error on zeropoint, estimated from repeatability + number of obs')
603 zptSchema.addField('fgcmfZptChebXyMax', type='ArrayD', size=2,
604 doc='maximum x/maximum y to scale to apply chebyshev parameters')
605 zptSchema.addField('fgcmfZptCheb', type='ArrayD',
606 size=zptChebyshevSize,
607 doc='Chebyshev parameters (flattened) for zeropoint')
608 zptSchema.addField('fgcmfZptSstarCheb', type='ArrayD',
609 size=superStarChebyshevSize,
610 doc='Chebyshev parameters (flattened) for superStarFlat')
611 zptSchema.addField('fgcmI0', type=np.float64, doc='Integral of the passband')
612 zptSchema.addField('fgcmI10', type=np.float64, doc='Normalized chromatic integral')
613 zptSchema.addField('fgcmR0', type=np.float64,
614 doc='Retrieved i0 integral, estimated from stars (only for flag 1)')
615 zptSchema.addField('fgcmR10', type=np.float64,
616 doc='Retrieved i10 integral, estimated from stars (only for flag 1)')
617 zptSchema.addField('fgcmGry', type=np.float64,
618 doc='Estimated gray extinction relative to atmospheric solution; '
619 'only for fgcmFlag <= 4 (see fgcmFlag) ')
620 zptSchema.addField('fgcmDeltaChrom', type=np.float64,
621 doc='Mean chromatic correction for stars in this ccd; '
622 'only for fgcmFlag <= 4 (see fgcmFlag)')
623 zptSchema.addField('fgcmZptVar', type=np.float64, doc='Variance of zeropoint over ccd')
624 zptSchema.addField('fgcmTilings', type=np.float64,
625 doc='Number of photometric tilings used for solution for ccd')
626 zptSchema.addField('fgcmFpGry', type=np.float64,
627 doc='Average gray extinction over the full focal plane '
628 '(same for all ccds in a visit)')
629 zptSchema.addField('fgcmFpGryBlue', type=np.float64,
630 doc='Average gray extinction over the full focal plane '
631 'for 25% bluest stars')
632 zptSchema.addField('fgcmFpGryBlueErr', type=np.float64,
633 doc='Error on Average gray extinction over the full focal plane '
634 'for 25% bluest stars')
635 zptSchema.addField('fgcmFpGryRed', type=np.float64,
636 doc='Average gray extinction over the full focal plane '
637 'for 25% reddest stars')
638 zptSchema.addField('fgcmFpGryRedErr', type=np.float64,
639 doc='Error on Average gray extinction over the full focal plane '
640 'for 25% reddest stars')
641 zptSchema.addField('fgcmFpVar', type=np.float64,
642 doc='Variance of gray extinction over the full focal plane '
643 '(same for all ccds in a visit)')
644 zptSchema.addField('fgcmDust', type=np.float64,
645 doc='Gray dust extinction from the primary/corrector'
646 'at the time of the exposure')
647 zptSchema.addField('fgcmFlat', type=np.float64, doc='Superstarflat illumination correction')
648 zptSchema.addField('fgcmAperCorr', type=np.float64, doc='Aperture correction estimated by fgcm')
649 zptSchema.addField('fgcmDeltaMagBkg', type=np.float64,
650 doc=('Local background correction from brightest percentile '
651 '(value set by deltaMagBkgOffsetPercentile) calibration '
652 'stars.'))
653 zptSchema.addField('exptime', type=np.float32, doc='Exposure time')
654 zptSchema.addField('filtername', type=str, size=30, doc='Filter name')
656 return zptSchema
659def makeZptCat(zptSchema, zpStruct):
660 """
661 Make the zeropoint catalog for persistence
663 Parameters
664 ----------
665 zptSchema: `lsst.afw.table.Schema`
666 Zeropoint catalog schema
667 zpStruct: `numpy.ndarray`
668 Zeropoint structure from fgcm
670 Returns
671 -------
672 zptCat: `afwTable.BaseCatalog`
673 Zeropoint catalog for persistence
674 """
676 zptCat = afwTable.BaseCatalog(zptSchema)
677 zptCat.reserve(zpStruct.size)
679 for filterName in zpStruct['FILTERNAME']:
680 rec = zptCat.addNew()
681 rec['filtername'] = filterName.decode('utf-8')
683 zptCat['visit'][:] = zpStruct[FGCM_EXP_FIELD]
684 zptCat['detector'][:] = zpStruct[FGCM_CCD_FIELD]
685 zptCat['fgcmFlag'][:] = zpStruct['FGCM_FLAG']
686 zptCat['fgcmZpt'][:] = zpStruct['FGCM_ZPT']
687 zptCat['fgcmZptErr'][:] = zpStruct['FGCM_ZPTERR']
688 zptCat['fgcmfZptChebXyMax'][:, :] = zpStruct['FGCM_FZPT_XYMAX']
689 zptCat['fgcmfZptCheb'][:, :] = zpStruct['FGCM_FZPT_CHEB']
690 zptCat['fgcmfZptSstarCheb'][:, :] = zpStruct['FGCM_FZPT_SSTAR_CHEB']
691 zptCat['fgcmI0'][:] = zpStruct['FGCM_I0']
692 zptCat['fgcmI10'][:] = zpStruct['FGCM_I10']
693 zptCat['fgcmR0'][:] = zpStruct['FGCM_R0']
694 zptCat['fgcmR10'][:] = zpStruct['FGCM_R10']
695 zptCat['fgcmGry'][:] = zpStruct['FGCM_GRY']
696 zptCat['fgcmDeltaChrom'][:] = zpStruct['FGCM_DELTACHROM']
697 zptCat['fgcmZptVar'][:] = zpStruct['FGCM_ZPTVAR']
698 zptCat['fgcmTilings'][:] = zpStruct['FGCM_TILINGS']
699 zptCat['fgcmFpGry'][:] = zpStruct['FGCM_FPGRY']
700 zptCat['fgcmFpGryBlue'][:] = zpStruct['FGCM_FPGRY_CSPLIT'][:, 0]
701 zptCat['fgcmFpGryBlueErr'][:] = zpStruct['FGCM_FPGRY_CSPLITERR'][:, 0]
702 zptCat['fgcmFpGryRed'][:] = zpStruct['FGCM_FPGRY_CSPLIT'][:, 2]
703 zptCat['fgcmFpGryRedErr'][:] = zpStruct['FGCM_FPGRY_CSPLITERR'][:, 2]
704 zptCat['fgcmFpVar'][:] = zpStruct['FGCM_FPVAR']
705 zptCat['fgcmDust'][:] = zpStruct['FGCM_DUST']
706 zptCat['fgcmFlat'][:] = zpStruct['FGCM_FLAT']
707 zptCat['fgcmAperCorr'][:] = zpStruct['FGCM_APERCORR']
708 zptCat['fgcmDeltaMagBkg'][:] = zpStruct['FGCM_DELTAMAGBKG']
709 zptCat['exptime'][:] = zpStruct['EXPTIME']
711 return zptCat
714def makeAtmSchema():
715 """
716 Make the atmosphere schema
718 Returns
719 -------
720 atmSchema: `lsst.afw.table.Schema`
721 """
723 atmSchema = afwTable.Schema()
725 atmSchema.addField('visit', type=np.int64, doc='Visit number')
726 atmSchema.addField('pmb', type=np.float64, doc='Barometric pressure (mb)')
727 atmSchema.addField('pwv', type=np.float64, doc='Water vapor (mm)')
728 atmSchema.addField('tau', type=np.float64, doc='Aerosol optical depth')
729 atmSchema.addField('alpha', type=np.float64, doc='Aerosol slope')
730 atmSchema.addField('o3', type=np.float64, doc='Ozone (dobson)')
731 atmSchema.addField('secZenith', type=np.float64, doc='Secant(zenith) (~ airmass)')
732 atmSchema.addField('cTrans', type=np.float64, doc='Transmission correction factor')
733 atmSchema.addField('lamStd', type=np.float64, doc='Wavelength for transmission correction')
735 return atmSchema
738def makeAtmCat(atmSchema, atmStruct):
739 """
740 Make the atmosphere catalog for persistence
742 Parameters
743 ----------
744 atmSchema: `lsst.afw.table.Schema`
745 Atmosphere catalog schema
746 atmStruct: `numpy.ndarray`
747 Atmosphere structure from fgcm
749 Returns
750 -------
751 atmCat: `lsst.afw.table.BaseCatalog`
752 Atmosphere catalog for persistence
753 """
755 atmCat = afwTable.BaseCatalog(atmSchema)
756 atmCat.resize(atmStruct.size)
758 atmCat['visit'][:] = atmStruct['VISIT']
759 atmCat['pmb'][:] = atmStruct['PMB']
760 atmCat['pwv'][:] = atmStruct['PWV']
761 atmCat['tau'][:] = atmStruct['TAU']
762 atmCat['alpha'][:] = atmStruct['ALPHA']
763 atmCat['o3'][:] = atmStruct['O3']
764 atmCat['secZenith'][:] = atmStruct['SECZENITH']
765 atmCat['cTrans'][:] = atmStruct['CTRANS']
766 atmCat['lamStd'][:] = atmStruct['LAMSTD']
768 return atmCat
771def makeStdSchema(nBands):
772 """
773 Make the standard star schema
775 Parameters
776 ----------
777 nBands: `int`
778 Number of bands in standard star catalog
780 Returns
781 -------
782 stdSchema: `lsst.afw.table.Schema`
783 """
785 stdSchema = afwTable.SimpleTable.makeMinimalSchema()
786 stdSchema.addField('isolated_star_id', type='L', doc='ID from isolated star table')
787 stdSchema.addField('ngood', type='ArrayI', doc='Number of good observations',
788 size=nBands)
789 stdSchema.addField('ntotal', type='ArrayI', doc='Number of total observations',
790 size=nBands)
791 stdSchema.addField('mag_std_noabs', type='ArrayF',
792 doc='Standard magnitude (no absolute calibration)',
793 size=nBands)
794 stdSchema.addField('magErr_std', type='ArrayF',
795 doc='Standard magnitude error',
796 size=nBands)
797 stdSchema.addField('npsfcand', type='ArrayI',
798 doc='Number of observations flagged as psf candidates',
799 size=nBands)
800 stdSchema.addField('delta_aper', type='ArrayF',
801 doc='Delta mag (small - large aperture)',
802 size=nBands)
804 return stdSchema
807def makeStdCat(stdSchema, stdStruct, goodBands):
808 """
809 Make the standard star catalog for persistence
811 Parameters
812 ----------
813 stdSchema: `lsst.afw.table.Schema`
814 Standard star catalog schema
815 stdStruct: `numpy.ndarray`
816 Standard star structure in FGCM format
817 goodBands: `list`
818 List of good band names used in stdStruct
820 Returns
821 -------
822 stdCat: `lsst.afw.table.BaseCatalog`
823 Standard star catalog for persistence
824 """
826 stdCat = afwTable.SimpleCatalog(stdSchema)
827 stdCat.resize(stdStruct.size)
829 stdCat['id'][:] = stdStruct['FGCM_ID']
830 stdCat['isolated_star_id'][:] = stdStruct['ALTERNATE_ID']
831 stdCat['coord_ra'][:] = stdStruct['RA'] * geom.degrees
832 stdCat['coord_dec'][:] = stdStruct['DEC'] * geom.degrees
833 stdCat['ngood'][:, :] = stdStruct['NGOOD'][:, :]
834 stdCat['ntotal'][:, :] = stdStruct['NTOTAL'][:, :]
835 stdCat['mag_std_noabs'][:, :] = stdStruct['MAG_STD'][:, :]
836 stdCat['magErr_std'][:, :] = stdStruct['MAGERR_STD'][:, :]
837 if 'NPSFCAND' in stdStruct.dtype.names:
838 stdCat['npsfcand'][:, :] = stdStruct['NPSFCAND'][:, :]
839 stdCat['delta_aper'][:, :] = stdStruct['DELTA_APER'][:, :]
841 md = PropertyList()
842 md.set("BANDS", list(goodBands))
843 stdCat.setMetadata(md)
845 return stdCat
848def computeApertureRadiusFromName(fluxField):
849 """
850 Compute the radius associated with a CircularApertureFlux or ApFlux field.
852 Parameters
853 ----------
854 fluxField : `str`
855 CircularApertureFlux or ApFlux
857 Returns
858 -------
859 apertureRadius : `float`
860 Radius of the aperture field, in pixels.
862 Raises
863 ------
864 RuntimeError: Raised if flux field is not a CircularApertureFlux,
865 ApFlux, or apFlux.
866 """
867 # TODO: Move this method to more general stack method in DM-25775
868 m = re.search(r'(CircularApertureFlux|ApFlux|apFlux)_(\d+)_(\d+)_', fluxField)
870 if m is None:
871 raise RuntimeError(f"Flux field {fluxField} does not correspond to a CircularApertureFlux or ApFlux")
873 apertureRadius = float(m.groups()[1]) + float(m.groups()[2])/10.
875 return apertureRadius
878def extractReferenceMags(refStars, bands, filterMap):
879 """
880 Extract reference magnitudes from refStars for given bands and
881 associated filterMap.
883 Parameters
884 ----------
885 refStars : `astropy.table.Table` or `lsst.afw.table.BaseCatalog`
886 FGCM reference star catalog.
887 bands : `list`
888 List of bands for calibration.
889 filterMap: `dict`
890 FGCM mapping of filter to band.
892 Returns
893 -------
894 refMag : `np.ndarray`
895 nstar x nband array of reference magnitudes.
896 refMagErr : `np.ndarray`
897 nstar x nband array of reference magnitude errors.
898 """
899 hasAstropyMeta = False
900 try:
901 meta = refStars.meta
902 hasAstropyMeta = True
903 except AttributeError:
904 meta = refStars.getMetadata()
906 if 'FILTERNAMES' in meta:
907 if hasAstropyMeta:
908 filternames = meta['FILTERNAMES']
909 else:
910 filternames = meta.getArray('FILTERNAMES')
912 # The reference catalog that fgcm wants has one entry per band
913 # in the config file
914 refMag = np.zeros((len(refStars), len(bands)),
915 dtype=refStars['refMag'].dtype) + 99.0
916 refMagErr = np.zeros_like(refMag) + 99.0
917 for i, filtername in enumerate(filternames):
918 # We are allowed to run the fit configured so that we do not
919 # use every column in the reference catalog.
920 try:
921 band = filterMap[filtername]
922 except KeyError:
923 continue
924 try:
925 ind = bands.index(band)
926 except ValueError:
927 continue
929 refMag[:, ind] = refStars['refMag'][:, i]
930 refMagErr[:, ind] = refStars['refMagErr'][:, i]
931 else:
932 raise RuntimeError("FGCM reference stars missing FILTERNAMES metadata.")
934 return refMag, refMagErr
937def lookupStaticCalibrations(datasetType, registry, quantumDataId, collections):
938 # For static calibrations, we search with a timespan that has unbounded
939 # begin and end; we'll get an error if there's more than one match (because
940 # then it's not static).
941 timespan = Timespan(begin=None, end=None)
942 result = []
943 # First iterate over all of the data IDs for this dataset type that are
944 # consistent with the quantum data ID.
945 for dataId in registry.queryDataIds(datasetType.dimensions, dataId=quantumDataId):
946 # Find the dataset with this data ID using the unbounded timespan.
947 if ref := registry.findDataset(datasetType, dataId, collections=collections, timespan=timespan):
948 result.append(ref)
949 return result
952def countDetectors(camera, useScienceDetectors):
953 """Count the detectors in the camera.
955 This may be limited to just the science detectors.
957 Parameters
958 ----------
959 camera : `lsst.afw.cameraGeom.Camera`
960 Camera object.
961 useScienceDetectors : `bool`, optional
962 Limit to just science detectors?
963 """
964 if not useScienceDetectors:
965 return len(camera)
967 nDetector = 0
968 for detector in camera:
969 if not detector.getType() == afwCameraGeom.DetectorType.SCIENCE:
970 continue
971 nDetector += 1
973 return nDetector