Hide keyboard shortcuts

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# 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 re 

29 

30from lsst.daf.base import PropertyList 

31import lsst.afw.cameraGeom as afwCameraGeom 

32import lsst.afw.table as afwTable 

33import lsst.afw.image as afwImage 

34import lsst.afw.math as afwMath 

35import lsst.geom as geom 

36from lsst.obs.base import createInitialSkyWcs 

37 

38import fgcm 

39 

40 

41def makeConfigDict(config, log, camera, maxIter, 

42 resetFitParameters, outputZeropoints, tract=None): 

43 """ 

44 Make the FGCM fit cycle configuration dict 

45 

46 Parameters 

47 ---------- 

48 config: `lsst.fgcmcal.FgcmFitCycleConfig` 

49 Configuration object 

50 log: `lsst.log.Log` 

51 LSST log object 

52 camera: `lsst.afw.cameraGeom.Camera` 

53 Camera from the butler 

54 maxIter: `int` 

55 Maximum number of iterations 

56 resetFitParameters: `bool` 

57 Reset fit parameters before fitting? 

58 outputZeropoints: `bool` 

59 Compute zeropoints for output? 

60 tract: `int`, optional 

61 Tract number for extending the output file name for debugging. 

62 Default is None. 

63 

64 Returns 

65 ------- 

66 configDict: `dict` 

67 Configuration dictionary for fgcm 

68 """ 

69 # Extract the bands that are _not_ being fit for fgcm configuration 

70 notFitBands = [b for b in config.bands if b not in config.fitBands] 

71 

72 # process the starColorCuts 

73 starColorCutList = [] 

74 for ccut in config.starColorCuts: 

75 parts = ccut.split(',') 

76 starColorCutList.append([parts[0], parts[1], float(parts[2]), float(parts[3])]) 

77 

78 # TODO: Having direct access to the mirror area from the camera would be 

79 # useful. See DM-16489. 

80 # Mirror area in cm**2 

81 mirrorArea = np.pi*(camera.telescopeDiameter*100./2.)**2. 

82 

83 # Get approximate average camera gain: 

84 gains = [amp.getGain() for detector in camera for amp in detector.getAmplifiers()] 

85 cameraGain = float(np.median(gains)) 

86 

87 if tract is None: 

88 outfileBase = config.outfileBase 

89 else: 

90 outfileBase = '%s-%06d' % (config.outfileBase, tract) 

91 

92 # create a configuration dictionary for fgcmFitCycle 

93 configDict = {'outfileBase': outfileBase, 

94 'logger': log, 

95 'exposureFile': None, 

96 'obsFile': None, 

97 'indexFile': None, 

98 'lutFile': None, 

99 'mirrorArea': mirrorArea, 

100 'cameraGain': cameraGain, 

101 'ccdStartIndex': camera[0].getId(), 

102 'expField': 'VISIT', 

103 'ccdField': 'CCD', 

104 'seeingField': 'DELTA_APER', 

105 'fwhmField': 'PSFSIGMA', 

106 'skyBrightnessField': 'SKYBACKGROUND', 

107 'deepFlag': 'DEEPFLAG', # unused 

108 'bands': list(config.bands), 

109 'fitBands': list(config.fitBands), 

110 'notFitBands': notFitBands, 

111 'requiredBands': list(config.requiredBands), 

112 'filterToBand': dict(config.filterMap), 

113 'logLevel': 'INFO', # FIXME 

114 'nCore': config.nCore, 

115 'nStarPerRun': config.nStarPerRun, 

116 'nExpPerRun': config.nExpPerRun, 

117 'reserveFraction': config.reserveFraction, 

118 'freezeStdAtmosphere': config.freezeStdAtmosphere, 

119 'precomputeSuperStarInitialCycle': config.precomputeSuperStarInitialCycle, 

120 'superStarSubCCDDict': dict(config.superStarSubCcdDict), 

121 'superStarSubCCDChebyshevOrder': config.superStarSubCcdChebyshevOrder, 

122 'superStarSubCCDTriangular': config.superStarSubCcdTriangular, 

123 'superStarSigmaClip': config.superStarSigmaClip, 

124 'focalPlaneSigmaClip': config.focalPlaneSigmaClip, 

125 'ccdGraySubCCDDict': dict(config.ccdGraySubCcdDict), 

126 'ccdGraySubCCDChebyshevOrder': config.ccdGraySubCcdChebyshevOrder, 

127 'ccdGraySubCCDTriangular': config.ccdGraySubCcdTriangular, 

128 'ccdGrayFocalPlaneDict': dict(config.ccdGrayFocalPlaneDict), 

129 'ccdGrayFocalPlaneChebyshevOrder': config.ccdGrayFocalPlaneChebyshevOrder, 

130 'ccdGrayFocalPlaneFitMinCcd': config.ccdGrayFocalPlaneFitMinCcd, 

131 'cycleNumber': config.cycleNumber, 

132 'maxIter': maxIter, 

133 'deltaMagBkgOffsetPercentile': config.deltaMagBkgOffsetPercentile, 

134 'deltaMagBkgPerCcd': config.deltaMagBkgPerCcd, 

135 'UTBoundary': config.utBoundary, 

136 'washMJDs': config.washMjds, 

137 'epochMJDs': config.epochMjds, 

138 'coatingMJDs': config.coatingMjds, 

139 'minObsPerBand': config.minObsPerBand, 

140 'latitude': config.latitude, 

141 'brightObsGrayMax': config.brightObsGrayMax, 

142 'minStarPerCCD': config.minStarPerCcd, 

143 'minCCDPerExp': config.minCcdPerExp, 

144 'maxCCDGrayErr': config.maxCcdGrayErr, 

145 'minStarPerExp': config.minStarPerExp, 

146 'minExpPerNight': config.minExpPerNight, 

147 'expGrayInitialCut': config.expGrayInitialCut, 

148 'expGrayPhotometricCutDict': dict(config.expGrayPhotometricCutDict), 

149 'expGrayHighCutDict': dict(config.expGrayHighCutDict), 

150 'expGrayRecoverCut': config.expGrayRecoverCut, 

151 'expVarGrayPhotometricCutDict': dict(config.expVarGrayPhotometricCutDict), 

152 'expGrayErrRecoverCut': config.expGrayErrRecoverCut, 

153 'refStarSnMin': config.refStarSnMin, 

154 'refStarOutlierNSig': config.refStarOutlierNSig, 

155 'applyRefStarColorCuts': config.applyRefStarColorCuts, 

156 'illegalValue': -9999.0, # internally used by fgcm. 

157 'starColorCuts': starColorCutList, 

158 'aperCorrFitNBins': config.aperCorrFitNBins, 

159 'aperCorrInputSlopeDict': dict(config.aperCorrInputSlopeDict), 

160 'sedBoundaryTermDict': config.sedboundaryterms.toDict()['data'], 

161 'sedTermDict': config.sedterms.toDict()['data'], 

162 'colorSplitBands': list(config.colorSplitBands), 

163 'sigFgcmMaxErr': config.sigFgcmMaxErr, 

164 'sigFgcmMaxEGrayDict': dict(config.sigFgcmMaxEGrayDict), 

165 'ccdGrayMaxStarErr': config.ccdGrayMaxStarErr, 

166 'approxThroughputDict': dict(config.approxThroughputDict), 

167 'sigmaCalRange': list(config.sigmaCalRange), 

168 'sigmaCalFitPercentile': list(config.sigmaCalFitPercentile), 

169 'sigmaCalPlotPercentile': list(config.sigmaCalPlotPercentile), 

170 'sigma0Phot': config.sigma0Phot, 

171 'mapLongitudeRef': config.mapLongitudeRef, 

172 'mapNSide': config.mapNSide, 

173 'varNSig': 100.0, # Turn off 'variable star selection' which doesn't work yet 

174 'varMinBand': 2, 

175 'useRetrievedPwv': False, 

176 'useNightlyRetrievedPwv': False, 

177 'pwvRetrievalSmoothBlock': 25, 

178 'useQuadraticPwv': config.useQuadraticPwv, 

179 'useRetrievedTauInit': False, 

180 'tauRetrievalMinCCDPerNight': 500, 

181 'modelMagErrors': config.modelMagErrors, 

182 'instrumentParsPerBand': config.instrumentParsPerBand, 

183 'instrumentSlopeMinDeltaT': config.instrumentSlopeMinDeltaT, 

184 'fitMirrorChromaticity': config.fitMirrorChromaticity, 

185 'useRepeatabilityForExpGrayCutsDict': dict(config.useRepeatabilityForExpGrayCutsDict), 

186 'autoPhotometricCutNSig': config.autoPhotometricCutNSig, 

187 'autoHighCutNSig': config.autoHighCutNSig, 

188 'printOnly': False, 

189 'quietMode': config.quietMode, 

190 'randomSeed': config.randomSeed, 

191 'outputStars': False, 

192 'clobber': True, 

193 'useSedLUT': False, 

194 'resetParameters': resetFitParameters, 

195 'outputFgcmcalZpts': True, # when outputting zpts, use fgcmcal format 

196 'outputZeropoints': outputZeropoints} 

197 

198 return configDict 

199 

200 

201def translateFgcmLut(lutCat, filterMap): 

202 """ 

203 Translate the FGCM look-up-table into an fgcm-compatible object 

204 

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 

211 

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 

220 

221 Notes 

222 ----- 

223 After running this code, it is wise to `del lutCat` to clear the memory. 

224 """ 

225 

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') 

230 

231 # Note that any discrepancies between config values will raise relevant 

232 # exceptions in the FGCM code. 

233 

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')]) 

248 

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'] 

261 

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'][:] 

297 

298 lutTypes = [row['luttype'] for row in lutCat] 

299 

300 # And the flattened look-up-table 

301 lutFlat = np.zeros(lutCat[0]['lut'].size, dtype=[('I0', 'f4'), 

302 ('I1', 'f4')]) 

303 

304 lutFlat['I0'][:] = lutCat[lutTypes.index('I0')]['lut'][:] 

305 lutFlat['I1'][:] = lutCat[lutTypes.index('I1')]['lut'][:] 

306 

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')]) 

317 

318 for name in lutDerivFlat.dtype.names: 

319 lutDerivFlat[name][:] = lutCat[lutTypes.index(name)]['lut'][:] 

320 

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) 

328 

329 return fgcmLut, lutIndexVals, lutStd 

330 

331 

332def translateVisitCatalog(visitCat): 

333 """ 

334 Translate the FGCM visit catalog to an fgcm-compatible object 

335 

336 Parameters 

337 ---------- 

338 visitCat: `lsst.afw.table.BaseCatalog` 

339 FGCM visitCat from `lsst.fgcmcal.FgcmBuildStarsTask` 

340 

341 Returns 

342 ------- 

343 fgcmExpInfo: `numpy.ndarray` 

344 Numpy array for visit information for FGCM 

345 

346 Notes 

347 ----- 

348 After running this code, it is wise to `del visitCat` to clear the memory. 

349 """ 

350 

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'] 

379 

380 return fgcmExpInfo 

381 

382 

383def computeCcdOffsets(camera, defaultOrientation): 

384 """ 

385 Compute the CCD offsets in ra/dec and x/y space 

386 

387 Parameters 

388 ---------- 

389 camera: `lsst.afw.cameraGeom.Camera` 

390 defaultOrientation: `float` 

391 Default camera orientation (degrees) 

392 

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 

400 

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')]) 

409 

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) 

413 

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 

422 

423 # Create a temporary visitInfo for input to createInitialSkyWcs 

424 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight, 

425 boresightRotAngle=orientation, 

426 rotType=afwImage.visitInfo.RotType.SKY) 

427 

428 for i, detector in enumerate(camera): 

429 ccdOffsets['CCDNUM'][i] = detector.getId() 

430 

431 wcs = createInitialSkyWcs(visitInfo, detector, flipX) 

432 

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() 

436 

437 bbox = detector.getBBox() 

438 

439 detCorner1 = wcs.pixelToSky(geom.Point2D(bbox.getMin())) 

440 detCorner2 = wcs.pixelToSky(geom.Point2D(bbox.getMax())) 

441 

442 ccdOffsets['RA_SIZE'][i] = np.abs((detCorner2.getRa() - detCorner1.getRa()).asDegrees()) 

443 ccdOffsets['DEC_SIZE'][i] = np.abs((detCorner2.getDec() - detCorner1.getDec()).asDegrees()) 

444 

445 ccdOffsets['X_SIZE'][i] = bbox.getMaxX() 

446 ccdOffsets['Y_SIZE'][i] = bbox.getMaxY() 

447 

448 return ccdOffsets 

449 

450 

451def computeReferencePixelScale(camera): 

452 """ 

453 Compute the median pixel scale in the camera 

454 

455 Returns 

456 ------- 

457 pixelScale: `float` 

458 Average pixel scale (arcsecond) over the camera 

459 """ 

460 

461 boresight = geom.SpherePoint(180.0*geom.degrees, 0.0*geom.degrees) 

462 orientation = 0.0*geom.degrees 

463 flipX = False 

464 

465 # Create a temporary visitInfo for input to createInitialSkyWcs 

466 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight, 

467 boresightRotAngle=orientation, 

468 rotType=afwImage.visitInfo.RotType.SKY) 

469 

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() 

474 

475 ok, = np.where(pixelScales > 0.0) 

476 return np.median(pixelScales[ok]) 

477 

478 

479def computeApproxPixelAreaFields(camera): 

480 """ 

481 Compute the approximate pixel area bounded fields from the camera 

482 geometry. 

483 

484 Parameters 

485 ---------- 

486 camera: `lsst.afw.cameraGeom.Camera` 

487 

488 Returns 

489 ------- 

490 approxPixelAreaFields: `dict` 

491 Dictionary of approximate area fields, keyed with detector ID 

492 """ 

493 

494 areaScaling = 1. / computeReferencePixelScale(camera)**2. 

495 

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) 

499 

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) 

506 

507 approxPixelAreaFields = {} 

508 

509 for i, detector in enumerate(camera): 

510 key = detector.getId() 

511 

512 wcs = createInitialSkyWcs(visitInfo, detector, flipX) 

513 bbox = detector.getBBox() 

514 

515 areaField = afwMath.PixelAreaBoundedField(bbox, wcs, 

516 unit=geom.arcseconds, scaling=areaScaling) 

517 approxAreaField = afwMath.ChebyshevBoundedField.approximate(areaField) 

518 

519 approxPixelAreaFields[key] = approxAreaField 

520 

521 return approxPixelAreaFields 

522 

523 

524def makeZptSchema(superStarChebyshevSize, zptChebyshevSize): 

525 """ 

526 Make the zeropoint schema 

527 

528 Parameters 

529 ---------- 

530 superStarChebyshevSize: `int` 

531 Length of the superstar chebyshev array 

532 zptChebyshevSize: `int` 

533 Length of the zeropoint chebyshev array 

534 

535 Returns 

536 ------- 

537 zptSchema: `lsst.afw.table.schema` 

538 """ 

539 

540 zptSchema = afwTable.Schema() 

541 

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 fgcmFlag <= 4 (see fgcmFlag) ') 

571 zptSchema.addField('fgcmDeltaChrom', type=np.float64, 

572 doc='Mean chromatic correction for stars in this ccd; ' 

573 'only for fgcmFlag <= 4 (see fgcmFlag)') 

574 zptSchema.addField('fgcmZptVar', type=np.float64, doc='Variance of zeropoint over ccd') 

575 zptSchema.addField('fgcmTilings', type=np.float64, 

576 doc='Number of photometric tilings used for solution for ccd') 

577 zptSchema.addField('fgcmFpGry', type=np.float64, 

578 doc='Average gray extinction over the full focal plane ' 

579 '(same for all ccds in a visit)') 

580 zptSchema.addField('fgcmFpGryBlue', type=np.float64, 

581 doc='Average gray extinction over the full focal plane ' 

582 'for 25% bluest stars') 

583 zptSchema.addField('fgcmFpGryBlueErr', type=np.float64, 

584 doc='Error on Average gray extinction over the full focal plane ' 

585 'for 25% bluest stars') 

586 zptSchema.addField('fgcmFpGryRed', type=np.float64, 

587 doc='Average gray extinction over the full focal plane ' 

588 'for 25% reddest stars') 

589 zptSchema.addField('fgcmFpGryRedErr', type=np.float64, 

590 doc='Error on Average gray extinction over the full focal plane ' 

591 'for 25% reddest stars') 

592 zptSchema.addField('fgcmFpVar', type=np.float64, 

593 doc='Variance of gray extinction over the full focal plane ' 

594 '(same for all ccds in a visit)') 

595 zptSchema.addField('fgcmDust', type=np.float64, 

596 doc='Gray dust extinction from the primary/corrector' 

597 'at the time of the exposure') 

598 zptSchema.addField('fgcmFlat', type=np.float64, doc='Superstarflat illumination correction') 

599 zptSchema.addField('fgcmAperCorr', type=np.float64, doc='Aperture correction estimated by fgcm') 

600 zptSchema.addField('fgcmDeltaMagBkg', type=np.float64, 

601 doc=('Local background correction from brightest percentile ' 

602 '(value set by deltaMagBkgOffsetPercentile) calibration ' 

603 'stars.')) 

604 zptSchema.addField('exptime', type=np.float32, doc='Exposure time') 

605 zptSchema.addField('filtername', type=str, size=10, doc='Filter name') 

606 

607 return zptSchema 

608 

609 

610def makeZptCat(zptSchema, zpStruct): 

611 """ 

612 Make the zeropoint catalog for persistence 

613 

614 Parameters 

615 ---------- 

616 zptSchema: `lsst.afw.table.Schema` 

617 Zeropoint catalog schema 

618 zpStruct: `numpy.ndarray` 

619 Zeropoint structure from fgcm 

620 

621 Returns 

622 ------- 

623 zptCat: `afwTable.BaseCatalog` 

624 Zeropoint catalog for persistence 

625 """ 

626 

627 zptCat = afwTable.BaseCatalog(zptSchema) 

628 zptCat.reserve(zpStruct.size) 

629 

630 for filterName in zpStruct['FILTERNAME']: 

631 rec = zptCat.addNew() 

632 rec['filtername'] = filterName.decode('utf-8') 

633 

634 zptCat['visit'][:] = zpStruct['VISIT'] 

635 zptCat['ccd'][:] = zpStruct['CCD'] 

636 zptCat['fgcmFlag'][:] = zpStruct['FGCM_FLAG'] 

637 zptCat['fgcmZpt'][:] = zpStruct['FGCM_ZPT'] 

638 zptCat['fgcmZptErr'][:] = zpStruct['FGCM_ZPTERR'] 

639 zptCat['fgcmfZptChebXyMax'][:, :] = zpStruct['FGCM_FZPT_XYMAX'] 

640 zptCat['fgcmfZptCheb'][:, :] = zpStruct['FGCM_FZPT_CHEB'] 

641 zptCat['fgcmfZptSstarCheb'][:, :] = zpStruct['FGCM_FZPT_SSTAR_CHEB'] 

642 zptCat['fgcmI0'][:] = zpStruct['FGCM_I0'] 

643 zptCat['fgcmI10'][:] = zpStruct['FGCM_I10'] 

644 zptCat['fgcmR0'][:] = zpStruct['FGCM_R0'] 

645 zptCat['fgcmR10'][:] = zpStruct['FGCM_R10'] 

646 zptCat['fgcmGry'][:] = zpStruct['FGCM_GRY'] 

647 zptCat['fgcmDeltaChrom'][:] = zpStruct['FGCM_DELTACHROM'] 

648 zptCat['fgcmZptVar'][:] = zpStruct['FGCM_ZPTVAR'] 

649 zptCat['fgcmTilings'][:] = zpStruct['FGCM_TILINGS'] 

650 zptCat['fgcmFpGry'][:] = zpStruct['FGCM_FPGRY'] 

651 zptCat['fgcmFpGryBlue'][:] = zpStruct['FGCM_FPGRY_CSPLIT'][:, 0] 

652 zptCat['fgcmFpGryBlueErr'][:] = zpStruct['FGCM_FPGRY_CSPLITERR'][:, 0] 

653 zptCat['fgcmFpGryRed'][:] = zpStruct['FGCM_FPGRY_CSPLIT'][:, 2] 

654 zptCat['fgcmFpGryRedErr'][:] = zpStruct['FGCM_FPGRY_CSPLITERR'][:, 2] 

655 zptCat['fgcmFpVar'][:] = zpStruct['FGCM_FPVAR'] 

656 zptCat['fgcmDust'][:] = zpStruct['FGCM_DUST'] 

657 zptCat['fgcmFlat'][:] = zpStruct['FGCM_FLAT'] 

658 zptCat['fgcmAperCorr'][:] = zpStruct['FGCM_APERCORR'] 

659 zptCat['fgcmDeltaMagBkg'][:] = zpStruct['FGCM_DELTAMAGBKG'] 

660 zptCat['exptime'][:] = zpStruct['EXPTIME'] 

661 

662 return zptCat 

663 

664 

665def makeAtmSchema(): 

666 """ 

667 Make the atmosphere schema 

668 

669 Returns 

670 ------- 

671 atmSchema: `lsst.afw.table.Schema` 

672 """ 

673 

674 atmSchema = afwTable.Schema() 

675 

676 atmSchema.addField('visit', type=np.int32, doc='Visit number') 

677 atmSchema.addField('pmb', type=np.float64, doc='Barometric pressure (mb)') 

678 atmSchema.addField('pwv', type=np.float64, doc='Water vapor (mm)') 

679 atmSchema.addField('tau', type=np.float64, doc='Aerosol optical depth') 

680 atmSchema.addField('alpha', type=np.float64, doc='Aerosol slope') 

681 atmSchema.addField('o3', type=np.float64, doc='Ozone (dobson)') 

682 atmSchema.addField('secZenith', type=np.float64, doc='Secant(zenith) (~ airmass)') 

683 atmSchema.addField('cTrans', type=np.float64, doc='Transmission correction factor') 

684 atmSchema.addField('lamStd', type=np.float64, doc='Wavelength for transmission correction') 

685 

686 return atmSchema 

687 

688 

689def makeAtmCat(atmSchema, atmStruct): 

690 """ 

691 Make the atmosphere catalog for persistence 

692 

693 Parameters 

694 ---------- 

695 atmSchema: `lsst.afw.table.Schema` 

696 Atmosphere catalog schema 

697 atmStruct: `numpy.ndarray` 

698 Atmosphere structure from fgcm 

699 

700 Returns 

701 ------- 

702 atmCat: `lsst.afw.table.BaseCatalog` 

703 Atmosphere catalog for persistence 

704 """ 

705 

706 atmCat = afwTable.BaseCatalog(atmSchema) 

707 atmCat.resize(atmStruct.size) 

708 

709 atmCat['visit'][:] = atmStruct['VISIT'] 

710 atmCat['pmb'][:] = atmStruct['PMB'] 

711 atmCat['pwv'][:] = atmStruct['PWV'] 

712 atmCat['tau'][:] = atmStruct['TAU'] 

713 atmCat['alpha'][:] = atmStruct['ALPHA'] 

714 atmCat['o3'][:] = atmStruct['O3'] 

715 atmCat['secZenith'][:] = atmStruct['SECZENITH'] 

716 atmCat['cTrans'][:] = atmStruct['CTRANS'] 

717 atmCat['lamStd'][:] = atmStruct['LAMSTD'] 

718 

719 return atmCat 

720 

721 

722def makeStdSchema(nBands): 

723 """ 

724 Make the standard star schema 

725 

726 Parameters 

727 ---------- 

728 nBands: `int` 

729 Number of bands in standard star catalog 

730 

731 Returns 

732 ------- 

733 stdSchema: `lsst.afw.table.Schema` 

734 """ 

735 

736 stdSchema = afwTable.SimpleTable.makeMinimalSchema() 

737 stdSchema.addField('ngood', type='ArrayI', doc='Number of good observations', 

738 size=nBands) 

739 stdSchema.addField('ntotal', type='ArrayI', doc='Number of total observations', 

740 size=nBands) 

741 stdSchema.addField('mag_std_noabs', type='ArrayF', 

742 doc='Standard magnitude (no absolute calibration)', 

743 size=nBands) 

744 stdSchema.addField('magErr_std', type='ArrayF', 

745 doc='Standard magnitude error', 

746 size=nBands) 

747 stdSchema.addField('npsfcand', type='ArrayI', 

748 doc='Number of observations flagged as psf candidates', 

749 size=nBands) 

750 

751 return stdSchema 

752 

753 

754def makeStdCat(stdSchema, stdStruct, goodBands): 

755 """ 

756 Make the standard star catalog for persistence 

757 

758 Parameters 

759 ---------- 

760 stdSchema: `lsst.afw.table.Schema` 

761 Standard star catalog schema 

762 stdStruct: `numpy.ndarray` 

763 Standard star structure in FGCM format 

764 goodBands: `list` 

765 List of good band names used in stdStruct 

766 

767 Returns 

768 ------- 

769 stdCat: `lsst.afw.table.BaseCatalog` 

770 Standard star catalog for persistence 

771 """ 

772 

773 stdCat = afwTable.SimpleCatalog(stdSchema) 

774 stdCat.resize(stdStruct.size) 

775 

776 stdCat['id'][:] = stdStruct['FGCM_ID'] 

777 stdCat['coord_ra'][:] = stdStruct['RA'] * geom.degrees 

778 stdCat['coord_dec'][:] = stdStruct['DEC'] * geom.degrees 

779 stdCat['ngood'][:, :] = stdStruct['NGOOD'][:, :] 

780 stdCat['ntotal'][:, :] = stdStruct['NTOTAL'][:, :] 

781 stdCat['mag_std_noabs'][:, :] = stdStruct['MAG_STD'][:, :] 

782 stdCat['magErr_std'][:, :] = stdStruct['MAGERR_STD'][:, :] 

783 stdCat['npsfcand'][:, :] = stdStruct['NPSFCAND'][:, :] 

784 

785 md = PropertyList() 

786 md.set("BANDS", list(goodBands)) 

787 stdCat.setMetadata(md) 

788 

789 return stdCat 

790 

791 

792def computeApertureRadiusFromDataRef(dataRef, fluxField): 

793 """ 

794 Compute the radius associated with a CircularApertureFlux field or 

795 associated slot. 

796 

797 Parameters 

798 ---------- 

799 dataRef : `lsst.daf.persistence.ButlerDataRef` 

800 fluxField : `str` 

801 CircularApertureFlux or associated slot. 

802 

803 Returns 

804 ------- 

805 apertureRadius : `float` 

806 Radius of the aperture field, in pixels. 

807 

808 Raises 

809 ------ 

810 RuntimeError: Raised if flux field is not a CircularApertureFlux, ApFlux, 

811 or associated slot. 

812 """ 

813 # TODO: Move this method to more general stack method in DM-25775 

814 datasetType = dataRef.butlerSubset.datasetType 

815 

816 if datasetType == 'src': 

817 schema = dataRef.get(datasetType='src_schema').schema 

818 try: 

819 fluxFieldName = schema[fluxField].asField().getName() 

820 except LookupError: 

821 raise RuntimeError("Could not find %s or associated slot in schema." % (fluxField)) 

822 # This may also raise a RuntimeError 

823 apertureRadius = computeApertureRadiusFromName(fluxFieldName) 

824 else: 

825 # This is a sourceTable_visit 

826 apertureRadius = computeApertureRadiusFromName(fluxField) 

827 

828 return apertureRadius 

829 

830 

831def computeApertureRadiusFromName(fluxField): 

832 """ 

833 Compute the radius associated with a CircularApertureFlux or ApFlux field. 

834 

835 Parameters 

836 ---------- 

837 fluxField : `str` 

838 CircularApertureFlux or ApFlux 

839 

840 Returns 

841 ------- 

842 apertureRadius : `float` 

843 Radius of the aperture field, in pixels. 

844 

845 Raises 

846 ------ 

847 RuntimeError: Raised if flux field is not a CircularApertureFlux 

848 or ApFlux. 

849 """ 

850 # TODO: Move this method to more general stack method in DM-25775 

851 m = re.search(r'(CircularApertureFlux|ApFlux)_(\d+)_(\d+)_', fluxField) 

852 

853 if m is None: 

854 raise RuntimeError(f"Flux field {fluxField} does not correspond to a CircularApertureFlux or ApFlux") 

855 

856 apertureRadius = float(m.groups()[1]) + float(m.groups()[2])/10. 

857 

858 return apertureRadius 

859 

860 

861def extractReferenceMags(refStars, bands, filterMap): 

862 """ 

863 Extract reference magnitudes from refStars for given bands and 

864 associated filterMap. 

865 

866 Parameters 

867 ---------- 

868 refStars : `lsst.afw.table.BaseCatalog` 

869 FGCM reference star catalog 

870 bands : `list` 

871 List of bands for calibration 

872 filterMap: `dict` 

873 FGCM mapping of filter to band 

874 

875 Returns 

876 ------- 

877 refMag : `np.ndarray` 

878 nstar x nband array of reference magnitudes 

879 refMagErr : `np.ndarray` 

880 nstar x nband array of reference magnitude errors 

881 """ 

882 # After DM-23331 fgcm reference catalogs have FILTERNAMES to prevent 

883 # against index errors and allow more flexibility in fitting after 

884 # the build stars step. 

885 

886 md = refStars.getMetadata() 

887 if 'FILTERNAMES' in md: 

888 filternames = md.getArray('FILTERNAMES') 

889 

890 # The reference catalog that fgcm wants has one entry per band 

891 # in the config file 

892 refMag = np.zeros((len(refStars), len(bands)), 

893 dtype=refStars['refMag'].dtype) + 99.0 

894 refMagErr = np.zeros_like(refMag) + 99.0 

895 for i, filtername in enumerate(filternames): 

896 # We are allowed to run the fit configured so that we do not 

897 # use every column in the reference catalog. 

898 try: 

899 band = filterMap[filtername] 

900 except KeyError: 

901 continue 

902 try: 

903 ind = bands.index(band) 

904 except ValueError: 

905 continue 

906 

907 refMag[:, ind] = refStars['refMag'][:, i] 

908 refMagErr[:, ind] = refStars['refMagErr'][:, i] 

909 

910 else: 

911 # Continue to use old catalogs as before. 

912 refMag = refStars['refMag'][:, :] 

913 refMagErr = refStars['refMagErr'][:, :] 

914 

915 return refMag, refMagErr