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