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

284 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 03:50 -0700

1# This file is part of fgcmcal. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

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

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

7# for details of code ownership. 

8# 

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

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

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

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

21"""Utility functions for fgcmcal. 

22 

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

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

25""" 

26 

27import numpy as np 

28import os 

29import re 

30from deprecated.sphinx import deprecated 

31 

32from lsst.daf.base import PropertyList 

33import lsst.afw.cameraGeom as afwCameraGeom 

34import lsst.afw.table as afwTable 

35import lsst.afw.image as afwImage 

36import lsst.afw.math as afwMath 

37import lsst.geom as geom 

38from lsst.obs.base import createInitialSkyWcs 

39from lsst.pipe.base import Instrument 

40 

41import fgcm 

42 

43 

44FGCM_EXP_FIELD = 'VISIT' 

45FGCM_CCD_FIELD = 'DETECTOR' 

46FGCM_ILLEGAL_VALUE = -9999.0 

47 

48 

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

50 resetFitParameters, outputZeropoints, 

51 lutFilterNames, tract=None): 

52 """ 

53 Make the FGCM fit cycle configuration dict 

54 

55 Parameters 

56 ---------- 

57 config: `lsst.fgcmcal.FgcmFitCycleConfig` 

58 Configuration object 

59 log: `lsst.log.Log` 

60 LSST log object 

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

62 Camera from the butler 

63 maxIter: `int` 

64 Maximum number of iterations 

65 resetFitParameters: `bool` 

66 Reset fit parameters before fitting? 

67 outputZeropoints: `bool` 

68 Compute zeropoints for output? 

69 lutFilterNames : array-like, `str` 

70 Array of physical filter names in the LUT. 

71 tract: `int`, optional 

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

73 Default is None. 

74 

75 Returns 

76 ------- 

77 configDict: `dict` 

78 Configuration dictionary for fgcm 

79 """ 

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

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

82 

83 # process the starColorCuts 

84 starColorCutList = [] 

85 for ccut in config.starColorCuts: 

86 parts = ccut.split(',') 

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

88 

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

90 # useful. See DM-16489. 

91 # Mirror area in cm**2 

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

93 

94 # Get approximate average camera gain: 

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

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

97 

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

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

100 filterName in lutFilterNames} 

101 

102 if tract is None: 

103 outfileBase = config.outfileBase 

104 else: 

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

106 

107 # create a configuration dictionary for fgcmFitCycle 

108 configDict = {'outfileBase': outfileBase, 

109 'logger': log, 

110 'exposureFile': None, 

111 'obsFile': None, 

112 'indexFile': None, 

113 'lutFile': None, 

114 'mirrorArea': mirrorArea, 

115 'cameraGain': cameraGain, 

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

117 'expField': FGCM_EXP_FIELD, 

118 'ccdField': FGCM_CCD_FIELD, 

119 'seeingField': 'DELTA_APER', 

120 'fwhmField': 'PSFSIGMA', 

121 'skyBrightnessField': 'SKYBACKGROUND', 

122 'deepFlag': 'DEEPFLAG', # unused 

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

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

125 'notFitBands': notFitBands, 

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

127 'filterToBand': filterToBand, 

128 'logLevel': 'INFO', 

129 'nCore': config.nCore, 

130 'nStarPerRun': config.nStarPerRun, 

131 'nExpPerRun': config.nExpPerRun, 

132 'reserveFraction': config.reserveFraction, 

133 'freezeStdAtmosphere': config.freezeStdAtmosphere, 

134 'precomputeSuperStarInitialCycle': config.precomputeSuperStarInitialCycle, 

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

136 'superStarSubCCDChebyshevOrder': config.superStarSubCcdChebyshevOrder, 

137 'superStarSubCCDTriangular': config.superStarSubCcdTriangular, 

138 'superStarSigmaClip': config.superStarSigmaClip, 

139 'focalPlaneSigmaClip': config.focalPlaneSigmaClip, 

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

141 'ccdGraySubCCDChebyshevOrder': config.ccdGraySubCcdChebyshevOrder, 

142 'ccdGraySubCCDTriangular': config.ccdGraySubCcdTriangular, 

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

144 'ccdGrayFocalPlaneChebyshevOrder': config.ccdGrayFocalPlaneChebyshevOrder, 

145 'ccdGrayFocalPlaneFitMinCcd': config.ccdGrayFocalPlaneFitMinCcd, 

146 'cycleNumber': config.cycleNumber, 

147 'maxIter': maxIter, 

148 'deltaMagBkgOffsetPercentile': config.deltaMagBkgOffsetPercentile, 

149 'deltaMagBkgPerCcd': config.deltaMagBkgPerCcd, 

150 'UTBoundary': config.utBoundary, 

151 'washMJDs': config.washMjds, 

152 'epochMJDs': config.epochMjds, 

153 'coatingMJDs': config.coatingMjds, 

154 'minObsPerBand': config.minObsPerBand, 

155 'latitude': config.latitude, 

156 'defaultCameraOrientation': config.defaultCameraOrientation, 

157 'brightObsGrayMax': config.brightObsGrayMax, 

158 'minStarPerCCD': config.minStarPerCcd, 

159 'minCCDPerExp': config.minCcdPerExp, 

160 'maxCCDGrayErr': config.maxCcdGrayErr, 

161 'minStarPerExp': config.minStarPerExp, 

162 'minExpPerNight': config.minExpPerNight, 

163 'expGrayInitialCut': config.expGrayInitialCut, 

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

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

166 'expGrayRecoverCut': config.expGrayRecoverCut, 

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

168 'expGrayErrRecoverCut': config.expGrayErrRecoverCut, 

169 'refStarSnMin': config.refStarSnMin, 

170 'refStarOutlierNSig': config.refStarOutlierNSig, 

171 'applyRefStarColorCuts': config.applyRefStarColorCuts, 

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

173 'starColorCuts': starColorCutList, 

174 'aperCorrFitNBins': config.aperCorrFitNBins, 

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

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

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

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

179 'sigFgcmMaxErr': config.sigFgcmMaxErr, 

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

181 'ccdGrayMaxStarErr': config.ccdGrayMaxStarErr, 

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

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

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

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

186 'sigma0Phot': config.sigma0Phot, 

187 'mapLongitudeRef': config.mapLongitudeRef, 

188 'mapNSide': config.mapNSide, 

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

190 'varMinBand': 2, 

191 'useRetrievedPwv': False, 

192 'useNightlyRetrievedPwv': False, 

193 'pwvRetrievalSmoothBlock': 25, 

194 'useQuadraticPwv': config.useQuadraticPwv, 

195 'useRetrievedTauInit': False, 

196 'tauRetrievalMinCCDPerNight': 500, 

197 'modelMagErrors': config.modelMagErrors, 

198 'instrumentParsPerBand': config.instrumentParsPerBand, 

199 'instrumentSlopeMinDeltaT': config.instrumentSlopeMinDeltaT, 

200 'fitMirrorChromaticity': config.fitMirrorChromaticity, 

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

202 'autoPhotometricCutNSig': config.autoPhotometricCutNSig, 

203 'autoHighCutNSig': config.autoHighCutNSig, 

204 'deltaAperInnerRadiusArcsec': config.deltaAperInnerRadiusArcsec, 

205 'deltaAperOuterRadiusArcsec': config.deltaAperOuterRadiusArcsec, 

206 'deltaAperFitMinNgoodObs': config.deltaAperFitMinNgoodObs, 

207 'deltaAperFitPerCcdNx': config.deltaAperFitPerCcdNx, 

208 'deltaAperFitPerCcdNy': config.deltaAperFitPerCcdNy, 

209 'deltaAperFitSpatialNside': config.deltaAperFitSpatialNside, 

210 'doComputeDeltaAperExposures': config.doComputeDeltaAperPerVisit, 

211 'doComputeDeltaAperStars': config.doComputeDeltaAperPerStar, 

212 'doComputeDeltaAperMap': config.doComputeDeltaAperMap, 

213 'doComputeDeltaAperPerCcd': config.doComputeDeltaAperPerCcd, 

214 'printOnly': False, 

215 'quietMode': config.quietMode, 

216 'randomSeed': config.randomSeed, 

217 'outputStars': False, 

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

219 'clobber': True, 

220 'useSedLUT': False, 

221 'resetParameters': resetFitParameters, 

222 'doPlots': config.doPlots, 

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

224 'outputZeropoints': outputZeropoints} 

225 

226 return configDict 

227 

228 

229def translateFgcmLut(lutCat, physicalFilterMap): 

230 """ 

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

232 

233 Parameters 

234 ---------- 

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

236 Catalog describing the FGCM look-up table 

237 physicalFilterMap: `dict` 

238 Physical filter to band mapping 

239 

240 Returns 

241 ------- 

242 fgcmLut: `lsst.fgcm.FgcmLut` 

243 Lookup table for FGCM 

244 lutIndexVals: `numpy.ndarray` 

245 Numpy array with LUT index information for FGCM 

246 lutStd: `numpy.ndarray` 

247 Numpy array with LUT standard throughput values for FGCM 

248 

249 Notes 

250 ----- 

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

252 """ 

253 

254 # first we need the lutIndexVals 

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

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

257 

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

259 # exceptions in the FGCM code. 

260 

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

262 lutFilterNames.size), 

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

264 lutStdFilterNames.size), 

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

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

267 ('PMBELEVATION', 'f8'), 

268 ('LAMBDANORM', 'f8'), 

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

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

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

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

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

274 ('NCCD', 'i4')]) 

275 

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

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

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

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

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

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

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

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

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

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

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

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

288 

289 # now we need the Standard Values 

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

291 ('PWVSTD', 'f8'), 

292 ('O3STD', 'f8'), 

293 ('TAUSTD', 'f8'), 

294 ('ALPHASTD', 'f8'), 

295 ('ZENITHSTD', 'f8'), 

296 ('LAMBDARANGE', 'f8', 2), 

297 ('LAMBDASTEP', 'f8'), 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

324 

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

326 

327 # And the flattened look-up-table 

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

329 ('I1', 'f4')]) 

330 

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

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

333 

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

335 ('D_O3', 'f4'), 

336 ('D_LNTAU', 'f4'), 

337 ('D_ALPHA', 'f4'), 

338 ('D_SECZENITH', 'f4'), 

339 ('D_LNPWV_I1', 'f4'), 

340 ('D_O3_I1', 'f4'), 

341 ('D_LNTAU_I1', 'f4'), 

342 ('D_ALPHA_I1', 'f4'), 

343 ('D_SECZENITH_I1', 'f4')]) 

344 

345 for name in lutDerivFlat.dtype.names: 

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

347 

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

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

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

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

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

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

354 filterToBand=physicalFilterMap) 

355 

356 return fgcmLut, lutIndexVals, lutStd 

357 

358 

359def translateVisitCatalog(visitCat): 

360 """ 

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

362 

363 Parameters 

364 ---------- 

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

366 FGCM visitCat from `lsst.fgcmcal.FgcmBuildStarsTask` 

367 

368 Returns 

369 ------- 

370 fgcmExpInfo: `numpy.ndarray` 

371 Numpy array for visit information for FGCM 

372 

373 Notes 

374 ----- 

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

376 """ 

377 

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

379 ('MJD', 'f8'), 

380 ('EXPTIME', 'f8'), 

381 ('PSFSIGMA', 'f8'), 

382 ('DELTA_APER', 'f8'), 

383 ('SKYBACKGROUND', 'f8'), 

384 ('DEEPFLAG', 'i2'), 

385 ('TELHA', 'f8'), 

386 ('TELRA', 'f8'), 

387 ('TELDEC', 'f8'), 

388 ('TELROT', 'f8'), 

389 ('PMB', 'f8'), 

390 ('FILTERNAME', 'a50')]) 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

406 

407 return fgcmExpInfo 

408 

409 

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

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

412def computeCcdOffsets(camera, defaultOrientation): 

413 """ 

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

415 

416 Parameters 

417 ---------- 

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

419 defaultOrientation: `float` 

420 Default camera orientation (degrees) 

421 

422 Returns 

423 ------- 

424 ccdOffsets: `numpy.ndarray` 

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

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

427 """ 

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

429 

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

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

432 ('DELTA_RA', 'f8'), 

433 ('DELTA_DEC', 'f8'), 

434 ('RA_SIZE', 'f8'), 

435 ('DEC_SIZE', 'f8'), 

436 ('X_SIZE', 'i4'), 

437 ('Y_SIZE', 'i4')]) 

438 

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

440 # since we are looking for relative positions 

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

442 

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

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

445 # time being, there is this ungainly hack. 

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

447 orientation = 270*geom.degrees 

448 else: 

449 orientation = defaultOrientation*geom.degrees 

450 flipX = False 

451 

452 # Create a temporary visitInfo for input to createInitialSkyWcs 

453 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight, 

454 boresightRotAngle=orientation, 

455 rotType=afwImage.RotType.SKY) 

456 

457 for i, detector in enumerate(camera): 

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

459 

460 wcs = createInitialSkyWcs(visitInfo, detector, flipX) 

461 

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

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

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

465 

466 bbox = detector.getBBox() 

467 

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

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

470 

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

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

473 

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

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

476 

477 return ccdOffsets 

478 

479 

480def computeReferencePixelScale(camera): 

481 """ 

482 Compute the median pixel scale in the camera 

483 

484 Returns 

485 ------- 

486 pixelScale: `float` 

487 Average pixel scale (arcsecond) over the camera 

488 """ 

489 

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

491 orientation = 0.0*geom.degrees 

492 flipX = False 

493 

494 # Create a temporary visitInfo for input to createInitialSkyWcs 

495 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight, 

496 boresightRotAngle=orientation, 

497 rotType=afwImage.RotType.SKY) 

498 

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

500 for i, detector in enumerate(camera): 

501 wcs = createInitialSkyWcs(visitInfo, detector, flipX) 

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

503 

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

505 return np.median(pixelScales[ok]) 

506 

507 

508def computeApproxPixelAreaFields(camera): 

509 """ 

510 Compute the approximate pixel area bounded fields from the camera 

511 geometry. 

512 

513 Parameters 

514 ---------- 

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

516 

517 Returns 

518 ------- 

519 approxPixelAreaFields: `dict` 

520 Dictionary of approximate area fields, keyed with detector ID 

521 """ 

522 

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

524 

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

526 # since we are looking for relative scales 

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

528 

529 flipX = False 

530 # Create a temporary visitInfo for input to createInitialSkyWcs 

531 # The orientation does not matter for the area computation 

532 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight, 

533 boresightRotAngle=0.0*geom.degrees, 

534 rotType=afwImage.RotType.SKY) 

535 

536 approxPixelAreaFields = {} 

537 

538 for i, detector in enumerate(camera): 

539 key = detector.getId() 

540 

541 wcs = createInitialSkyWcs(visitInfo, detector, flipX) 

542 bbox = detector.getBBox() 

543 

544 areaField = afwMath.PixelAreaBoundedField(bbox, wcs, 

545 unit=geom.arcseconds, scaling=areaScaling) 

546 approxAreaField = afwMath.ChebyshevBoundedField.approximate(areaField) 

547 

548 approxPixelAreaFields[key] = approxAreaField 

549 

550 return approxPixelAreaFields 

551 

552 

553def makeZptSchema(superStarChebyshevSize, zptChebyshevSize): 

554 """ 

555 Make the zeropoint schema 

556 

557 Parameters 

558 ---------- 

559 superStarChebyshevSize: `int` 

560 Length of the superstar chebyshev array 

561 zptChebyshevSize: `int` 

562 Length of the zeropoint chebyshev array 

563 

564 Returns 

565 ------- 

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

567 """ 

568 

569 zptSchema = afwTable.Schema() 

570 

571 zptSchema.addField('visit', type=np.int64, doc='Visit number') 

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

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

574 '1: Photometric, used in fit; ' 

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

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

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

578 '16: No zeropoint could be determined; ' 

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

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

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

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

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

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

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

586 size=zptChebyshevSize, 

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

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

589 size=superStarChebyshevSize, 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

611 'for 25% bluest stars') 

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

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

614 'for 25% bluest stars') 

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

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

617 'for 25% reddest stars') 

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

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

620 'for 25% reddest stars') 

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

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

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

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

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

626 'at the time of the exposure') 

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

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

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

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

631 '(value set by deltaMagBkgOffsetPercentile) calibration ' 

632 'stars.')) 

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

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

635 

636 return zptSchema 

637 

638 

639def makeZptCat(zptSchema, zpStruct): 

640 """ 

641 Make the zeropoint catalog for persistence 

642 

643 Parameters 

644 ---------- 

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

646 Zeropoint catalog schema 

647 zpStruct: `numpy.ndarray` 

648 Zeropoint structure from fgcm 

649 

650 Returns 

651 ------- 

652 zptCat: `afwTable.BaseCatalog` 

653 Zeropoint catalog for persistence 

654 """ 

655 

656 zptCat = afwTable.BaseCatalog(zptSchema) 

657 zptCat.reserve(zpStruct.size) 

658 

659 for filterName in zpStruct['FILTERNAME']: 

660 rec = zptCat.addNew() 

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

662 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

690 

691 return zptCat 

692 

693 

694def makeAtmSchema(): 

695 """ 

696 Make the atmosphere schema 

697 

698 Returns 

699 ------- 

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

701 """ 

702 

703 atmSchema = afwTable.Schema() 

704 

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

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

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

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

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

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

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

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

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

714 

715 return atmSchema 

716 

717 

718def makeAtmCat(atmSchema, atmStruct): 

719 """ 

720 Make the atmosphere catalog for persistence 

721 

722 Parameters 

723 ---------- 

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

725 Atmosphere catalog schema 

726 atmStruct: `numpy.ndarray` 

727 Atmosphere structure from fgcm 

728 

729 Returns 

730 ------- 

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

732 Atmosphere catalog for persistence 

733 """ 

734 

735 atmCat = afwTable.BaseCatalog(atmSchema) 

736 atmCat.resize(atmStruct.size) 

737 

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

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

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

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

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

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

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

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

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

747 

748 return atmCat 

749 

750 

751def makeStdSchema(nBands): 

752 """ 

753 Make the standard star schema 

754 

755 Parameters 

756 ---------- 

757 nBands: `int` 

758 Number of bands in standard star catalog 

759 

760 Returns 

761 ------- 

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

763 """ 

764 

765 stdSchema = afwTable.SimpleTable.makeMinimalSchema() 

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

767 size=nBands) 

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

769 size=nBands) 

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

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

772 size=nBands) 

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

774 doc='Standard magnitude error', 

775 size=nBands) 

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

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

778 size=nBands) 

779 stdSchema.addField('delta_aper', type='ArrayF', 

780 doc='Delta mag (small - large aperture)', 

781 size=nBands) 

782 

783 return stdSchema 

784 

785 

786def makeStdCat(stdSchema, stdStruct, goodBands): 

787 """ 

788 Make the standard star catalog for persistence 

789 

790 Parameters 

791 ---------- 

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

793 Standard star catalog schema 

794 stdStruct: `numpy.ndarray` 

795 Standard star structure in FGCM format 

796 goodBands: `list` 

797 List of good band names used in stdStruct 

798 

799 Returns 

800 ------- 

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

802 Standard star catalog for persistence 

803 """ 

804 

805 stdCat = afwTable.SimpleCatalog(stdSchema) 

806 stdCat.resize(stdStruct.size) 

807 

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

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

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

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

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

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

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

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

816 stdCat['delta_aper'][:, :] = stdStruct['DELTA_APER'][:, :] 

817 

818 md = PropertyList() 

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

820 stdCat.setMetadata(md) 

821 

822 return stdCat 

823 

824 

825def computeApertureRadiusFromName(fluxField): 

826 """ 

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

828 

829 Parameters 

830 ---------- 

831 fluxField : `str` 

832 CircularApertureFlux or ApFlux 

833 

834 Returns 

835 ------- 

836 apertureRadius : `float` 

837 Radius of the aperture field, in pixels. 

838 

839 Raises 

840 ------ 

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

842 ApFlux, or apFlux. 

843 """ 

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

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

846 

847 if m is None: 

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

849 

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

851 

852 return apertureRadius 

853 

854 

855def extractReferenceMags(refStars, bands, filterMap): 

856 """ 

857 Extract reference magnitudes from refStars for given bands and 

858 associated filterMap. 

859 

860 Parameters 

861 ---------- 

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

863 FGCM reference star catalog 

864 bands : `list` 

865 List of bands for calibration 

866 filterMap: `dict` 

867 FGCM mapping of filter to band 

868 

869 Returns 

870 ------- 

871 refMag : `np.ndarray` 

872 nstar x nband array of reference magnitudes 

873 refMagErr : `np.ndarray` 

874 nstar x nband array of reference magnitude errors 

875 """ 

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

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

878 # the build stars step. 

879 

880 md = refStars.getMetadata() 

881 if 'FILTERNAMES' in md: 

882 filternames = md.getArray('FILTERNAMES') 

883 

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

885 # in the config file 

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

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

888 refMagErr = np.zeros_like(refMag) + 99.0 

889 for i, filtername in enumerate(filternames): 

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

891 # use every column in the reference catalog. 

892 try: 

893 band = filterMap[filtername] 

894 except KeyError: 

895 continue 

896 try: 

897 ind = bands.index(band) 

898 except ValueError: 

899 continue 

900 

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

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

903 

904 else: 

905 # Continue to use old catalogs as before. 

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

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

908 

909 return refMag, refMagErr 

910 

911 

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

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

914 unboundedCollection = instrument.makeUnboundedCalibrationRunName() 

915 

916 return registry.queryDatasets(datasetType, 

917 dataId=quantumDataId, 

918 collections=[unboundedCollection])