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

306 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-13 01:15 -0800

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 

30 

31from lsst.daf.base import PropertyList 

32from lsst.daf.butler import Timespan 

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 

39 

40import fgcm 

41 

42 

43FGCM_EXP_FIELD = 'VISIT' 

44FGCM_CCD_FIELD = 'DETECTOR' 

45FGCM_ILLEGAL_VALUE = -9999.0 

46 

47 

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

49 resetFitParameters, outputZeropoints, 

50 lutFilterNames, tract=None, nCore=1, doPlots=False): 

51 """ 

52 Make the FGCM fit cycle configuration dict 

53 

54 Parameters 

55 ---------- 

56 config : `lsst.fgcmcal.FgcmFitCycleConfig` 

57 Configuration object 

58 log : `logging.Logger` 

59 Log object. 

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

61 Camera from the butler 

62 maxIter : `int` 

63 Maximum number of iterations 

64 resetFitParameters: `bool` 

65 Reset fit parameters before fitting? 

66 outputZeropoints : `bool` 

67 Compute zeropoints for output? 

68 lutFilterNames : array-like, `str` 

69 Array of physical filter names in the LUT. 

70 tract : `int`, optional 

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

72 Default is None. 

73 nCore : `int`, optional 

74 Number of cores to use. 

75 doPlots : `bool`, optional 

76 Make FGCM QA plots? 

77 

78 Returns 

79 ------- 

80 configDict : `dict` 

81 Configuration dictionary for fgcm 

82 """ 

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

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

85 

86 # process the starColorCuts 

87 starColorCutList = [] 

88 for ccut in config.starColorCuts: 

89 if ccut == 'NO_DATA': 

90 # No color cuts to apply. 

91 break 

92 parts = ccut.split(',') 

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

94 

95 # process the refStarColorCuts 

96 refStarColorCutList = [] 

97 for ccut in config.refStarColorCuts: 

98 if ccut == 'NO_DATA': 

99 # No color cuts to apply. 

100 break 

101 parts = ccut.split(',') 

102 refStarColorCutList.append([parts[0], parts[1], float(parts[2]), float(parts[3])]) 

103 

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

105 # useful. See DM-16489. 

106 # Mirror area in cm**2 

107 if config.mirrorArea is None: 

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

109 else: 

110 # Convert to square cm. 

111 mirrorArea = config.mirrorArea * 100.**2. 

112 

113 if config.cameraGain is None: 

114 # Get approximate average camera gain: 

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

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

117 else: 

118 cameraGain = config.cameraGain 

119 

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

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

122 filterName in lutFilterNames} 

123 

124 if tract is None: 

125 outfileBase = config.outfileBase 

126 else: 

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

128 

129 if config.aperCorrPerCcd: 

130 seeingField = 'DELTA_APER_DETECTOR' 

131 else: 

132 seeingField = 'DELTA_APER' 

133 

134 # create a configuration dictionary for fgcmFitCycle 

135 configDict = {'outfileBase': outfileBase, 

136 'logger': log, 

137 'exposureFile': None, 

138 'obsFile': None, 

139 'indexFile': None, 

140 'lutFile': None, 

141 'mirrorArea': mirrorArea, 

142 'cameraGain': cameraGain, 

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

144 'expField': FGCM_EXP_FIELD, 

145 'ccdField': FGCM_CCD_FIELD, 

146 'seeingField': seeingField, 

147 'fwhmField': 'PSFFWHM', 

148 'skyBrightnessField': 'SKYBACKGROUND', 

149 'deepFlag': 'DEEPFLAG', # unused 

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

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

152 'notFitBands': notFitBands, 

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

154 'filterToBand': filterToBand, 

155 'logLevel': 'INFO', 

156 'nCore': nCore, 

157 'nStarPerRun': config.nStarPerRun, 

158 'nStarPerGrayRun': config.nStarPerGrayRun, 

159 'nExpPerRun': config.nExpPerRun, 

160 'reserveFraction': config.reserveFraction, 

161 'freezeStdAtmosphere': config.freezeStdAtmosphere, 

162 'precomputeSuperStarInitialCycle': config.precomputeSuperStarInitialCycle, 

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

164 'superStarSubCCDChebyshevOrder': config.superStarSubCcdChebyshevOrder, 

165 'superStarSubCCDTriangular': config.superStarSubCcdTriangular, 

166 'superStarSigmaClip': config.superStarSigmaClip, 

167 'superStarPlotCCDResiduals': config.superStarPlotCcdResiduals, 

168 'superStarForceZeroMean': config.superStarForceZeroMean, 

169 'focalPlaneSigmaClip': config.focalPlaneSigmaClip, 

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

171 'ccdGraySubCCDChebyshevOrder': config.ccdGraySubCcdChebyshevOrder, 

172 'ccdGraySubCCDTriangular': config.ccdGraySubCcdTriangular, 

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

174 'ccdGrayFocalPlaneChebyshevOrder': config.ccdGrayFocalPlaneChebyshevOrder, 

175 'ccdGrayFocalPlaneFitMinCcd': config.ccdGrayFocalPlaneFitMinCcd, 

176 'ccdGrayFocalPlaneMaxStars': config.ccdGrayFocalPlaneMaxStars, 

177 'cycleNumber': config.cycleNumber, 

178 'maxIter': maxIter, 

179 'deltaMagBkgOffsetPercentile': config.deltaMagBkgOffsetPercentile, 

180 'deltaMagBkgPerCcd': config.deltaMagBkgPerCcd, 

181 'UTBoundary': config.utBoundary, 

182 'washMJDs': config.washMjds, 

183 'epochMJDs': config.epochMjds, 

184 'coatingMJDs': config.coatingMjds, 

185 'minObsPerBand': config.minObsPerBand, 

186 'latitude': config.latitude, 

187 'defaultCameraOrientation': config.defaultCameraOrientation, 

188 'brightObsGrayMax': config.brightObsGrayMax, 

189 'minStarPerCCD': config.minStarPerCcd, 

190 'minCCDPerExp': config.minCcdPerExp, 

191 'maxCCDGrayErr': config.maxCcdGrayErr, 

192 'minStarPerExp': config.minStarPerExp, 

193 'minExpPerNight': config.minExpPerNight, 

194 'expGrayInitialCut': config.expGrayInitialCut, 

195 'expFwhmCutDict': dict(config.expFwhmCutDict), 

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

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

198 'expGrayRecoverCut': config.expGrayRecoverCut, 

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

200 'expGrayErrRecoverCut': config.expGrayErrRecoverCut, 

201 'refStarSnMin': config.refStarSnMin, 

202 'refStarOutlierNSig': config.refStarOutlierNSig, 

203 'applyRefStarColorCuts': config.applyRefStarColorCuts, 

204 'refStarMaxFracUse': config.refStarMaxFracUse, 

205 'useExposureReferenceOffset': config.useExposureReferenceOffset, 

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

207 'starColorCuts': starColorCutList, 

208 'refStarColorCuts': refStarColorCutList, 

209 'aperCorrFitNBins': config.aperCorrFitNBins, 

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

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

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

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

214 'sigFgcmMaxErr': config.sigFgcmMaxErr, 

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

216 'ccdGrayMaxStarErr': config.ccdGrayMaxStarErr, 

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

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

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

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

221 'sigma0Phot': config.sigma0Phot, 

222 'mapLongitudeRef': config.mapLongitudeRef, 

223 'mapNSide': config.mapNSide, 

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

225 'varMinBand': 2, 

226 'useRetrievedPwv': False, 

227 'useNightlyRetrievedPwv': False, 

228 'pwvRetrievalSmoothBlock': 25, 

229 'useQuadraticPwv': config.useQuadraticPwv, 

230 'useRetrievedTauInit': False, 

231 'tauRetrievalMinCCDPerNight': 500, 

232 'modelMagErrors': config.modelMagErrors, 

233 'instrumentParsPerBand': config.instrumentParsPerBand, 

234 'instrumentSlopeMinDeltaT': config.instrumentSlopeMinDeltaT, 

235 'fitMirrorChromaticity': config.fitMirrorChromaticity, 

236 'fitCCDChromaticityDict': dict(config.fitCcdChromaticityDict), 

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

238 'autoPhotometricCutNSig': config.autoPhotometricCutNSig, 

239 'autoHighCutNSig': config.autoHighCutNSig, 

240 'deltaAperInnerRadiusArcsec': config.deltaAperInnerRadiusArcsec, 

241 'deltaAperOuterRadiusArcsec': config.deltaAperOuterRadiusArcsec, 

242 'deltaAperFitMinNgoodObs': config.deltaAperFitMinNgoodObs, 

243 'deltaAperFitPerCcdNx': config.deltaAperFitPerCcdNx, 

244 'deltaAperFitPerCcdNy': config.deltaAperFitPerCcdNy, 

245 'deltaAperFitSpatialNside': config.deltaAperFitSpatialNside, 

246 'doComputeDeltaAperExposures': config.doComputeDeltaAperPerVisit, 

247 'doComputeDeltaAperStars': config.doComputeDeltaAperPerStar, 

248 'doComputeDeltaAperMap': config.doComputeDeltaAperMap, 

249 'doComputeDeltaAperPerCcd': config.doComputeDeltaAperPerCcd, 

250 'printOnly': False, 

251 'quietMode': config.quietMode, 

252 'randomSeed': config.randomSeed, 

253 'outputStars': False, 

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

255 'clobber': True, 

256 'useSedLUT': False, 

257 'resetParameters': resetFitParameters, 

258 'doPlots': doPlots, 

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

260 'outputZeropoints': outputZeropoints} 

261 

262 return configDict 

263 

264 

265def translateFgcmLut(lutCat, physicalFilterMap): 

266 """ 

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

268 

269 Parameters 

270 ---------- 

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

272 Catalog describing the FGCM look-up table 

273 physicalFilterMap: `dict` 

274 Physical filter to band mapping 

275 

276 Returns 

277 ------- 

278 fgcmLut: `lsst.fgcm.FgcmLut` 

279 Lookup table for FGCM 

280 lutIndexVals: `numpy.ndarray` 

281 Numpy array with LUT index information for FGCM 

282 lutStd: `numpy.ndarray` 

283 Numpy array with LUT standard throughput values for FGCM 

284 

285 Notes 

286 ----- 

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

288 """ 

289 

290 # first we need the lutIndexVals 

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

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

293 

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

295 # exceptions in the FGCM code. 

296 

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

298 lutFilterNames.size), 

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

300 lutStdFilterNames.size), 

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

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

303 ('PMBELEVATION', 'f8'), 

304 ('LAMBDANORM', 'f8'), 

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

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

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

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

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

310 ('NCCD', 'i4')]) 

311 

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

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

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

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

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

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

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

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

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

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

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

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

324 

325 # now we need the Standard Values 

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

327 ('PWVSTD', 'f8'), 

328 ('O3STD', 'f8'), 

329 ('TAUSTD', 'f8'), 

330 ('ALPHASTD', 'f8'), 

331 ('ZENITHSTD', 'f8'), 

332 ('LAMBDARANGE', 'f8', 2), 

333 ('LAMBDASTEP', 'f8'), 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

360 

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

362 

363 # And the flattened look-up-table 

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

365 ('I1', 'f4')]) 

366 

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

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

369 

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

371 ('D_O3', 'f4'), 

372 ('D_LNTAU', 'f4'), 

373 ('D_ALPHA', 'f4'), 

374 ('D_SECZENITH', 'f4'), 

375 ('D_LNPWV_I1', 'f4'), 

376 ('D_O3_I1', 'f4'), 

377 ('D_LNTAU_I1', 'f4'), 

378 ('D_ALPHA_I1', 'f4'), 

379 ('D_SECZENITH_I1', 'f4')]) 

380 

381 for name in lutDerivFlat.dtype.names: 

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

383 

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

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

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

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

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

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

390 filterToBand=physicalFilterMap) 

391 

392 return fgcmLut, lutIndexVals, lutStd 

393 

394 

395def translateVisitCatalog(visitCat): 

396 """ 

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

398 

399 Parameters 

400 ---------- 

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

402 FGCM visitCat from `lsst.fgcmcal.FgcmBuildStarsTask` 

403 

404 Returns 

405 ------- 

406 fgcmExpInfo: `numpy.ndarray` 

407 Numpy array for visit information for FGCM 

408 

409 Notes 

410 ----- 

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

412 """ 

413 

414 nDetector = visitCat.schema["deltaAperDetector"].asKey().getSize() 

415 

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

417 ('MJD', 'f8'), 

418 ('EXPTIME', 'f8'), 

419 ('PSFFWHM', 'f8'), 

420 ('DELTA_APER', 'f8'), 

421 ('DELTA_APER_DETECTOR', ('f8', nDetector)), 

422 ('SKYBACKGROUND', 'f8'), 

423 ('DEEPFLAG', 'i2'), 

424 ('TELHA', 'f8'), 

425 ('TELRA', 'f8'), 

426 ('TELDEC', 'f8'), 

427 ('TELROT', 'f8'), 

428 ('PMB', 'f8'), 

429 ('FILTERNAME', 'S50')]) 

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

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

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

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

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

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

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

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

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

439 fgcmExpInfo['PSFFWHM'][:] = visitCat['psfFwhm'] 

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

441 fgcmExpInfo['DELTA_APER_DETECTOR'][:, :] = visitCat['deltaAperDetector'] 

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

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

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

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

446 

447 return fgcmExpInfo 

448 

449 

450def _computeDefaultVisitInfo(): 

451 """ 

452 Compute a default visitInfo for use with pixel scale / jacobians. 

453 

454 Returns 

455 ------- 

456 visitInfo : `lsst.afw.image.VisitInfo` 

457 The visit info object. 

458 """ 

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

460 # since we are looking for relative scales 

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

462 orientation = 0.0*geom.degrees 

463 

464 # Create a temporary visitInfo for input to createInitialSkyWcs 

465 # The orientation does not matter for the area computation 

466 visitInfo = afwImage.VisitInfo(boresightRaDec=boresight, 

467 boresightRotAngle=orientation, 

468 rotType=afwImage.RotType.SKY) 

469 return visitInfo 

470 

471 

472def computeReferencePixelScale(camera, useScienceDetectors=False): 

473 """ 

474 Compute the median pixel scale in the camera 

475 

476 Returns 

477 ------- 

478 pixelScale : `float` 

479 Average pixel scale (arcsecond) over the camera 

480 useScienceDetectors : `bool`, optional 

481 Limit to just science detectors? 

482 """ 

483 visitInfo = _computeDefaultVisitInfo() 

484 

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

486 for i, detector in enumerate(camera): 

487 if useScienceDetectors: 

488 if not detector.getType() == afwCameraGeom.DetectorType.SCIENCE: 

489 continue 

490 wcs = createInitialSkyWcs(visitInfo, detector, False) 

491 pixelScales[i] = wcs.getPixelScale(detector.getBBox().getCenter()).asArcseconds() 

492 

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

494 return np.median(pixelScales[ok]) 

495 

496 

497def computePixelAreaFieldDetector(detector, visitInfo=None, areaScaling=1.0, approximate=False): 

498 """ 

499 Compute the reference pixel area field for a detector. 

500 

501 Parameters 

502 ---------- 

503 visitInfo : `lsst.afw.image.VisitInfo` 

504 The visit info object. 

505 detector : `lsst.afw.cameraGeom.detector` 

506 The detector object. 

507 visitInfo : `lsst.afw.image.VisitInfo`, optional 

508 If not supplied, will compute a default visitInfo. 

509 areaScaling : `float`, optional 

510 Area scaling factor. 

511 approximate : `bool`, optional 

512 Compute Chebyshev approximation to pixel area field? 

513 

514 Returns 

515 ------- 

516 pixelAreaField : `lsst.afw.math.BoundedField` 

517 Bounded field describing the pixel area from the detector model. 

518 """ 

519 if visitInfo is None: 

520 visitInfo = _computeDefaultVisitInfo() 

521 

522 wcs = createInitialSkyWcs(visitInfo, detector, False) 

523 bbox = detector.getBBox() 

524 

525 pixelAreaField = afwMath.PixelAreaBoundedField( 

526 bbox, 

527 wcs, 

528 unit=geom.arcseconds, 

529 scaling=areaScaling, 

530 ) 

531 

532 if approximate: 

533 pixelAreaField = afwMath.ChebyshevBoundedField.approximate(pixelAreaField) 

534 

535 return pixelAreaField 

536 

537 

538def computeApproxPixelAreaFields(camera): 

539 """ 

540 Compute the approximate pixel area bounded fields from the camera 

541 geometry. 

542 

543 Parameters 

544 ---------- 

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

546 

547 Returns 

548 ------- 

549 approxPixelAreaFields: `dict` 

550 Dictionary of approximate area fields, keyed with detector ID 

551 """ 

552 

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

554 

555 visitInfo = _computeDefaultVisitInfo() 

556 

557 approxPixelAreaFields = {} 

558 

559 for i, detector in enumerate(camera): 

560 key = detector.getId() 

561 

562 approxAreaField = computePixelAreaFieldDetector( 

563 detector, 

564 visitInfo=visitInfo, 

565 areaScaling=areaScaling, 

566 approximate=True, 

567 ) 

568 

569 approxPixelAreaFields[key] = approxAreaField 

570 

571 return approxPixelAreaFields 

572 

573 

574def makeZptSchema(superStarChebyshevSize, zptChebyshevSize): 

575 """ 

576 Make the zeropoint schema 

577 

578 Parameters 

579 ---------- 

580 superStarChebyshevSize: `int` 

581 Length of the superstar chebyshev array 

582 zptChebyshevSize: `int` 

583 Length of the zeropoint chebyshev array 

584 

585 Returns 

586 ------- 

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

588 """ 

589 

590 zptSchema = afwTable.Schema() 

591 

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

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

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

595 '1: Photometric, used in fit; ' 

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

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

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

599 '16: No zeropoint could be determined; ' 

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

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

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

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

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

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

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

607 size=zptChebyshevSize, 

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

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

610 size=superStarChebyshevSize, 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

632 'for 25% bluest stars') 

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

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

635 'for 25% bluest stars') 

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

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

638 'for 25% reddest stars') 

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

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

641 'for 25% reddest stars') 

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

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

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

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

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

647 'at the time of the exposure') 

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

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

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

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

652 '(value set by deltaMagBkgOffsetPercentile) calibration ' 

653 'stars.')) 

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

655 zptSchema.addField('filtername', type=str, size=30, doc='Filter name') 

656 

657 return zptSchema 

658 

659 

660def makeZptCat(zptSchema, zpStruct): 

661 """ 

662 Make the zeropoint catalog for persistence 

663 

664 Parameters 

665 ---------- 

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

667 Zeropoint catalog schema 

668 zpStruct: `numpy.ndarray` 

669 Zeropoint structure from fgcm 

670 

671 Returns 

672 ------- 

673 zptCat: `afwTable.BaseCatalog` 

674 Zeropoint catalog for persistence 

675 """ 

676 

677 zptCat = afwTable.BaseCatalog(zptSchema) 

678 zptCat.reserve(zpStruct.size) 

679 

680 for filterName in zpStruct['FILTERNAME']: 

681 rec = zptCat.addNew() 

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

683 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

711 

712 return zptCat 

713 

714 

715def makeAtmSchema(): 

716 """ 

717 Make the atmosphere schema 

718 

719 Returns 

720 ------- 

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

722 """ 

723 

724 atmSchema = afwTable.Schema() 

725 

726 atmSchema.addField('visit', type=np.int64, doc='Visit number') 

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

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

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

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

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

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

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

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

735 

736 return atmSchema 

737 

738 

739def makeAtmCat(atmSchema, atmStruct): 

740 """ 

741 Make the atmosphere catalog for persistence 

742 

743 Parameters 

744 ---------- 

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

746 Atmosphere catalog schema 

747 atmStruct: `numpy.ndarray` 

748 Atmosphere structure from fgcm 

749 

750 Returns 

751 ------- 

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

753 Atmosphere catalog for persistence 

754 """ 

755 

756 atmCat = afwTable.BaseCatalog(atmSchema) 

757 atmCat.resize(atmStruct.size) 

758 

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

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

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

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

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

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

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

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

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

768 

769 return atmCat 

770 

771 

772def makeStdSchema(nBands): 

773 """ 

774 Make the standard star schema 

775 

776 Parameters 

777 ---------- 

778 nBands: `int` 

779 Number of bands in standard star catalog 

780 

781 Returns 

782 ------- 

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

784 """ 

785 

786 stdSchema = afwTable.SimpleTable.makeMinimalSchema() 

787 stdSchema.addField('isolated_star_id', type='L', doc='ID from isolated star table') 

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

789 size=nBands) 

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

791 size=nBands) 

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

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

794 size=nBands) 

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

796 doc='Standard magnitude error', 

797 size=nBands) 

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

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

800 size=nBands) 

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

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

803 size=nBands) 

804 

805 return stdSchema 

806 

807 

808def makeStdCat(stdSchema, stdStruct, goodBands): 

809 """ 

810 Make the standard star catalog for persistence 

811 

812 Parameters 

813 ---------- 

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

815 Standard star catalog schema 

816 stdStruct: `numpy.ndarray` 

817 Standard star structure in FGCM format 

818 goodBands: `list` 

819 List of good band names used in stdStruct 

820 

821 Returns 

822 ------- 

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

824 Standard star catalog for persistence 

825 """ 

826 

827 stdCat = afwTable.SimpleCatalog(stdSchema) 

828 stdCat.resize(stdStruct.size) 

829 

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

831 stdCat['isolated_star_id'][:] = stdStruct['ALTERNATE_ID'] 

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

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

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

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

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

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

838 if 'NPSFCAND' in stdStruct.dtype.names: 

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

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

841 

842 md = PropertyList() 

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

844 stdCat.setMetadata(md) 

845 

846 return stdCat 

847 

848 

849def computeApertureRadiusFromName(fluxField): 

850 """ 

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

852 

853 Parameters 

854 ---------- 

855 fluxField : `str` 

856 CircularApertureFlux or ApFlux 

857 

858 Returns 

859 ------- 

860 apertureRadius : `float` 

861 Radius of the aperture field, in pixels. 

862 

863 Raises 

864 ------ 

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

866 ApFlux, or apFlux. 

867 """ 

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

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

870 

871 if m is None: 

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

873 

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

875 

876 return apertureRadius 

877 

878 

879def extractReferenceMags(refStars, bands, filterMap): 

880 """ 

881 Extract reference magnitudes from refStars for given bands and 

882 associated filterMap. 

883 

884 Parameters 

885 ---------- 

886 refStars : `astropy.table.Table` or `lsst.afw.table.BaseCatalog` 

887 FGCM reference star catalog. 

888 bands : `list` 

889 List of bands for calibration. 

890 filterMap: `dict` 

891 FGCM mapping of filter to band. 

892 

893 Returns 

894 ------- 

895 refMag : `np.ndarray` 

896 nstar x nband array of reference magnitudes. 

897 refMagErr : `np.ndarray` 

898 nstar x nband array of reference magnitude errors. 

899 """ 

900 hasAstropyMeta = False 

901 try: 

902 meta = refStars.meta 

903 hasAstropyMeta = True 

904 except AttributeError: 

905 meta = refStars.getMetadata() 

906 

907 if 'FILTERNAMES' in meta: 

908 if hasAstropyMeta: 

909 filternames = meta['FILTERNAMES'] 

910 else: 

911 filternames = meta.getArray('FILTERNAMES') 

912 

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

914 # in the config file 

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

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

917 refMagErr = np.zeros_like(refMag) + 99.0 

918 for i, filtername in enumerate(filternames): 

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

920 # use every column in the reference catalog. 

921 try: 

922 band = filterMap[filtername] 

923 except KeyError: 

924 continue 

925 try: 

926 ind = bands.index(band) 

927 except ValueError: 

928 continue 

929 

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

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

932 else: 

933 raise RuntimeError("FGCM reference stars missing FILTERNAMES metadata.") 

934 

935 return refMag, refMagErr 

936 

937 

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

939 # For static calibrations, we search with a timespan that has unbounded 

940 # begin and end; we'll get an error if there's more than one match (because 

941 # then it's not static). 

942 timespan = Timespan(begin=None, end=None) 

943 result = [] 

944 # First iterate over all of the data IDs for this dataset type that are 

945 # consistent with the quantum data ID. 

946 for dataId in registry.queryDataIds(datasetType.dimensions, dataId=quantumDataId): 

947 # Find the dataset with this data ID using the unbounded timespan. 

948 if ref := registry.findDataset(datasetType, dataId, collections=collections, timespan=timespan): 

949 result.append(ref) 

950 return result 

951 

952 

953def countDetectors(camera, useScienceDetectors): 

954 """Count the detectors in the camera. 

955 

956 This may be limited to just the science detectors. 

957 

958 Parameters 

959 ---------- 

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

961 Camera object. 

962 useScienceDetectors : `bool`, optional 

963 Limit to just science detectors? 

964 """ 

965 if not useScienceDetectors: 

966 return len(camera) 

967 

968 nDetector = 0 

969 for detector in camera: 

970 if not detector.getType() == afwCameraGeom.DetectorType.SCIENCE: 

971 continue 

972 nDetector += 1 

973 

974 return nDetector