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

Shortcuts 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

296 statements  

1# This file is part of fgcmcal. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21"""Utility functions for fgcmcal. 

22 

23This file contains utility functions that are used by more than one task, 

24and do not need to be part of a task. 

25""" 

26 

27import numpy as np 

28import os 

29import re 

30from deprecated.sphinx import deprecated 

31 

32from lsst.daf.base import PropertyList 

33import lsst.daf.persistence as dafPersist 

34import lsst.afw.cameraGeom as afwCameraGeom 

35import lsst.afw.table as afwTable 

36import lsst.afw.image as afwImage 

37import lsst.afw.math as afwMath 

38import lsst.geom as geom 

39from lsst.obs.base import createInitialSkyWcs 

40from lsst.obs.base import Instrument 

41 

42import fgcm 

43 

44 

45FGCM_EXP_FIELD = 'VISIT' 

46FGCM_CCD_FIELD = 'DETECTOR' 

47FGCM_ILLEGAL_VALUE = -9999.0 

48 

49 

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

51 resetFitParameters, outputZeropoints, 

52 lutFilterNames, tract=None): 

53 """ 

54 Make the FGCM fit cycle configuration dict 

55 

56 Parameters 

57 ---------- 

58 config: `lsst.fgcmcal.FgcmFitCycleConfig` 

59 Configuration object 

60 log: `lsst.log.Log` 

61 LSST log object 

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

63 Camera from the butler 

64 maxIter: `int` 

65 Maximum number of iterations 

66 resetFitParameters: `bool` 

67 Reset fit parameters before fitting? 

68 outputZeropoints: `bool` 

69 Compute zeropoints for output? 

70 lutFilterNames : array-like, `str` 

71 Array of physical filter names in the LUT. 

72 tract: `int`, optional 

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

74 Default is None. 

75 

76 Returns 

77 ------- 

78 configDict: `dict` 

79 Configuration dictionary for fgcm 

80 """ 

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

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

83 

84 # process the starColorCuts 

85 starColorCutList = [] 

86 for ccut in config.starColorCuts: 

87 parts = ccut.split(',') 

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

89 

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

91 # useful. See DM-16489. 

92 # Mirror area in cm**2 

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

94 

95 # Get approximate average camera gain: 

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

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

98 

99 # Cut down the filter map to those that are in the LUT 

100 filterToBand = {filterName: config.physicalFilterMap[filterName] for 

101 filterName in lutFilterNames} 

102 

103 if tract is None: 

104 outfileBase = config.outfileBase 

105 else: 

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

107 

108 # create a configuration dictionary for fgcmFitCycle 

109 configDict = {'outfileBase': outfileBase, 

110 'logger': log, 

111 'exposureFile': None, 

112 'obsFile': None, 

113 'indexFile': None, 

114 'lutFile': None, 

115 'mirrorArea': mirrorArea, 

116 'cameraGain': cameraGain, 

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

118 'expField': FGCM_EXP_FIELD, 

119 'ccdField': FGCM_CCD_FIELD, 

120 'seeingField': 'DELTA_APER', 

121 'fwhmField': 'PSFSIGMA', 

122 'skyBrightnessField': 'SKYBACKGROUND', 

123 'deepFlag': 'DEEPFLAG', # unused 

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

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

126 'notFitBands': notFitBands, 

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

128 'filterToBand': filterToBand, 

129 'logLevel': 'INFO', 

130 'nCore': config.nCore, 

131 'nStarPerRun': config.nStarPerRun, 

132 'nExpPerRun': config.nExpPerRun, 

133 'reserveFraction': config.reserveFraction, 

134 'freezeStdAtmosphere': config.freezeStdAtmosphere, 

135 'precomputeSuperStarInitialCycle': config.precomputeSuperStarInitialCycle, 

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

137 'superStarSubCCDChebyshevOrder': config.superStarSubCcdChebyshevOrder, 

138 'superStarSubCCDTriangular': config.superStarSubCcdTriangular, 

139 'superStarSigmaClip': config.superStarSigmaClip, 

140 'focalPlaneSigmaClip': config.focalPlaneSigmaClip, 

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

142 'ccdGraySubCCDChebyshevOrder': config.ccdGraySubCcdChebyshevOrder, 

143 'ccdGraySubCCDTriangular': config.ccdGraySubCcdTriangular, 

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

145 'ccdGrayFocalPlaneChebyshevOrder': config.ccdGrayFocalPlaneChebyshevOrder, 

146 'ccdGrayFocalPlaneFitMinCcd': config.ccdGrayFocalPlaneFitMinCcd, 

147 'cycleNumber': config.cycleNumber, 

148 'maxIter': maxIter, 

149 'deltaMagBkgOffsetPercentile': config.deltaMagBkgOffsetPercentile, 

150 'deltaMagBkgPerCcd': config.deltaMagBkgPerCcd, 

151 'UTBoundary': config.utBoundary, 

152 'washMJDs': config.washMjds, 

153 'epochMJDs': config.epochMjds, 

154 'coatingMJDs': config.coatingMjds, 

155 'minObsPerBand': config.minObsPerBand, 

156 'latitude': config.latitude, 

157 'defaultCameraOrientation': config.defaultCameraOrientation, 

158 'brightObsGrayMax': config.brightObsGrayMax, 

159 'minStarPerCCD': config.minStarPerCcd, 

160 'minCCDPerExp': config.minCcdPerExp, 

161 'maxCCDGrayErr': config.maxCcdGrayErr, 

162 'minStarPerExp': config.minStarPerExp, 

163 'minExpPerNight': config.minExpPerNight, 

164 'expGrayInitialCut': config.expGrayInitialCut, 

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

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

167 'expGrayRecoverCut': config.expGrayRecoverCut, 

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

169 'expGrayErrRecoverCut': config.expGrayErrRecoverCut, 

170 'refStarSnMin': config.refStarSnMin, 

171 'refStarOutlierNSig': config.refStarOutlierNSig, 

172 'applyRefStarColorCuts': config.applyRefStarColorCuts, 

173 'illegalValue': FGCM_ILLEGAL_VALUE, # internally used by fgcm. 

174 'starColorCuts': starColorCutList, 

175 'aperCorrFitNBins': config.aperCorrFitNBins, 

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

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

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

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

180 'sigFgcmMaxErr': config.sigFgcmMaxErr, 

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

182 'ccdGrayMaxStarErr': config.ccdGrayMaxStarErr, 

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

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

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

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

187 'sigma0Phot': config.sigma0Phot, 

188 'mapLongitudeRef': config.mapLongitudeRef, 

189 'mapNSide': config.mapNSide, 

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

191 'varMinBand': 2, 

192 'useRetrievedPwv': False, 

193 'useNightlyRetrievedPwv': False, 

194 'pwvRetrievalSmoothBlock': 25, 

195 'useQuadraticPwv': config.useQuadraticPwv, 

196 'useRetrievedTauInit': False, 

197 'tauRetrievalMinCCDPerNight': 500, 

198 'modelMagErrors': config.modelMagErrors, 

199 'instrumentParsPerBand': config.instrumentParsPerBand, 

200 'instrumentSlopeMinDeltaT': config.instrumentSlopeMinDeltaT, 

201 'fitMirrorChromaticity': config.fitMirrorChromaticity, 

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

203 'autoPhotometricCutNSig': config.autoPhotometricCutNSig, 

204 'autoHighCutNSig': config.autoHighCutNSig, 

205 'printOnly': False, 

206 'quietMode': config.quietMode, 

207 'randomSeed': config.randomSeed, 

208 'outputStars': False, 

209 'outputPath': os.path.abspath('.'), 

210 'clobber': True, 

211 'useSedLUT': False, 

212 'resetParameters': resetFitParameters, 

213 'doPlots': config.doPlots, 

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

215 'outputZeropoints': outputZeropoints} 

216 

217 return configDict 

218 

219 

220def translateFgcmLut(lutCat, physicalFilterMap): 

221 """ 

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

223 

224 Parameters 

225 ---------- 

226 lutCat: `lsst.afw.table.BaseCatalog` 

227 Catalog describing the FGCM look-up table 

228 physicalFilterMap: `dict` 

229 Physical filter to band mapping 

230 

231 Returns 

232 ------- 

233 fgcmLut: `lsst.fgcm.FgcmLut` 

234 Lookup table for FGCM 

235 lutIndexVals: `numpy.ndarray` 

236 Numpy array with LUT index information for FGCM 

237 lutStd: `numpy.ndarray` 

238 Numpy array with LUT standard throughput values for FGCM 

239 

240 Notes 

241 ----- 

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

243 """ 

244 

245 # first we need the lutIndexVals 

246 lutFilterNames = np.array(lutCat[0]['physicalFilters'].split(','), dtype='U') 

247 lutStdFilterNames = np.array(lutCat[0]['stdPhysicalFilters'].split(','), dtype='U') 

248 

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

250 # exceptions in the FGCM code. 

251 

252 lutIndexVals = np.zeros(1, dtype=[('FILTERNAMES', lutFilterNames.dtype.str, 

253 lutFilterNames.size), 

254 ('STDFILTERNAMES', lutStdFilterNames.dtype.str, 

255 lutStdFilterNames.size), 

256 ('PMB', 'f8', lutCat[0]['pmb'].size), 

257 ('PMBFACTOR', 'f8', lutCat[0]['pmbFactor'].size), 

258 ('PMBELEVATION', 'f8'), 

259 ('LAMBDANORM', 'f8'), 

260 ('PWV', 'f8', lutCat[0]['pwv'].size), 

261 ('O3', 'f8', lutCat[0]['o3'].size), 

262 ('TAU', 'f8', lutCat[0]['tau'].size), 

263 ('ALPHA', 'f8', lutCat[0]['alpha'].size), 

264 ('ZENITH', 'f8', lutCat[0]['zenith'].size), 

265 ('NCCD', 'i4')]) 

266 

267 lutIndexVals['FILTERNAMES'][:] = lutFilterNames 

268 lutIndexVals['STDFILTERNAMES'][:] = lutStdFilterNames 

269 lutIndexVals['PMB'][:] = lutCat[0]['pmb'] 

270 lutIndexVals['PMBFACTOR'][:] = lutCat[0]['pmbFactor'] 

271 lutIndexVals['PMBELEVATION'] = lutCat[0]['pmbElevation'] 

272 lutIndexVals['LAMBDANORM'] = lutCat[0]['lambdaNorm'] 

273 lutIndexVals['PWV'][:] = lutCat[0]['pwv'] 

274 lutIndexVals['O3'][:] = lutCat[0]['o3'] 

275 lutIndexVals['TAU'][:] = lutCat[0]['tau'] 

276 lutIndexVals['ALPHA'][:] = lutCat[0]['alpha'] 

277 lutIndexVals['ZENITH'][:] = lutCat[0]['zenith'] 

278 lutIndexVals['NCCD'] = lutCat[0]['nCcd'] 

279 

280 # now we need the Standard Values 

281 lutStd = np.zeros(1, dtype=[('PMBSTD', 'f8'), 

282 ('PWVSTD', 'f8'), 

283 ('O3STD', 'f8'), 

284 ('TAUSTD', 'f8'), 

285 ('ALPHASTD', 'f8'), 

286 ('ZENITHSTD', 'f8'), 

287 ('LAMBDARANGE', 'f8', 2), 

288 ('LAMBDASTEP', 'f8'), 

289 ('LAMBDASTD', 'f8', lutFilterNames.size), 

290 ('LAMBDASTDFILTER', 'f8', lutStdFilterNames.size), 

291 ('I0STD', 'f8', lutFilterNames.size), 

292 ('I1STD', 'f8', lutFilterNames.size), 

293 ('I10STD', 'f8', lutFilterNames.size), 

294 ('I2STD', 'f8', lutFilterNames.size), 

295 ('LAMBDAB', 'f8', lutFilterNames.size), 

296 ('ATMLAMBDA', 'f8', lutCat[0]['atmLambda'].size), 

297 ('ATMSTDTRANS', 'f8', lutCat[0]['atmStdTrans'].size)]) 

298 lutStd['PMBSTD'] = lutCat[0]['pmbStd'] 

299 lutStd['PWVSTD'] = lutCat[0]['pwvStd'] 

300 lutStd['O3STD'] = lutCat[0]['o3Std'] 

301 lutStd['TAUSTD'] = lutCat[0]['tauStd'] 

302 lutStd['ALPHASTD'] = lutCat[0]['alphaStd'] 

303 lutStd['ZENITHSTD'] = lutCat[0]['zenithStd'] 

304 lutStd['LAMBDARANGE'][:] = lutCat[0]['lambdaRange'][:] 

305 lutStd['LAMBDASTEP'] = lutCat[0]['lambdaStep'] 

306 lutStd['LAMBDASTD'][:] = lutCat[0]['lambdaStd'] 

307 lutStd['LAMBDASTDFILTER'][:] = lutCat[0]['lambdaStdFilter'] 

308 lutStd['I0STD'][:] = lutCat[0]['i0Std'] 

309 lutStd['I1STD'][:] = lutCat[0]['i1Std'] 

310 lutStd['I10STD'][:] = lutCat[0]['i10Std'] 

311 lutStd['I2STD'][:] = lutCat[0]['i2Std'] 

312 lutStd['LAMBDAB'][:] = lutCat[0]['lambdaB'] 

313 lutStd['ATMLAMBDA'][:] = lutCat[0]['atmLambda'][:] 

314 lutStd['ATMSTDTRANS'][:] = lutCat[0]['atmStdTrans'][:] 

315 

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

317 

318 # And the flattened look-up-table 

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

320 ('I1', 'f4')]) 

321 

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

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

324 

325 lutDerivFlat = np.zeros(lutCat[0]['lut'].size, dtype=[('D_LNPWV', 'f4'), 

326 ('D_O3', 'f4'), 

327 ('D_LNTAU', 'f4'), 

328 ('D_ALPHA', 'f4'), 

329 ('D_SECZENITH', 'f4'), 

330 ('D_LNPWV_I1', 'f4'), 

331 ('D_O3_I1', 'f4'), 

332 ('D_LNTAU_I1', 'f4'), 

333 ('D_ALPHA_I1', 'f4'), 

334 ('D_SECZENITH_I1', 'f4')]) 

335 

336 for name in lutDerivFlat.dtype.names: 

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

338 

339 # The fgcm.FgcmLUT() class copies all the LUT information into special 

340 # shared memory objects that will not blow up the memory usage when used 

341 # with python multiprocessing. Once all the numbers are copied, the 

342 # references to the temporary objects (lutCat, lutFlat, lutDerivFlat) 

343 # will fall out of scope and can be cleaned up by the garbage collector. 

344 fgcmLut = fgcm.FgcmLUT(lutIndexVals, lutFlat, lutDerivFlat, lutStd, 

345 filterToBand=physicalFilterMap) 

346 

347 return fgcmLut, lutIndexVals, lutStd 

348 

349 

350def translateVisitCatalog(visitCat): 

351 """ 

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

353 

354 Parameters 

355 ---------- 

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

357 FGCM visitCat from `lsst.fgcmcal.FgcmBuildStarsTask` 

358 

359 Returns 

360 ------- 

361 fgcmExpInfo: `numpy.ndarray` 

362 Numpy array for visit information for FGCM 

363 

364 Notes 

365 ----- 

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

367 """ 

368 

369 fgcmExpInfo = np.zeros(len(visitCat), dtype=[('VISIT', 'i8'), 

370 ('MJD', 'f8'), 

371 ('EXPTIME', 'f8'), 

372 ('PSFSIGMA', 'f8'), 

373 ('DELTA_APER', 'f8'), 

374 ('SKYBACKGROUND', 'f8'), 

375 ('DEEPFLAG', 'i2'), 

376 ('TELHA', 'f8'), 

377 ('TELRA', 'f8'), 

378 ('TELDEC', 'f8'), 

379 ('TELROT', 'f8'), 

380 ('PMB', 'f8'), 

381 ('FILTERNAME', 'a50')]) 

382 fgcmExpInfo['VISIT'][:] = visitCat['visit'] 

383 fgcmExpInfo['MJD'][:] = visitCat['mjd'] 

384 fgcmExpInfo['EXPTIME'][:] = visitCat['exptime'] 

385 fgcmExpInfo['DEEPFLAG'][:] = visitCat['deepFlag'] 

386 fgcmExpInfo['TELHA'][:] = visitCat['telha'] 

387 fgcmExpInfo['TELRA'][:] = visitCat['telra'] 

388 fgcmExpInfo['TELDEC'][:] = visitCat['teldec'] 

389 fgcmExpInfo['TELROT'][:] = visitCat['telrot'] 

390 fgcmExpInfo['PMB'][:] = visitCat['pmb'] 

391 fgcmExpInfo['PSFSIGMA'][:] = visitCat['psfSigma'] 

392 fgcmExpInfo['DELTA_APER'][:] = visitCat['deltaAper'] 

393 fgcmExpInfo['SKYBACKGROUND'][:] = visitCat['skyBackground'] 

394 # Note that we have to go through asAstropy() to get a string 

395 # array out of an afwTable. This is faster than a row-by-row loop. 

396 fgcmExpInfo['FILTERNAME'][:] = visitCat.asAstropy()['physicalFilter'] 

397 

398 return fgcmExpInfo 

399 

400 

401@deprecated(reason="This method is no longer used in fgcmcal. It will be removed after v23.", 

402 version="v23.0", category=FutureWarning) 

403def computeCcdOffsets(camera, defaultOrientation): 

404 """ 

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

406 

407 Parameters 

408 ---------- 

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

410 defaultOrientation: `float` 

411 Default camera orientation (degrees) 

412 

413 Returns 

414 ------- 

415 ccdOffsets: `numpy.ndarray` 

416 Numpy array with ccd offset information for input to FGCM. 

417 Angular units are degrees, and x/y units are pixels. 

418 """ 

419 # TODO: DM-21215 will fully generalize to arbitrary camera orientations 

420 

421 # and we need to know the ccd offsets from the camera geometry 

422 ccdOffsets = np.zeros(len(camera), dtype=[('CCDNUM', 'i4'), 

423 ('DELTA_RA', 'f8'), 

424 ('DELTA_DEC', 'f8'), 

425 ('RA_SIZE', 'f8'), 

426 ('DEC_SIZE', 'f8'), 

427 ('X_SIZE', 'i4'), 

428 ('Y_SIZE', 'i4')]) 

429 

430 # Generate fake WCSs centered at 180/0 to avoid the RA=0/360 problem, 

431 # since we are looking for relative positions 

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

433 

434 # TODO: DM-17597 will update testdata_jointcal so that the test data 

435 # does not have nan as the boresight angle for HSC data. For the 

436 # time being, there is this ungainly hack. 

437 if camera.getName() == 'HSC' and np.isnan(defaultOrientation): 

438 orientation = 270*geom.degrees 

439 else: 

440 orientation = defaultOrientation*geom.degrees 

441 flipX = False 

442 

443 # Create a temporary visitInfo for input to createInitialSkyWcs 

444 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight, 

445 boresightRotAngle=orientation, 

446 rotType=afwImage.RotType.SKY) 

447 

448 for i, detector in enumerate(camera): 

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

450 

451 wcs = createInitialSkyWcs(visitInfo, detector, flipX) 

452 

453 detCenter = wcs.pixelToSky(detector.getCenter(afwCameraGeom.PIXELS)) 

454 ccdOffsets['DELTA_RA'][i] = (detCenter.getRa() - boresight.getRa()).asDegrees() 

455 ccdOffsets['DELTA_DEC'][i] = (detCenter.getDec() - boresight.getDec()).asDegrees() 

456 

457 bbox = detector.getBBox() 

458 

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

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

461 

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

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

464 

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

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

467 

468 return ccdOffsets 

469 

470 

471def computeReferencePixelScale(camera): 

472 """ 

473 Compute the median pixel scale in the camera 

474 

475 Returns 

476 ------- 

477 pixelScale: `float` 

478 Average pixel scale (arcsecond) over the camera 

479 """ 

480 

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

482 orientation = 0.0*geom.degrees 

483 flipX = False 

484 

485 # Create a temporary visitInfo for input to createInitialSkyWcs 

486 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight, 

487 boresightRotAngle=orientation, 

488 rotType=afwImage.RotType.SKY) 

489 

490 pixelScales = np.zeros(len(camera)) 

491 for i, detector in enumerate(camera): 

492 wcs = createInitialSkyWcs(visitInfo, detector, flipX) 

493 pixelScales[i] = wcs.getPixelScale().asArcseconds() 

494 

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

496 return np.median(pixelScales[ok]) 

497 

498 

499def computeApproxPixelAreaFields(camera): 

500 """ 

501 Compute the approximate pixel area bounded fields from the camera 

502 geometry. 

503 

504 Parameters 

505 ---------- 

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

507 

508 Returns 

509 ------- 

510 approxPixelAreaFields: `dict` 

511 Dictionary of approximate area fields, keyed with detector ID 

512 """ 

513 

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

515 

516 # Generate fake WCSs centered at 180/0 to avoid the RA=0/360 problem, 

517 # since we are looking for relative scales 

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

519 

520 flipX = False 

521 # Create a temporary visitInfo for input to createInitialSkyWcs 

522 # The orientation does not matter for the area computation 

523 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight, 

524 boresightRotAngle=0.0*geom.degrees, 

525 rotType=afwImage.RotType.SKY) 

526 

527 approxPixelAreaFields = {} 

528 

529 for i, detector in enumerate(camera): 

530 key = detector.getId() 

531 

532 wcs = createInitialSkyWcs(visitInfo, detector, flipX) 

533 bbox = detector.getBBox() 

534 

535 areaField = afwMath.PixelAreaBoundedField(bbox, wcs, 

536 unit=geom.arcseconds, scaling=areaScaling) 

537 approxAreaField = afwMath.ChebyshevBoundedField.approximate(areaField) 

538 

539 approxPixelAreaFields[key] = approxAreaField 

540 

541 return approxPixelAreaFields 

542 

543 

544def makeZptSchema(superStarChebyshevSize, zptChebyshevSize): 

545 """ 

546 Make the zeropoint schema 

547 

548 Parameters 

549 ---------- 

550 superStarChebyshevSize: `int` 

551 Length of the superstar chebyshev array 

552 zptChebyshevSize: `int` 

553 Length of the zeropoint chebyshev array 

554 

555 Returns 

556 ------- 

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

558 """ 

559 

560 zptSchema = afwTable.Schema() 

561 

562 zptSchema.addField('visit', type=np.int32, doc='Visit number') 

563 zptSchema.addField('detector', type=np.int32, doc='Detector ID number') 

564 zptSchema.addField('fgcmFlag', type=np.int32, doc=('FGCM flag value: ' 

565 '1: Photometric, used in fit; ' 

566 '2: Photometric, not used in fit; ' 

567 '4: Non-photometric, on partly photometric night; ' 

568 '8: Non-photometric, on non-photometric night; ' 

569 '16: No zeropoint could be determined; ' 

570 '32: Too few stars for reliable gray computation')) 

571 zptSchema.addField('fgcmZpt', type=np.float64, doc='FGCM zeropoint (center of CCD)') 

572 zptSchema.addField('fgcmZptErr', type=np.float64, 

573 doc='Error on zeropoint, estimated from repeatability + number of obs') 

574 zptSchema.addField('fgcmfZptChebXyMax', type='ArrayD', size=2, 

575 doc='maximum x/maximum y to scale to apply chebyshev parameters') 

576 zptSchema.addField('fgcmfZptCheb', type='ArrayD', 

577 size=zptChebyshevSize, 

578 doc='Chebyshev parameters (flattened) for zeropoint') 

579 zptSchema.addField('fgcmfZptSstarCheb', type='ArrayD', 

580 size=superStarChebyshevSize, 

581 doc='Chebyshev parameters (flattened) for superStarFlat') 

582 zptSchema.addField('fgcmI0', type=np.float64, doc='Integral of the passband') 

583 zptSchema.addField('fgcmI10', type=np.float64, doc='Normalized chromatic integral') 

584 zptSchema.addField('fgcmR0', type=np.float64, 

585 doc='Retrieved i0 integral, estimated from stars (only for flag 1)') 

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

587 doc='Retrieved i10 integral, estimated from stars (only for flag 1)') 

588 zptSchema.addField('fgcmGry', type=np.float64, 

589 doc='Estimated gray extinction relative to atmospheric solution; ' 

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

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

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

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

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

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

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

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

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

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

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

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

602 'for 25% bluest stars') 

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

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

605 'for 25% bluest stars') 

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

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

608 'for 25% reddest stars') 

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

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

611 'for 25% reddest stars') 

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

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

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

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

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

617 'at the time of the exposure') 

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

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

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

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

622 '(value set by deltaMagBkgOffsetPercentile) calibration ' 

623 'stars.')) 

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

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

626 

627 return zptSchema 

628 

629 

630def makeZptCat(zptSchema, zpStruct): 

631 """ 

632 Make the zeropoint catalog for persistence 

633 

634 Parameters 

635 ---------- 

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

637 Zeropoint catalog schema 

638 zpStruct: `numpy.ndarray` 

639 Zeropoint structure from fgcm 

640 

641 Returns 

642 ------- 

643 zptCat: `afwTable.BaseCatalog` 

644 Zeropoint catalog for persistence 

645 """ 

646 

647 zptCat = afwTable.BaseCatalog(zptSchema) 

648 zptCat.reserve(zpStruct.size) 

649 

650 for filterName in zpStruct['FILTERNAME']: 

651 rec = zptCat.addNew() 

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

653 

654 zptCat['visit'][:] = zpStruct[FGCM_EXP_FIELD] 

655 zptCat['detector'][:] = zpStruct[FGCM_CCD_FIELD] 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

681 

682 return zptCat 

683 

684 

685def makeAtmSchema(): 

686 """ 

687 Make the atmosphere schema 

688 

689 Returns 

690 ------- 

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

692 """ 

693 

694 atmSchema = afwTable.Schema() 

695 

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

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

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

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

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

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

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

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

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

705 

706 return atmSchema 

707 

708 

709def makeAtmCat(atmSchema, atmStruct): 

710 """ 

711 Make the atmosphere catalog for persistence 

712 

713 Parameters 

714 ---------- 

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

716 Atmosphere catalog schema 

717 atmStruct: `numpy.ndarray` 

718 Atmosphere structure from fgcm 

719 

720 Returns 

721 ------- 

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

723 Atmosphere catalog for persistence 

724 """ 

725 

726 atmCat = afwTable.BaseCatalog(atmSchema) 

727 atmCat.resize(atmStruct.size) 

728 

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

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

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

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

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

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

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

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

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

738 

739 return atmCat 

740 

741 

742def makeStdSchema(nBands): 

743 """ 

744 Make the standard star schema 

745 

746 Parameters 

747 ---------- 

748 nBands: `int` 

749 Number of bands in standard star catalog 

750 

751 Returns 

752 ------- 

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

754 """ 

755 

756 stdSchema = afwTable.SimpleTable.makeMinimalSchema() 

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

758 size=nBands) 

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

760 size=nBands) 

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

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

763 size=nBands) 

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

765 doc='Standard magnitude error', 

766 size=nBands) 

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

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

769 size=nBands) 

770 

771 return stdSchema 

772 

773 

774def makeStdCat(stdSchema, stdStruct, goodBands): 

775 """ 

776 Make the standard star catalog for persistence 

777 

778 Parameters 

779 ---------- 

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

781 Standard star catalog schema 

782 stdStruct: `numpy.ndarray` 

783 Standard star structure in FGCM format 

784 goodBands: `list` 

785 List of good band names used in stdStruct 

786 

787 Returns 

788 ------- 

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

790 Standard star catalog for persistence 

791 """ 

792 

793 stdCat = afwTable.SimpleCatalog(stdSchema) 

794 stdCat.resize(stdStruct.size) 

795 

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

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

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

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

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

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

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

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

804 

805 md = PropertyList() 

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

807 stdCat.setMetadata(md) 

808 

809 return stdCat 

810 

811 

812def computeApertureRadiusFromDataRef(dataRef, fluxField): 

813 """ 

814 Compute the radius associated with a CircularApertureFlux field or 

815 associated slot. 

816 

817 Parameters 

818 ---------- 

819 dataRef : `lsst.daf.persistence.ButlerDataRef` or 

820 `lsst.daf.butler.DeferredDatasetHandle` 

821 fluxField : `str` 

822 CircularApertureFlux or associated slot. 

823 

824 Returns 

825 ------- 

826 apertureRadius : `float` 

827 Radius of the aperture field, in pixels. 

828 

829 Raises 

830 ------ 

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

832 apFlux, or associated slot. 

833 """ 

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

835 if isinstance(dataRef, dafPersist.ButlerDataRef): 

836 # Gen2 dataRef 

837 datasetType = dataRef.butlerSubset.datasetType 

838 else: 

839 # Gen3 dataRef 

840 datasetType = dataRef.ref.datasetType.name 

841 

842 if datasetType == 'src': 

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

844 try: 

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

846 except LookupError: 

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

848 # This may also raise a RuntimeError 

849 apertureRadius = computeApertureRadiusFromName(fluxFieldName) 

850 else: 

851 # This is a sourceTable_visit 

852 apertureRadius = computeApertureRadiusFromName(fluxField) 

853 

854 return apertureRadius 

855 

856 

857def computeApertureRadiusFromName(fluxField): 

858 """ 

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

860 

861 Parameters 

862 ---------- 

863 fluxField : `str` 

864 CircularApertureFlux or ApFlux 

865 

866 Returns 

867 ------- 

868 apertureRadius : `float` 

869 Radius of the aperture field, in pixels. 

870 

871 Raises 

872 ------ 

873 RuntimeError: Raised if flux field is not a CircularApertureFlux, 

874 ApFlux, or apFlux. 

875 """ 

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

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

878 

879 if m is None: 

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

881 

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

883 

884 return apertureRadius 

885 

886 

887def extractReferenceMags(refStars, bands, filterMap): 

888 """ 

889 Extract reference magnitudes from refStars for given bands and 

890 associated filterMap. 

891 

892 Parameters 

893 ---------- 

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

895 FGCM reference star catalog 

896 bands : `list` 

897 List of bands for calibration 

898 filterMap: `dict` 

899 FGCM mapping of filter to band 

900 

901 Returns 

902 ------- 

903 refMag : `np.ndarray` 

904 nstar x nband array of reference magnitudes 

905 refMagErr : `np.ndarray` 

906 nstar x nband array of reference magnitude errors 

907 """ 

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

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

910 # the build stars step. 

911 

912 md = refStars.getMetadata() 

913 if 'FILTERNAMES' in md: 

914 filternames = md.getArray('FILTERNAMES') 

915 

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

917 # in the config file 

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

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

920 refMagErr = np.zeros_like(refMag) + 99.0 

921 for i, filtername in enumerate(filternames): 

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

923 # use every column in the reference catalog. 

924 try: 

925 band = filterMap[filtername] 

926 except KeyError: 

927 continue 

928 try: 

929 ind = bands.index(band) 

930 except ValueError: 

931 continue 

932 

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

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

935 

936 else: 

937 # Continue to use old catalogs as before. 

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

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

940 

941 return refMag, refMagErr 

942 

943 

944def lookupStaticCalibrations(datasetType, registry, quantumDataId, collections): 

945 instrument = Instrument.fromName(quantumDataId["instrument"], registry) 

946 unboundedCollection = instrument.makeUnboundedCalibrationRunName() 

947 

948 return registry.queryDatasets(datasetType, 

949 dataId=quantumDataId, 

950 collections=[unboundedCollection])