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

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# See COPYRIGHT file at the top of the source tree.
2#
3# This file is part of fgcmcal.
4#
5# Developed for the LSST Data Management System.
6# This product includes software developed by the LSST Project
7# (https://www.lsst.org).
8# See the COPYRIGHT file at the top-level directory of this distribution
9# for details of code ownership.
10#
11# This program is free software: you can redistribute it and/or modify
12# it under the terms of the GNU General Public License as published by
13# the Free Software Foundation, either version 3 of the License, or
14# (at your option) any later version.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program. If not, see <https://www.gnu.org/licenses/>.
23"""Utility functions for fgcmcal.
25This file contains utility functions that are used by more than one task,
26and do not need to be part of a task.
27"""
29import numpy as np
30import re
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
40import fgcm
43def makeConfigDict(config, log, camera, maxIter,
44 resetFitParameters, outputZeropoints, tract=None):
45 """
46 Make the FGCM fit cycle configuration dict
48 Parameters
49 ----------
50 config: `lsst.fgcmcal.FgcmFitCycleConfig`
51 Configuration object
52 log: `lsst.log.Log`
53 LSST log object
54 camera: `lsst.afw.cameraGeom.Camera`
55 Camera from the butler
56 maxIter: `int`
57 Maximum number of iterations
58 resetFitParameters: `bool`
59 Reset fit parameters before fitting?
60 outputZeropoints: `bool`
61 Compute zeropoints for output?
62 tract: `int`, optional
63 Tract number for extending the output file name for debugging.
64 Default is None.
66 Returns
67 -------
68 configDict: `dict`
69 Configuration dictionary for fgcm
70 """
72 fitFlag = np.array(config.fitFlag, dtype=np.bool)
73 requiredFlag = np.array(config.requiredFlag, dtype=np.bool)
75 fitBands = [b for i, b in enumerate(config.bands) if fitFlag[i]]
76 notFitBands = [b for i, b in enumerate(config.bands) if not fitFlag[i]]
77 requiredBands = [b for i, b in enumerate(config.bands) if requiredFlag[i]]
79 # process the starColorCuts
80 starColorCutList = []
81 for ccut in config.starColorCuts:
82 parts = ccut.split(',')
83 starColorCutList.append([parts[0], parts[1], float(parts[2]), float(parts[3])])
85 # TODO: Having direct access to the mirror area from the camera would be
86 # useful. See DM-16489.
87 # Mirror area in cm**2
88 mirrorArea = np.pi*(camera.telescopeDiameter*100./2.)**2.
90 # Get approximate average camera gain:
91 gains = [amp.getGain() for detector in camera for amp in detector.getAmplifiers()]
92 cameraGain = float(np.median(gains))
94 if tract is None:
95 outfileBase = config.outfileBase
96 else:
97 outfileBase = '%s-%06d' % (config.outfileBase, tract)
99 # create a configuration dictionary for fgcmFitCycle
100 configDict = {'outfileBase': outfileBase,
101 'logger': log,
102 'exposureFile': None,
103 'obsFile': None,
104 'indexFile': None,
105 'lutFile': None,
106 'mirrorArea': mirrorArea,
107 'cameraGain': cameraGain,
108 'ccdStartIndex': camera[0].getId(),
109 'expField': 'VISIT',
110 'ccdField': 'CCD',
111 'seeingField': 'DELTA_APER',
112 'fwhmField': 'PSFSIGMA',
113 'skyBrightnessField': 'SKYBACKGROUND',
114 'deepFlag': 'DEEPFLAG', # unused
115 'bands': list(config.bands),
116 'fitBands': list(fitBands),
117 'notFitBands': list(notFitBands),
118 'requiredBands': list(requiredBands),
119 'filterToBand': dict(config.filterMap),
120 'logLevel': 'INFO', # FIXME
121 'nCore': config.nCore,
122 'nStarPerRun': config.nStarPerRun,
123 'nExpPerRun': config.nExpPerRun,
124 'reserveFraction': config.reserveFraction,
125 'freezeStdAtmosphere': config.freezeStdAtmosphere,
126 'precomputeSuperStarInitialCycle': config.precomputeSuperStarInitialCycle,
127 'superStarSubCCD': config.superStarSubCcd,
128 'superStarSubCCDChebyshevOrder': config.superStarSubCcdChebyshevOrder,
129 'superStarSubCCDTriangular': config.superStarSubCcdTriangular,
130 'superStarSigmaClip': config.superStarSigmaClip,
131 'ccdGraySubCCD': config.ccdGraySubCcd,
132 'ccdGraySubCCDChebyshevOrder': config.ccdGraySubCcdChebyshevOrder,
133 'ccdGraySubCCDTriangular': config.ccdGraySubCcdTriangular,
134 'cycleNumber': config.cycleNumber,
135 'maxIter': maxIter,
136 'UTBoundary': config.utBoundary,
137 'washMJDs': config.washMjds,
138 'epochMJDs': config.epochMjds,
139 'coatingMJDs': config.coatingMjds,
140 'minObsPerBand': config.minObsPerBand,
141 'latitude': config.latitude,
142 'brightObsGrayMax': config.brightObsGrayMax,
143 'minStarPerCCD': config.minStarPerCcd,
144 'minCCDPerExp': config.minCcdPerExp,
145 'maxCCDGrayErr': config.maxCcdGrayErr,
146 'minStarPerExp': config.minStarPerExp,
147 'minExpPerNight': config.minExpPerNight,
148 'expGrayInitialCut': config.expGrayInitialCut,
149 'expGrayPhotometricCut': np.array(config.expGrayPhotometricCut),
150 'expGrayHighCut': np.array(config.expGrayHighCut),
151 'expGrayRecoverCut': config.expGrayRecoverCut,
152 'expVarGrayPhotometricCut': config.expVarGrayPhotometricCut,
153 'expGrayErrRecoverCut': config.expGrayErrRecoverCut,
154 'refStarSnMin': config.refStarSnMin,
155 'refStarOutlierNSig': config.refStarOutlierNSig,
156 'applyRefStarColorCuts': config.applyRefStarColorCuts,
157 'illegalValue': -9999.0, # internally used by fgcm.
158 'starColorCuts': starColorCutList,
159 'aperCorrFitNBins': config.aperCorrFitNBins,
160 'aperCorrInputSlopes': np.array(config.aperCorrInputSlopes),
161 'sedBoundaryTermDict': config.sedboundaryterms.toDict()['data'],
162 'sedTermDict': config.sedterms.toDict()['data'],
163 'colorSplitIndices': np.array(config.colorSplitIndices),
164 'sigFgcmMaxErr': config.sigFgcmMaxErr,
165 'sigFgcmMaxEGray': list(config.sigFgcmMaxEGray),
166 'ccdGrayMaxStarErr': config.ccdGrayMaxStarErr,
167 'approxThroughput': list(config.approxThroughput),
168 'sigmaCalRange': list(config.sigmaCalRange),
169 'sigmaCalFitPercentile': list(config.sigmaCalFitPercentile),
170 'sigmaCalPlotPercentile': list(config.sigmaCalPlotPercentile),
171 'sigma0Phot': config.sigma0Phot,
172 'mapLongitudeRef': config.mapLongitudeRef,
173 'mapNSide': config.mapNSide,
174 'varNSig': 100.0, # Turn off 'variable star selection' which doesn't work yet
175 'varMinBand': 2,
176 'useRetrievedPwv': False,
177 'useNightlyRetrievedPwv': False,
178 'pwvRetrievalSmoothBlock': 25,
179 'useQuadraticPwv': config.useQuadraticPwv,
180 'useRetrievedTauInit': False,
181 'tauRetrievalMinCCDPerNight': 500,
182 'modelMagErrors': config.modelMagErrors,
183 'instrumentParsPerBand': config.instrumentParsPerBand,
184 'instrumentSlopeMinDeltaT': config.instrumentSlopeMinDeltaT,
185 'fitMirrorChromaticity': config.fitMirrorChromaticity,
186 'useRepeatabilityForExpGrayCuts': list(config.useRepeatabilityForExpGrayCuts),
187 'autoPhotometricCutNSig': config.autoPhotometricCutNSig,
188 'autoHighCutNSig': config.autoHighCutNSig,
189 'printOnly': False,
190 'quietMode': config.quietMode,
191 'outputStars': False,
192 'clobber': True,
193 'useSedLUT': False,
194 'resetParameters': resetFitParameters,
195 'outputFgcmcalZpts': True, # when outputting zpts, use fgcmcal format
196 'outputZeropoints': outputZeropoints}
198 return configDict
201def translateFgcmLut(lutCat, filterMap):
202 """
203 Translate the FGCM look-up-table into an fgcm-compatible object
205 Parameters
206 ----------
207 lutCat: `lsst.afw.table.BaseCatalog`
208 Catalog describing the FGCM look-up table
209 filterMap: `dict`
210 Filter to band mapping
212 Returns
213 -------
214 fgcmLut: `lsst.fgcm.FgcmLut`
215 Lookup table for FGCM
216 lutIndexVals: `numpy.ndarray`
217 Numpy array with LUT index information for FGCM
218 lutStd: `numpy.ndarray`
219 Numpy array with LUT standard throughput values for FGCM
221 Notes
222 -----
223 After running this code, it is wise to `del lutCat` to clear the memory.
224 """
226 # first we need the lutIndexVals
227 # dtype is set for py2/py3/fits/fgcm compatibility
228 lutFilterNames = np.array(lutCat[0]['filterNames'].split(','), dtype='a')
229 lutStdFilterNames = np.array(lutCat[0]['stdFilterNames'].split(','), dtype='a')
231 # Note that any discrepancies between config values will raise relevant
232 # exceptions in the FGCM code.
234 lutIndexVals = np.zeros(1, dtype=[('FILTERNAMES', lutFilterNames.dtype.str,
235 lutFilterNames.size),
236 ('STDFILTERNAMES', lutStdFilterNames.dtype.str,
237 lutStdFilterNames.size),
238 ('PMB', 'f8', lutCat[0]['pmb'].size),
239 ('PMBFACTOR', 'f8', lutCat[0]['pmbFactor'].size),
240 ('PMBELEVATION', 'f8'),
241 ('LAMBDANORM', 'f8'),
242 ('PWV', 'f8', lutCat[0]['pwv'].size),
243 ('O3', 'f8', lutCat[0]['o3'].size),
244 ('TAU', 'f8', lutCat[0]['tau'].size),
245 ('ALPHA', 'f8', lutCat[0]['alpha'].size),
246 ('ZENITH', 'f8', lutCat[0]['zenith'].size),
247 ('NCCD', 'i4')])
249 lutIndexVals['FILTERNAMES'][:] = lutFilterNames
250 lutIndexVals['STDFILTERNAMES'][:] = lutStdFilterNames
251 lutIndexVals['PMB'][:] = lutCat[0]['pmb']
252 lutIndexVals['PMBFACTOR'][:] = lutCat[0]['pmbFactor']
253 lutIndexVals['PMBELEVATION'] = lutCat[0]['pmbElevation']
254 lutIndexVals['LAMBDANORM'] = lutCat[0]['lambdaNorm']
255 lutIndexVals['PWV'][:] = lutCat[0]['pwv']
256 lutIndexVals['O3'][:] = lutCat[0]['o3']
257 lutIndexVals['TAU'][:] = lutCat[0]['tau']
258 lutIndexVals['ALPHA'][:] = lutCat[0]['alpha']
259 lutIndexVals['ZENITH'][:] = lutCat[0]['zenith']
260 lutIndexVals['NCCD'] = lutCat[0]['nCcd']
262 # now we need the Standard Values
263 lutStd = np.zeros(1, dtype=[('PMBSTD', 'f8'),
264 ('PWVSTD', 'f8'),
265 ('O3STD', 'f8'),
266 ('TAUSTD', 'f8'),
267 ('ALPHASTD', 'f8'),
268 ('ZENITHSTD', 'f8'),
269 ('LAMBDARANGE', 'f8', 2),
270 ('LAMBDASTEP', 'f8'),
271 ('LAMBDASTD', 'f8', lutFilterNames.size),
272 ('LAMBDASTDFILTER', 'f8', lutStdFilterNames.size),
273 ('I0STD', 'f8', lutFilterNames.size),
274 ('I1STD', 'f8', lutFilterNames.size),
275 ('I10STD', 'f8', lutFilterNames.size),
276 ('I2STD', 'f8', lutFilterNames.size),
277 ('LAMBDAB', 'f8', lutFilterNames.size),
278 ('ATMLAMBDA', 'f8', lutCat[0]['atmLambda'].size),
279 ('ATMSTDTRANS', 'f8', lutCat[0]['atmStdTrans'].size)])
280 lutStd['PMBSTD'] = lutCat[0]['pmbStd']
281 lutStd['PWVSTD'] = lutCat[0]['pwvStd']
282 lutStd['O3STD'] = lutCat[0]['o3Std']
283 lutStd['TAUSTD'] = lutCat[0]['tauStd']
284 lutStd['ALPHASTD'] = lutCat[0]['alphaStd']
285 lutStd['ZENITHSTD'] = lutCat[0]['zenithStd']
286 lutStd['LAMBDARANGE'][:] = lutCat[0]['lambdaRange'][:]
287 lutStd['LAMBDASTEP'] = lutCat[0]['lambdaStep']
288 lutStd['LAMBDASTD'][:] = lutCat[0]['lambdaStd']
289 lutStd['LAMBDASTDFILTER'][:] = lutCat[0]['lambdaStdFilter']
290 lutStd['I0STD'][:] = lutCat[0]['i0Std']
291 lutStd['I1STD'][:] = lutCat[0]['i1Std']
292 lutStd['I10STD'][:] = lutCat[0]['i10Std']
293 lutStd['I2STD'][:] = lutCat[0]['i2Std']
294 lutStd['LAMBDAB'][:] = lutCat[0]['lambdaB']
295 lutStd['ATMLAMBDA'][:] = lutCat[0]['atmLambda'][:]
296 lutStd['ATMSTDTRANS'][:] = lutCat[0]['atmStdTrans'][:]
298 lutTypes = [row['luttype'] for row in lutCat]
300 # And the flattened look-up-table
301 lutFlat = np.zeros(lutCat[0]['lut'].size, dtype=[('I0', 'f4'),
302 ('I1', 'f4')])
304 lutFlat['I0'][:] = lutCat[lutTypes.index('I0')]['lut'][:]
305 lutFlat['I1'][:] = lutCat[lutTypes.index('I1')]['lut'][:]
307 lutDerivFlat = np.zeros(lutCat[0]['lut'].size, dtype=[('D_LNPWV', 'f4'),
308 ('D_O3', 'f4'),
309 ('D_LNTAU', 'f4'),
310 ('D_ALPHA', 'f4'),
311 ('D_SECZENITH', 'f4'),
312 ('D_LNPWV_I1', 'f4'),
313 ('D_O3_I1', 'f4'),
314 ('D_LNTAU_I1', 'f4'),
315 ('D_ALPHA_I1', 'f4'),
316 ('D_SECZENITH_I1', 'f4')])
318 for name in lutDerivFlat.dtype.names:
319 lutDerivFlat[name][:] = lutCat[lutTypes.index(name)]['lut'][:]
321 # The fgcm.FgcmLUT() class copies all the LUT information into special
322 # shared memory objects that will not blow up the memory usage when used
323 # with python multiprocessing. Once all the numbers are copied, the
324 # references to the temporary objects (lutCat, lutFlat, lutDerivFlat)
325 # will fall out of scope and can be cleaned up by the garbage collector.
326 fgcmLut = fgcm.FgcmLUT(lutIndexVals, lutFlat, lutDerivFlat, lutStd,
327 filterToBand=filterMap)
329 return fgcmLut, lutIndexVals, lutStd
332def translateVisitCatalog(visitCat):
333 """
334 Translate the FGCM visit catalog to an fgcm-compatible object
336 Parameters
337 ----------
338 visitCat: `lsst.afw.table.BaseCatalog`
339 FGCM visitCat from `lsst.fgcmcal.FgcmBuildStarsTask`
341 Returns
342 -------
343 fgcmExpInfo: `numpy.ndarray`
344 Numpy array for visit information for FGCM
346 Notes
347 -----
348 After running this code, it is wise to `del visitCat` to clear the memory.
349 """
351 fgcmExpInfo = np.zeros(len(visitCat), dtype=[('VISIT', 'i8'),
352 ('MJD', 'f8'),
353 ('EXPTIME', 'f8'),
354 ('PSFSIGMA', 'f8'),
355 ('DELTA_APER', 'f8'),
356 ('SKYBACKGROUND', 'f8'),
357 ('DEEPFLAG', 'i2'),
358 ('TELHA', 'f8'),
359 ('TELRA', 'f8'),
360 ('TELDEC', 'f8'),
361 ('TELROT', 'f8'),
362 ('PMB', 'f8'),
363 ('FILTERNAME', 'a10')])
364 fgcmExpInfo['VISIT'][:] = visitCat['visit']
365 fgcmExpInfo['MJD'][:] = visitCat['mjd']
366 fgcmExpInfo['EXPTIME'][:] = visitCat['exptime']
367 fgcmExpInfo['DEEPFLAG'][:] = visitCat['deepFlag']
368 fgcmExpInfo['TELHA'][:] = visitCat['telha']
369 fgcmExpInfo['TELRA'][:] = visitCat['telra']
370 fgcmExpInfo['TELDEC'][:] = visitCat['teldec']
371 fgcmExpInfo['TELROT'][:] = visitCat['telrot']
372 fgcmExpInfo['PMB'][:] = visitCat['pmb']
373 fgcmExpInfo['PSFSIGMA'][:] = visitCat['psfSigma']
374 fgcmExpInfo['DELTA_APER'][:] = visitCat['deltaAper']
375 fgcmExpInfo['SKYBACKGROUND'][:] = visitCat['skyBackground']
376 # Note that we have to go through asAstropy() to get a string
377 # array out of an afwTable. This is faster than a row-by-row loop.
378 fgcmExpInfo['FILTERNAME'][:] = visitCat.asAstropy()['filtername']
380 return fgcmExpInfo
383def computeCcdOffsets(camera, defaultOrientation):
384 """
385 Compute the CCD offsets in ra/dec and x/y space
387 Parameters
388 ----------
389 camera: `lsst.afw.cameraGeom.Camera`
390 defaultOrientation: `float`
391 Default camera orientation (degrees)
393 Returns
394 -------
395 ccdOffsets: `numpy.ndarray`
396 Numpy array with ccd offset information for input to FGCM.
397 Angular units are degrees, and x/y units are pixels.
398 """
399 # TODO: DM-21215 will fully generalize to arbitrary camera orientations
401 # and we need to know the ccd offsets from the camera geometry
402 ccdOffsets = np.zeros(len(camera), dtype=[('CCDNUM', 'i4'),
403 ('DELTA_RA', 'f8'),
404 ('DELTA_DEC', 'f8'),
405 ('RA_SIZE', 'f8'),
406 ('DEC_SIZE', 'f8'),
407 ('X_SIZE', 'i4'),
408 ('Y_SIZE', 'i4')])
410 # Generate fake WCSs centered at 180/0 to avoid the RA=0/360 problem,
411 # since we are looking for relative positions
412 boresight = geom.SpherePoint(180.0*geom.degrees, 0.0*geom.degrees)
414 # TODO: DM-17597 will update testdata_jointcal so that the test data
415 # does not have nan as the boresight angle for HSC data. For the
416 # time being, there is this ungainly hack.
417 if camera.getName() == 'HSC' and np.isnan(defaultOrientation):
418 orientation = 270*geom.degrees
419 else:
420 orientation = defaultOrientation*geom.degrees
421 flipX = False
423 # Create a temporary visitInfo for input to createInitialSkyWcs
424 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight,
425 boresightRotAngle=orientation,
426 rotType=afwImage.visitInfo.RotType.SKY)
428 for i, detector in enumerate(camera):
429 ccdOffsets['CCDNUM'][i] = detector.getId()
431 wcs = createInitialSkyWcs(visitInfo, detector, flipX)
433 detCenter = wcs.pixelToSky(detector.getCenter(afwCameraGeom.PIXELS))
434 ccdOffsets['DELTA_RA'][i] = (detCenter.getRa() - boresight.getRa()).asDegrees()
435 ccdOffsets['DELTA_DEC'][i] = (detCenter.getDec() - boresight.getDec()).asDegrees()
437 bbox = detector.getBBox()
439 detCorner1 = wcs.pixelToSky(geom.Point2D(bbox.getMin()))
440 detCorner2 = wcs.pixelToSky(geom.Point2D(bbox.getMax()))
442 ccdOffsets['RA_SIZE'][i] = np.abs((detCorner2.getRa() - detCorner1.getRa()).asDegrees())
443 ccdOffsets['DEC_SIZE'][i] = np.abs((detCorner2.getDec() - detCorner1.getDec()).asDegrees())
445 ccdOffsets['X_SIZE'][i] = bbox.getMaxX()
446 ccdOffsets['Y_SIZE'][i] = bbox.getMaxY()
448 return ccdOffsets
451def computeReferencePixelScale(camera):
452 """
453 Compute the median pixel scale in the camera
455 Returns
456 -------
457 pixelScale: `float`
458 Average pixel scale (arcsecond) over the camera
459 """
461 boresight = geom.SpherePoint(180.0*geom.degrees, 0.0*geom.degrees)
462 orientation = 0.0*geom.degrees
463 flipX = False
465 # Create a temporary visitInfo for input to createInitialSkyWcs
466 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight,
467 boresightRotAngle=orientation,
468 rotType=afwImage.visitInfo.RotType.SKY)
470 pixelScales = np.zeros(len(camera))
471 for i, detector in enumerate(camera):
472 wcs = createInitialSkyWcs(visitInfo, detector, flipX)
473 pixelScales[i] = wcs.getPixelScale().asArcseconds()
475 ok, = np.where(pixelScales > 0.0)
476 return np.median(pixelScales[ok])
479def computeApproxPixelAreaFields(camera):
480 """
481 Compute the approximate pixel area bounded fields from the camera
482 geometry.
484 Parameters
485 ----------
486 camera: `lsst.afw.cameraGeom.Camera`
488 Returns
489 -------
490 approxPixelAreaFields: `dict`
491 Dictionary of approximate area fields, keyed with detector ID
492 """
494 areaScaling = 1. / computeReferencePixelScale(camera)**2.
496 # Generate fake WCSs centered at 180/0 to avoid the RA=0/360 problem,
497 # since we are looking for relative scales
498 boresight = geom.SpherePoint(180.0*geom.degrees, 0.0*geom.degrees)
500 flipX = False
501 # Create a temporary visitInfo for input to createInitialSkyWcs
502 # The orientation does not matter for the area computation
503 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight,
504 boresightRotAngle=0.0*geom.degrees,
505 rotType=afwImage.visitInfo.RotType.SKY)
507 approxPixelAreaFields = {}
509 for i, detector in enumerate(camera):
510 key = detector.getId()
512 wcs = createInitialSkyWcs(visitInfo, detector, flipX)
513 bbox = detector.getBBox()
515 areaField = afwMath.PixelAreaBoundedField(bbox, wcs,
516 unit=geom.arcseconds, scaling=areaScaling)
517 approxAreaField = afwMath.ChebyshevBoundedField.approximate(areaField)
519 approxPixelAreaFields[key] = approxAreaField
521 return approxPixelAreaFields
524def makeZptSchema(superStarChebyshevSize, zptChebyshevSize):
525 """
526 Make the zeropoint schema
528 Parameters
529 ----------
530 superStarChebyshevSize: `int`
531 Length of the superstar chebyshev array
532 zptChebyshevSize: `int`
533 Length of the zeropoint chebyshev array
535 Returns
536 -------
537 zptSchema: `lsst.afw.table.schema`
538 """
540 zptSchema = afwTable.Schema()
542 zptSchema.addField('visit', type=np.int32, doc='Visit number')
543 zptSchema.addField('ccd', type=np.int32, doc='CCD number')
544 zptSchema.addField('fgcmFlag', type=np.int32, doc=('FGCM flag value: '
545 '1: Photometric, used in fit; '
546 '2: Photometric, not used in fit; '
547 '4: Non-photometric, on partly photometric night; '
548 '8: Non-photometric, on non-photometric night; '
549 '16: No zeropoint could be determined; '
550 '32: Too few stars for reliable gray computation'))
551 zptSchema.addField('fgcmZpt', type=np.float64, doc='FGCM zeropoint (center of CCD)')
552 zptSchema.addField('fgcmZptErr', type=np.float64,
553 doc='Error on zeropoint, estimated from repeatability + number of obs')
554 zptSchema.addField('fgcmfZptChebXyMax', type='ArrayD', size=2,
555 doc='maximum x/maximum y to scale to apply chebyshev parameters')
556 zptSchema.addField('fgcmfZptCheb', type='ArrayD',
557 size=zptChebyshevSize,
558 doc='Chebyshev parameters (flattened) for zeropoint')
559 zptSchema.addField('fgcmfZptSstarCheb', type='ArrayD',
560 size=superStarChebyshevSize,
561 doc='Chebyshev parameters (flattened) for superStarFlat')
562 zptSchema.addField('fgcmI0', type=np.float64, doc='Integral of the passband')
563 zptSchema.addField('fgcmI10', type=np.float64, doc='Normalized chromatic integral')
564 zptSchema.addField('fgcmR0', type=np.float64,
565 doc='Retrieved i0 integral, estimated from stars (only for flag 1)')
566 zptSchema.addField('fgcmR10', type=np.float64,
567 doc='Retrieved i10 integral, estimated from stars (only for flag 1)')
568 zptSchema.addField('fgcmGry', type=np.float64,
569 doc='Estimated gray extinction relative to atmospheric solution; '
570 'only for flag <= 4')
571 zptSchema.addField('fgcmZptVar', type=np.float64, doc='Variance of zeropoint over ccd')
572 zptSchema.addField('fgcmTilings', type=np.float64,
573 doc='Number of photometric tilings used for solution for ccd')
574 zptSchema.addField('fgcmFpGry', type=np.float64,
575 doc='Average gray extinction over the full focal plane '
576 '(same for all ccds in a visit)')
577 zptSchema.addField('fgcmFpGryBlue', type=np.float64,
578 doc='Average gray extinction over the full focal plane '
579 'for 25% bluest stars')
580 zptSchema.addField('fgcmFpGryBlueErr', type=np.float64,
581 doc='Error on Average gray extinction over the full focal plane '
582 'for 25% bluest stars')
583 zptSchema.addField('fgcmFpGryRed', type=np.float64,
584 doc='Average gray extinction over the full focal plane '
585 'for 25% reddest stars')
586 zptSchema.addField('fgcmFpGryRedErr', type=np.float64,
587 doc='Error on Average gray extinction over the full focal plane '
588 'for 25% reddest stars')
589 zptSchema.addField('fgcmFpVar', type=np.float64,
590 doc='Variance of gray extinction over the full focal plane '
591 '(same for all ccds in a visit)')
592 zptSchema.addField('fgcmDust', type=np.float64,
593 doc='Gray dust extinction from the primary/corrector'
594 'at the time of the exposure')
595 zptSchema.addField('fgcmFlat', type=np.float64, doc='Superstarflat illumination correction')
596 zptSchema.addField('fgcmAperCorr', type=np.float64, doc='Aperture correction estimated by fgcm')
597 zptSchema.addField('exptime', type=np.float32, doc='Exposure time')
598 zptSchema.addField('filtername', type=str, size=10, doc='Filter name')
600 return zptSchema
603def makeZptCat(zptSchema, zpStruct):
604 """
605 Make the zeropoint catalog for persistence
607 Parameters
608 ----------
609 zptSchema: `lsst.afw.table.Schema`
610 Zeropoint catalog schema
611 zpStruct: `numpy.ndarray`
612 Zeropoint structure from fgcm
614 Returns
615 -------
616 zptCat: `afwTable.BaseCatalog`
617 Zeropoint catalog for persistence
618 """
620 zptCat = afwTable.BaseCatalog(zptSchema)
621 zptCat.reserve(zpStruct.size)
623 for filterName in zpStruct['FILTERNAME']:
624 rec = zptCat.addNew()
625 rec['filtername'] = filterName.decode('utf-8')
627 zptCat['visit'][:] = zpStruct['VISIT']
628 zptCat['ccd'][:] = zpStruct['CCD']
629 zptCat['fgcmFlag'][:] = zpStruct['FGCM_FLAG']
630 zptCat['fgcmZpt'][:] = zpStruct['FGCM_ZPT']
631 zptCat['fgcmZptErr'][:] = zpStruct['FGCM_ZPTERR']
632 zptCat['fgcmfZptChebXyMax'][:, :] = zpStruct['FGCM_FZPT_XYMAX']
633 zptCat['fgcmfZptCheb'][:, :] = zpStruct['FGCM_FZPT_CHEB']
634 zptCat['fgcmfZptSstarCheb'][:, :] = zpStruct['FGCM_FZPT_SSTAR_CHEB']
635 zptCat['fgcmI0'][:] = zpStruct['FGCM_I0']
636 zptCat['fgcmI10'][:] = zpStruct['FGCM_I10']
637 zptCat['fgcmR0'][:] = zpStruct['FGCM_R0']
638 zptCat['fgcmR10'][:] = zpStruct['FGCM_R10']
639 zptCat['fgcmGry'][:] = zpStruct['FGCM_GRY']
640 zptCat['fgcmZptVar'][:] = zpStruct['FGCM_ZPTVAR']
641 zptCat['fgcmTilings'][:] = zpStruct['FGCM_TILINGS']
642 zptCat['fgcmFpGry'][:] = zpStruct['FGCM_FPGRY']
643 zptCat['fgcmFpGryBlue'][:] = zpStruct['FGCM_FPGRY_CSPLIT'][:, 0]
644 zptCat['fgcmFpGryBlueErr'][:] = zpStruct['FGCM_FPGRY_CSPLITERR'][:, 0]
645 zptCat['fgcmFpGryRed'][:] = zpStruct['FGCM_FPGRY_CSPLIT'][:, 2]
646 zptCat['fgcmFpGryRedErr'][:] = zpStruct['FGCM_FPGRY_CSPLITERR'][:, 2]
647 zptCat['fgcmFpVar'][:] = zpStruct['FGCM_FPVAR']
648 zptCat['fgcmDust'][:] = zpStruct['FGCM_DUST']
649 zptCat['fgcmFlat'][:] = zpStruct['FGCM_FLAT']
650 zptCat['fgcmAperCorr'][:] = zpStruct['FGCM_APERCORR']
651 zptCat['exptime'][:] = zpStruct['EXPTIME']
653 return zptCat
656def makeAtmSchema():
657 """
658 Make the atmosphere schema
660 Returns
661 -------
662 atmSchema: `lsst.afw.table.Schema`
663 """
665 atmSchema = afwTable.Schema()
667 atmSchema.addField('visit', type=np.int32, doc='Visit number')
668 atmSchema.addField('pmb', type=np.float64, doc='Barometric pressure (mb)')
669 atmSchema.addField('pwv', type=np.float64, doc='Water vapor (mm)')
670 atmSchema.addField('tau', type=np.float64, doc='Aerosol optical depth')
671 atmSchema.addField('alpha', type=np.float64, doc='Aerosol slope')
672 atmSchema.addField('o3', type=np.float64, doc='Ozone (dobson)')
673 atmSchema.addField('secZenith', type=np.float64, doc='Secant(zenith) (~ airmass)')
674 atmSchema.addField('cTrans', type=np.float64, doc='Transmission correction factor')
675 atmSchema.addField('lamStd', type=np.float64, doc='Wavelength for transmission correction')
677 return atmSchema
680def makeAtmCat(atmSchema, atmStruct):
681 """
682 Make the atmosphere catalog for persistence
684 Parameters
685 ----------
686 atmSchema: `lsst.afw.table.Schema`
687 Atmosphere catalog schema
688 atmStruct: `numpy.ndarray`
689 Atmosphere structure from fgcm
691 Returns
692 -------
693 atmCat: `lsst.afw.table.BaseCatalog`
694 Atmosphere catalog for persistence
695 """
697 atmCat = afwTable.BaseCatalog(atmSchema)
698 atmCat.reserve(atmStruct.size)
699 for i in range(atmStruct.size):
700 atmCat.addNew()
702 atmCat['visit'][:] = atmStruct['VISIT']
703 atmCat['pmb'][:] = atmStruct['PMB']
704 atmCat['pwv'][:] = atmStruct['PWV']
705 atmCat['tau'][:] = atmStruct['TAU']
706 atmCat['alpha'][:] = atmStruct['ALPHA']
707 atmCat['o3'][:] = atmStruct['O3']
708 atmCat['secZenith'][:] = atmStruct['SECZENITH']
709 atmCat['cTrans'][:] = atmStruct['CTRANS']
710 atmCat['lamStd'][:] = atmStruct['LAMSTD']
712 return atmCat
715def makeStdSchema(nBands):
716 """
717 Make the standard star schema
719 Parameters
720 ----------
721 nBands: `int`
722 Number of bands in standard star catalog
724 Returns
725 -------
726 stdSchema: `lsst.afw.table.Schema`
727 """
729 stdSchema = afwTable.SimpleTable.makeMinimalSchema()
730 stdSchema.addField('ngood', type='ArrayI', doc='Number of good observations',
731 size=nBands)
732 stdSchema.addField('ntotal', type='ArrayI', doc='Number of total observations',
733 size=nBands)
734 stdSchema.addField('mag_std_noabs', type='ArrayF',
735 doc='Standard magnitude (no absolute calibration)',
736 size=nBands)
737 stdSchema.addField('magErr_std', type='ArrayF',
738 doc='Standard magnitude error',
739 size=nBands)
740 stdSchema.addField('npsfcand', type='ArrayI',
741 doc='Number of observations flagged as psf candidates',
742 size=nBands)
744 return stdSchema
747def makeStdCat(stdSchema, stdStruct, goodBands):
748 """
749 Make the standard star catalog for persistence
751 Parameters
752 ----------
753 stdSchema: `lsst.afw.table.Schema`
754 Standard star catalog schema
755 stdStruct: `numpy.ndarray`
756 Standard star structure in FGCM format
757 goodBands: `list`
758 List of good band names used in stdStruct
760 Returns
761 -------
762 stdCat: `lsst.afw.table.BaseCatalog`
763 Standard star catalog for persistence
764 """
766 stdCat = afwTable.SimpleCatalog(stdSchema)
768 stdCat.reserve(stdStruct.size)
769 for i in range(stdStruct.size):
770 stdCat.addNew()
772 stdCat['id'][:] = stdStruct['FGCM_ID']
773 stdCat['coord_ra'][:] = stdStruct['RA'] * geom.degrees
774 stdCat['coord_dec'][:] = stdStruct['DEC'] * geom.degrees
775 stdCat['ngood'][:, :] = stdStruct['NGOOD'][:, :]
776 stdCat['ntotal'][:, :] = stdStruct['NTOTAL'][:, :]
777 stdCat['mag_std_noabs'][:, :] = stdStruct['MAG_STD'][:, :]
778 stdCat['magErr_std'][:, :] = stdStruct['MAGERR_STD'][:, :]
779 stdCat['npsfcand'][:, :] = stdStruct['NPSFCAND'][:, :]
781 md = PropertyList()
782 md.set("BANDS", list(goodBands))
783 stdCat.setMetadata(md)
785 return stdCat
788def computeApertureRadius(schema, fluxField):
789 """
790 Compute the radius associated with a CircularApertureFlux field or
791 associated slot.
793 Parameters
794 ----------
795 schema : `lsst.afw.table.schema`
796 fluxField : `str`
797 CircularApertureFlux field or associated slot.
799 Returns
800 -------
801 apertureRadius: `float`
802 Radius of the aperture field, in pixels.
804 Raises
805 ------
806 RuntimeError: Raised if flux field is not a CircularApertureFlux
807 or associated slot.
808 """
809 fluxFieldName = schema[fluxField].asField().getName()
811 m = re.search(r'CircularApertureFlux_(\d+)_(\d+)_', fluxFieldName)
813 if m is None:
814 raise RuntimeError("Flux field %s does not correspond to a circular aperture"
815 % (fluxField))
817 apertureRadius = float(m.groups()[0]) + float(m.groups()[1])/10.
819 return apertureRadius
822def extractReferenceMags(refStars, bands, filterMap):
823 """
824 Extract reference magnitudes from refStars for given bands and
825 associated filterMap.
827 Parameters
828 ----------
829 refStars : `lsst.afw.table.BaseCatalog`
830 FGCM reference star catalog
831 bands : `list`
832 List of bands for calibration
833 filterMap: `dict`
834 FGCM mapping of filter to band
836 Returns
837 -------
838 refMag : `np.ndarray`
839 nstar x nband array of reference magnitudes
840 refMagErr : `np.ndarray`
841 nstar x nband array of reference magnitude errors
842 """
843 # After DM-23331 fgcm reference catalogs have FILTERNAMES to prevent
844 # against index errors and allow more flexibility in fitting after
845 # the build stars step.
847 md = refStars.getMetadata()
848 if 'FILTERNAMES' in md:
849 filternames = md.getArray('FILTERNAMES')
851 # The reference catalog that fgcm wants has one entry per band
852 # in the config file
853 refMag = np.zeros((len(refStars), len(bands)),
854 dtype=refStars['refMag'].dtype) + 99.0
855 refMagErr = np.zeros_like(refMag) + 99.0
856 for i, filtername in enumerate(filternames):
857 # We are allowed to run the fit configured so that we do not
858 # use every column in the reference catalog.
859 try:
860 band = filterMap[filtername]
861 except KeyError:
862 continue
863 try:
864 ind = bands.index(band)
865 except ValueError:
866 continue
868 refMag[:, ind] = refStars['refMag'][:, i]
869 refMagErr[:, ind] = refStars['refMagErr'][:, i]
871 else:
872 # Continue to use old catalogs as before.
873 refMag = refStars['refMag'][:, :]
874 refMagErr = refStars['refMagErr'][:, :]
876 return refMag, refMagErr