lsst.fgcmcal g437c944c15+8a301509d2
utilities.py
Go to the documentation of this file.
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.
22
23This file contains utility functions that are used by more than one task,
24and do not need to be part of a task.
25"""
26
27import numpy as np
28import os
29import re
30from deprecated.sphinx import deprecated
31
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
40
41import fgcm
42
43
44FGCM_EXP_FIELD = 'VISIT'
45FGCM_CCD_FIELD = 'DETECTOR'
46FGCM_ILLEGAL_VALUE = -9999.0
47
48
49def makeConfigDict(config, log, camera, maxIter,
50 resetFitParameters, outputZeropoints,
51 lutFilterNames, tract=None):
52 """
53 Make the FGCM fit cycle configuration dict
54
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.
74
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]
82
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])])
88
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.
93
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))
97
98 # Cut down the filter map to those that are in the LUT
99 filterToBand = {filterName: config.physicalFilterMap[filterName] for
100 filterName in lutFilterNames}
101
102 if tract is None:
103 outfileBase = config.outfileBase
104 else:
105 outfileBase = '%s-%06d' % (config.outfileBase, tract)
106
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}
225
226 return configDict
227
228
229def translateFgcmLut(lutCat, physicalFilterMap):
230 """
231 Translate the FGCM look-up-table into an fgcm-compatible object
232
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
239
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
248
249 Notes
250 -----
251 After running this code, it is wise to `del lutCat` to clear the memory.
252 """
253
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')
257
258 # Note that any discrepancies between config values will raise relevant
259 # exceptions in the FGCM code.
260
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')])
275
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']
288
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'][:]
324
325 lutTypes = [row['luttype'] for row in lutCat]
326
327 # And the flattened look-up-table
328 lutFlat = np.zeros(lutCat[0]['lut'].size, dtype=[('I0', 'f4'),
329 ('I1', 'f4')])
330
331 lutFlat['I0'][:] = lutCat[lutTypes.index('I0')]['lut'][:]
332 lutFlat['I1'][:] = lutCat[lutTypes.index('I1')]['lut'][:]
333
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')])
344
345 for name in lutDerivFlat.dtype.names:
346 lutDerivFlat[name][:] = lutCat[lutTypes.index(name)]['lut'][:]
347
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)
355
356 return fgcmLut, lutIndexVals, lutStd
357
358
360 """
361 Translate the FGCM visit catalog to an fgcm-compatible object
362
363 Parameters
364 ----------
365 visitCat: `lsst.afw.table.BaseCatalog`
366 FGCM visitCat from `lsst.fgcmcal.FgcmBuildStarsTask`
367
368 Returns
369 -------
370 fgcmExpInfo: `numpy.ndarray`
371 Numpy array for visit information for FGCM
372
373 Notes
374 -----
375 After running this code, it is wise to `del visitCat` to clear the memory.
376 """
377
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']
406
407 return fgcmExpInfo
408
409
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
415
416 Parameters
417 ----------
418 camera: `lsst.afw.cameraGeom.Camera`
419 defaultOrientation: `float`
420 Default camera orientation (degrees)
421
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
429
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')])
438
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)
442
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
451
452 # Create a temporary visitInfo for input to createInitialSkyWcs
453 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight,
454 boresightRotAngle=orientation,
455 rotType=afwImage.RotType.SKY)
456
457 for i, detector in enumerate(camera):
458 ccdOffsets['CCDNUM'][i] = detector.getId()
459
460 wcs = createInitialSkyWcs(visitInfo, detector, flipX)
461
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()
465
466 bbox = detector.getBBox()
467
468 detCorner1 = wcs.pixelToSky(geom.Point2D(bbox.getMin()))
469 detCorner2 = wcs.pixelToSky(geom.Point2D(bbox.getMax()))
470
471 ccdOffsets['RA_SIZE'][i] = np.abs((detCorner2.getRa() - detCorner1.getRa()).asDegrees())
472 ccdOffsets['DEC_SIZE'][i] = np.abs((detCorner2.getDec() - detCorner1.getDec()).asDegrees())
473
474 ccdOffsets['X_SIZE'][i] = bbox.getMaxX()
475 ccdOffsets['Y_SIZE'][i] = bbox.getMaxY()
476
477 return ccdOffsets
478
479
481 """
482 Compute the median pixel scale in the camera
483
484 Returns
485 -------
486 pixelScale: `float`
487 Average pixel scale (arcsecond) over the camera
488 """
489
490 boresight = geom.SpherePoint(180.0*geom.degrees, 0.0*geom.degrees)
491 orientation = 0.0*geom.degrees
492 flipX = False
493
494 # Create a temporary visitInfo for input to createInitialSkyWcs
495 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight,
496 boresightRotAngle=orientation,
497 rotType=afwImage.RotType.SKY)
498
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()
503
504 ok, = np.where(pixelScales > 0.0)
505 return np.median(pixelScales[ok])
506
507
509 """
510 Compute the approximate pixel area bounded fields from the camera
511 geometry.
512
513 Parameters
514 ----------
515 camera: `lsst.afw.cameraGeom.Camera`
516
517 Returns
518 -------
519 approxPixelAreaFields: `dict`
520 Dictionary of approximate area fields, keyed with detector ID
521 """
522
523 areaScaling = 1. / computeReferencePixelScale(camera)**2.
524
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)
528
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)
535
536 approxPixelAreaFields = {}
537
538 for i, detector in enumerate(camera):
539 key = detector.getId()
540
541 wcs = createInitialSkyWcs(visitInfo, detector, flipX)
542 bbox = detector.getBBox()
543
544 areaField = afwMath.PixelAreaBoundedField(bbox, wcs,
545 unit=geom.arcseconds, scaling=areaScaling)
546 approxAreaField = afwMath.ChebyshevBoundedField.approximate(areaField)
547
548 approxPixelAreaFields[key] = approxAreaField
549
550 return approxPixelAreaFields
551
552
553def makeZptSchema(superStarChebyshevSize, zptChebyshevSize):
554 """
555 Make the zeropoint schema
556
557 Parameters
558 ----------
559 superStarChebyshevSize: `int`
560 Length of the superstar chebyshev array
561 zptChebyshevSize: `int`
562 Length of the zeropoint chebyshev array
563
564 Returns
565 -------
566 zptSchema: `lsst.afw.table.schema`
567 """
568
569 zptSchema = afwTable.Schema()
570
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')
635
636 return zptSchema
637
638
639def makeZptCat(zptSchema, zpStruct):
640 """
641 Make the zeropoint catalog for persistence
642
643 Parameters
644 ----------
645 zptSchema: `lsst.afw.table.Schema`
646 Zeropoint catalog schema
647 zpStruct: `numpy.ndarray`
648 Zeropoint structure from fgcm
649
650 Returns
651 -------
652 zptCat: `afwTable.BaseCatalog`
653 Zeropoint catalog for persistence
654 """
655
656 zptCat = afwTable.BaseCatalog(zptSchema)
657 zptCat.reserve(zpStruct.size)
658
659 for filterName in zpStruct['FILTERNAME']:
660 rec = zptCat.addNew()
661 rec['filtername'] = filterName.decode('utf-8')
662
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']
690
691 return zptCat
692
693
695 """
696 Make the atmosphere schema
697
698 Returns
699 -------
700 atmSchema: `lsst.afw.table.Schema`
701 """
702
703 atmSchema = afwTable.Schema()
704
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')
714
715 return atmSchema
716
717
718def makeAtmCat(atmSchema, atmStruct):
719 """
720 Make the atmosphere catalog for persistence
721
722 Parameters
723 ----------
724 atmSchema: `lsst.afw.table.Schema`
725 Atmosphere catalog schema
726 atmStruct: `numpy.ndarray`
727 Atmosphere structure from fgcm
728
729 Returns
730 -------
731 atmCat: `lsst.afw.table.BaseCatalog`
732 Atmosphere catalog for persistence
733 """
734
735 atmCat = afwTable.BaseCatalog(atmSchema)
736 atmCat.resize(atmStruct.size)
737
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']
747
748 return atmCat
749
750
751def makeStdSchema(nBands):
752 """
753 Make the standard star schema
754
755 Parameters
756 ----------
757 nBands: `int`
758 Number of bands in standard star catalog
759
760 Returns
761 -------
762 stdSchema: `lsst.afw.table.Schema`
763 """
764
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)
782
783 return stdSchema
784
785
786def makeStdCat(stdSchema, stdStruct, goodBands):
787 """
788 Make the standard star catalog for persistence
789
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
798
799 Returns
800 -------
801 stdCat: `lsst.afw.table.BaseCatalog`
802 Standard star catalog for persistence
803 """
804
805 stdCat = afwTable.SimpleCatalog(stdSchema)
806 stdCat.resize(stdStruct.size)
807
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'][:, :]
817
818 md = PropertyList()
819 md.set("BANDS", list(goodBands))
820 stdCat.setMetadata(md)
821
822 return stdCat
823
824
826 """
827 Compute the radius associated with a CircularApertureFlux or ApFlux field.
828
829 Parameters
830 ----------
831 fluxField : `str`
832 CircularApertureFlux or ApFlux
833
834 Returns
835 -------
836 apertureRadius : `float`
837 Radius of the aperture field, in pixels.
838
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)
846
847 if m is None:
848 raise RuntimeError(f"Flux field {fluxField} does not correspond to a CircularApertureFlux or ApFlux")
849
850 apertureRadius = float(m.groups()[1]) + float(m.groups()[2])/10.
851
852 return apertureRadius
853
854
855def extractReferenceMags(refStars, bands, filterMap):
856 """
857 Extract reference magnitudes from refStars for given bands and
858 associated filterMap.
859
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
868
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.
879
880 md = refStars.getMetadata()
881 if 'FILTERNAMES' in md:
882 filternames = md.getArray('FILTERNAMES')
883
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
900
901 refMag[:, ind] = refStars['refMag'][:, i]
902 refMagErr[:, ind] = refStars['refMagErr'][:, i]
903
904 else:
905 # Continue to use old catalogs as before.
906 refMag = refStars['refMag'][:, :]
907 refMagErr = refStars['refMagErr'][:, :]
908
909 return refMag, refMagErr
910
911
912def lookupStaticCalibrations(datasetType, registry, quantumDataId, collections):
913 instrument = Instrument.fromName(quantumDataId["instrument"], registry)
914 unboundedCollection = instrument.makeUnboundedCalibrationRunName()
915
916 return registry.queryDatasets(datasetType,
917 dataId=quantumDataId,
918 collections=[unboundedCollection])
def extractReferenceMags(refStars, bands, filterMap)
Definition: utilities.py:855
def lookupStaticCalibrations(datasetType, registry, quantumDataId, collections)
Definition: utilities.py:912
def makeStdSchema(nBands)
Definition: utilities.py:751
def makeAtmCat(atmSchema, atmStruct)
Definition: utilities.py:718
def makeConfigDict(config, log, camera, maxIter, resetFitParameters, outputZeropoints, lutFilterNames, tract=None)
Definition: utilities.py:51
def translateFgcmLut(lutCat, physicalFilterMap)
Definition: utilities.py:229
def computeReferencePixelScale(camera)
Definition: utilities.py:480
def makeZptCat(zptSchema, zpStruct)
Definition: utilities.py:639
def makeStdCat(stdSchema, stdStruct, goodBands)
Definition: utilities.py:786
def computeApertureRadiusFromName(fluxField)
Definition: utilities.py:825
def computeApproxPixelAreaFields(camera)
Definition: utilities.py:508
def makeZptSchema(superStarChebyshevSize, zptChebyshevSize)
Definition: utilities.py:553
def computeCcdOffsets(camera, defaultOrientation)
Definition: utilities.py:412
def translateVisitCatalog(visitCat)
Definition: utilities.py:359