Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# See COPYRIGHT file at the top of the source tree. 

2# 

3# This file is part of fgcmcal. 

4# 

5# Developed for the LSST Data Management System. 

6# This product includes software developed by the LSST Project 

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

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

9# for details of code ownership. 

10# 

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

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

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

14# (at your option) any later version. 

15# 

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

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

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

19# GNU General Public License for more details. 

20# 

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

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

23"""Make a look-up-table (LUT) for FGCM calibration. 

24 

25This task computes a look-up-table for the range in expected atmosphere 

26variation and variation in instrumental throughput (as tracked by the 

27transmission_filter products). By pre-computing linearized integrals, 

28the FGCM fit is orders of magnitude faster for stars with a broad range 

29of colors and observing bands, yielding precision at the 1-2 mmag level. 

30 

31Computing a LUT requires running MODTRAN or with a pre-generated 

32atmosphere table packaged with fgcm. 

33""" 

34 

35import sys 

36import traceback 

37 

38import numpy as np 

39 

40from lsst.obs.base import Instrument 

41import lsst.pex.config as pexConfig 

42import lsst.pipe.base as pipeBase 

43from lsst.pipe.base import connectionTypes 

44import lsst.afw.table as afwTable 

45import lsst.afw.cameraGeom as afwCameraGeom 

46from .utilities import lookupStaticCalibrations 

47 

48import fgcm 

49 

50__all__ = ['FgcmMakeLutParametersConfig', 'FgcmMakeLutConfig', 'FgcmMakeLutTask', 

51 'FgcmMakeLutRunner'] 

52 

53 

54class FgcmMakeLutConnections(pipeBase.PipelineTaskConnections, 

55 dimensions=('instrument',), 

56 defaultTemplates={}): 

57 camera = connectionTypes.PrerequisiteInput( 

58 doc="Camera instrument", 

59 name="camera", 

60 storageClass="Camera", 

61 dimensions=("instrument",), 

62 lookupFunction=lookupStaticCalibrations, 

63 isCalibration=True, 

64 ) 

65 

66 transmission_optics = connectionTypes.PrerequisiteInput( 

67 doc="Optics transmission curve information", 

68 name="transmission_optics", 

69 storageClass="TransmissionCurve", 

70 dimensions=("instrument",), 

71 lookupFunction=lookupStaticCalibrations, 

72 isCalibration=True, 

73 deferLoad=True, 

74 ) 

75 

76 transmission_sensor = connectionTypes.PrerequisiteInput( 

77 doc="Sensor transmission curve information", 

78 name="transmission_sensor", 

79 storageClass="TransmissionCurve", 

80 dimensions=("instrument", "detector",), 

81 lookupFunction=lookupStaticCalibrations, 

82 isCalibration=True, 

83 deferLoad=True, 

84 multiple=True, 

85 ) 

86 

87 transmission_filter = connectionTypes.PrerequisiteInput( 

88 doc="Filter transmission curve information", 

89 name="transmission_filter", 

90 storageClass="TransmissionCurve", 

91 dimensions=("band", "instrument", "physical_filter",), 

92 lookupFunction=lookupStaticCalibrations, 

93 isCalibration=True, 

94 deferLoad=True, 

95 multiple=True, 

96 ) 

97 

98 fgcmLookUpTable = connectionTypes.Output( 

99 doc=("Atmosphere + instrument look-up-table for FGCM throughput and " 

100 "chromatic corrections."), 

101 name="fgcmLookUpTable", 

102 storageClass="Catalog", 

103 dimensions=("instrument",), 

104 ) 

105 

106 

107class FgcmMakeLutParametersConfig(pexConfig.Config): 

108 """Config for parameters if atmosphereTableName not available""" 

109 # TODO: When DM-16511 is done, it will be possible to get the 

110 # telescope elevation directly from the camera. 

111 elevation = pexConfig.Field( 

112 doc="Telescope elevation (m)", 

113 dtype=float, 

114 default=None, 

115 ) 

116 pmbRange = pexConfig.ListField( 

117 doc=("Barometric Pressure range (millibar) " 

118 "Recommended range depends on the site."), 

119 dtype=float, 

120 default=None, 

121 ) 

122 pmbSteps = pexConfig.Field( 

123 doc="Barometric Pressure number of steps", 

124 dtype=int, 

125 default=5, 

126 ) 

127 pwvRange = pexConfig.ListField( 

128 doc=("Precipitable Water Vapor range (mm) " 

129 "Recommended range depends on the site."), 

130 dtype=float, 

131 default=None, 

132 ) 

133 pwvSteps = pexConfig.Field( 

134 doc="Precipitable Water Vapor number of steps", 

135 dtype=int, 

136 default=15, 

137 ) 

138 o3Range = pexConfig.ListField( 

139 doc="Ozone range (dob)", 

140 dtype=float, 

141 default=[220.0, 310.0], 

142 ) 

143 o3Steps = pexConfig.Field( 

144 doc="Ozone number of steps", 

145 dtype=int, 

146 default=3, 

147 ) 

148 tauRange = pexConfig.ListField( 

149 doc="Aerosol Optical Depth range (unitless)", 

150 dtype=float, 

151 default=[0.002, 0.35], 

152 ) 

153 tauSteps = pexConfig.Field( 

154 doc="Aerosol Optical Depth number of steps", 

155 dtype=int, 

156 default=11, 

157 ) 

158 alphaRange = pexConfig.ListField( 

159 doc="Aerosol alpha range (unitless)", 

160 dtype=float, 

161 default=[0.0, 2.0], 

162 ) 

163 alphaSteps = pexConfig.Field( 

164 doc="Aerosol alpha number of steps", 

165 dtype=int, 

166 default=9, 

167 ) 

168 zenithRange = pexConfig.ListField( 

169 doc="Zenith angle range (degree)", 

170 dtype=float, 

171 default=[0.0, 70.0], 

172 ) 

173 zenithSteps = pexConfig.Field( 

174 doc="Zenith angle number of steps", 

175 dtype=int, 

176 default=21, 

177 ) 

178 # Note that the standard atmosphere parameters depend on the observatory 

179 # and elevation, and so these should be set on a per-camera basis. 

180 pmbStd = pexConfig.Field( 

181 doc=("Standard Atmosphere pressure (millibar); " 

182 "Recommended default depends on the site."), 

183 dtype=float, 

184 default=None, 

185 ) 

186 pwvStd = pexConfig.Field( 

187 doc=("Standard Atmosphere PWV (mm); " 

188 "Recommended default depends on the site."), 

189 dtype=float, 

190 default=None, 

191 ) 

192 o3Std = pexConfig.Field( 

193 doc="Standard Atmosphere O3 (dob)", 

194 dtype=float, 

195 default=263.0, 

196 ) 

197 tauStd = pexConfig.Field( 

198 doc="Standard Atmosphere aerosol optical depth", 

199 dtype=float, 

200 default=0.03, 

201 ) 

202 alphaStd = pexConfig.Field( 

203 doc="Standard Atmosphere aerosol alpha", 

204 dtype=float, 

205 default=1.0, 

206 ) 

207 airmassStd = pexConfig.Field( 

208 doc=("Standard Atmosphere airmass; " 

209 "Recommended default depends on the survey strategy."), 

210 dtype=float, 

211 default=None, 

212 ) 

213 lambdaNorm = pexConfig.Field( 

214 doc="Aerosol Optical Depth normalization wavelength (Angstrom)", 

215 dtype=float, 

216 default=7750.0, 

217 ) 

218 lambdaStep = pexConfig.Field( 

219 doc="Wavelength step for generating atmospheres (nm)", 

220 dtype=float, 

221 default=0.5, 

222 ) 

223 lambdaRange = pexConfig.ListField( 

224 doc="Wavelength range for LUT (Angstrom)", 

225 dtype=float, 

226 default=[3000.0, 11000.0], 

227 ) 

228 

229 

230class FgcmMakeLutConfig(pipeBase.PipelineTaskConfig, 

231 pipelineConnections=FgcmMakeLutConnections): 

232 """Config for FgcmMakeLutTask""" 

233 

234 filterNames = pexConfig.ListField( 

235 doc="Filter names to build LUT ('short' names)", 

236 dtype=str, 

237 default=[], 

238 deprecated=("This field is no longer used, and has been deprecated by " 

239 "DM-28088. It will be removed after v22. Use " 

240 "stdPhysicalFilterMap instead.") 

241 ) 

242 stdFilterNames = pexConfig.ListField( 

243 doc=("Standard filterNames ('short' names). " 

244 "Each filter in filterName will be calibrated to a matched " 

245 "stdFilterName. In regular usage, one has g->g, r->r, ... " 

246 "In the case of HSC, one would have g->g, r->r2, r2->r2, ... " 

247 "which allows replacement (or time-variable) filters to be " 

248 "properly cross-calibrated."), 

249 dtype=str, 

250 default=[], 

251 deprecated=("This field is no longer used, and has been deprecated by " 

252 "DM-28088. It will be removed after v22. Use " 

253 "stdPhysicalFilterMap instead.") 

254 ) 

255 physicalFilters = pexConfig.ListField( 

256 doc="List of physicalFilter labels to generate look-up table.", 

257 dtype=str, 

258 default=[], 

259 ) 

260 stdPhysicalFilterOverrideMap = pexConfig.DictField( 

261 doc=("Override mapping from physical filter labels to 'standard' physical " 

262 "filter labels. The 'standard' physical filter defines the transmission " 

263 "curve that the FGCM standard bandpass will be based on. " 

264 "Any filter not listed here will be mapped to " 

265 "itself (e.g. g->g or HSC-G->HSC-G). Use this override for cross-" 

266 "filter calibration such as HSC-R->HSC-R2 and HSC-I->HSC-I2."), 

267 keytype=str, 

268 itemtype=str, 

269 default={}, 

270 ) 

271 atmosphereTableName = pexConfig.Field( 

272 doc="FGCM name or filename of precomputed atmospheres", 

273 dtype=str, 

274 default=None, 

275 optional=True, 

276 ) 

277 parameters = pexConfig.ConfigField( 

278 doc="Atmosphere parameters (required if no atmosphereTableName)", 

279 dtype=FgcmMakeLutParametersConfig, 

280 default=None, 

281 check=None) 

282 

283 def validate(self): 

284 """ 

285 Validate the config parameters. 

286 

287 This method behaves differently from the parent validate in the case 

288 that atmosphereTableName is set. In this case, the config values 

289 for standard values, step sizes, and ranges are loaded 

290 directly from the specified atmosphereTableName. 

291 """ 

292 # check that filterNames and stdFilterNames are okay 

293 self._fields['physicalFilters'].validate(self) 

294 self._fields['stdPhysicalFilterOverrideMap'].validate(self) 

295 

296 if self.atmosphereTableName is None: 

297 # Validate the parameters 

298 self._fields['parameters'].validate(self) 

299 

300 

301class FgcmMakeLutRunner(pipeBase.ButlerInitializedTaskRunner): 

302 """Subclass of TaskRunner for fgcmMakeLutTask 

303 

304 fgcmMakeLutTask.run() takes one argument, the butler, and 

305 does not run on any data in the repository. 

306 This runner does not use any parallelization. 

307 """ 

308 

309 @staticmethod 

310 def getTargetList(parsedCmd): 

311 """ 

312 Return a list with one element, the butler. 

313 """ 

314 return [parsedCmd.butler] 

315 

316 def __call__(self, butler): 

317 """ 

318 Parameters 

319 ---------- 

320 butler: `lsst.daf.persistence.Butler` 

321 

322 Returns 

323 ------- 

324 exitStatus: `list` with `pipeBase.Struct` 

325 exitStatus (0: success; 1: failure) 

326 """ 

327 task = self.TaskClass(config=self.config, log=self.log) 

328 

329 exitStatus = 0 

330 if self.doRaise: 

331 task.runDataRef(butler) 

332 else: 

333 try: 

334 task.runDataRef(butler) 

335 except Exception as e: 

336 exitStatus = 1 

337 task.log.fatal("Failed: %s" % e) 

338 if not isinstance(e, pipeBase.TaskError): 

339 traceback.print_exc(file=sys.stderr) 

340 

341 task.writeMetadata(butler) 

342 

343 # The task does not return any results: 

344 return [pipeBase.Struct(exitStatus=exitStatus)] 

345 

346 def run(self, parsedCmd): 

347 """ 

348 Run the task, with no multiprocessing 

349 

350 Parameters 

351 ---------- 

352 parsedCmd: ArgumentParser parsed command line 

353 """ 

354 

355 resultList = [] 

356 

357 if self.precall(parsedCmd): 

358 targetList = self.getTargetList(parsedCmd) 

359 # make sure that we only get 1 

360 resultList = self(targetList[0]) 

361 

362 return resultList 

363 

364 

365class FgcmMakeLutTask(pipeBase.PipelineTask, pipeBase.CmdLineTask): 

366 """ 

367 Make Look-Up Table for FGCM. 

368 

369 This task computes a look-up-table for the range in expected atmosphere 

370 variation and variation in instrumental throughput (as tracked by the 

371 transmission_filter products). By pre-computing linearized integrals, 

372 the FGCM fit is orders of magnitude faster for stars with a broad range 

373 of colors and observing bands, yielding precision at the 1-2 mmag level. 

374 

375 Computing a LUT requires running MODTRAN or with a pre-generated 

376 atmosphere table packaged with fgcm. 

377 """ 

378 

379 ConfigClass = FgcmMakeLutConfig 

380 RunnerClass = FgcmMakeLutRunner 

381 _DefaultName = "fgcmMakeLut" 

382 

383 def __init__(self, butler=None, initInputs=None, **kwargs): 

384 super().__init__(**kwargs) 

385 

386 # no saving of metadata for now 

387 def _getMetadataName(self): 

388 return None 

389 

390 @pipeBase.timeMethod 

391 def runDataRef(self, butler): 

392 """ 

393 Make a Look-Up Table for FGCM 

394 

395 Parameters 

396 ---------- 

397 butler: `lsst.daf.persistence.Butler` 

398 

399 Raises 

400 ------ 

401 ValueError : Raised if configured filter name does not match any of the 

402 available filter transmission curves. 

403 """ 

404 camera = butler.get('camera') 

405 opticsDataRef = butler.dataRef('transmission_optics') 

406 

407 sensorDataRefDict = {} 

408 for detector in camera: 

409 sensorDataRefDict[detector.getId()] = butler.dataRef('transmission_sensor', 

410 dataId={'ccd': detector.getId()}) 

411 

412 filterDataRefDict = {} 

413 for physicalFilter in self.config.physicalFilters: 

414 # The physical filters map directly to dataId filter names 

415 # for gen2 HSC. This is the only camera that will be supported 

416 # by Gen2 fgcmcal, so we do not need to worry about other cases. 

417 dataRef = butler.dataRef('transmission_filter', filter=physicalFilter) 

418 if not dataRef.datasetExists(): 

419 raise ValueError(f"Could not find transmission for filter {physicalFilter}.") 

420 filterDataRefDict[physicalFilter] = dataRef 

421 

422 lutCat = self._fgcmMakeLut(camera, 

423 opticsDataRef, 

424 sensorDataRefDict, 

425 filterDataRefDict) 

426 butler.put(lutCat, 'fgcmLookUpTable') 

427 

428 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

429 camera = butlerQC.get(inputRefs.camera) 

430 

431 # Instantiate the instrument to load filter information 

432 _ = Instrument.fromName(inputRefs.camera.dataId['instrument'], 

433 butlerQC.registry) 

434 opticsDataRef = butlerQC.get(inputRefs.transmission_optics) 

435 

436 sensorRefs = butlerQC.get(inputRefs.transmission_sensor) 

437 sensorDataRefDict = {sensorRef.dataId.byName()['detector']: sensorRef for 

438 sensorRef in sensorRefs} 

439 

440 filterRefs = butlerQC.get(inputRefs.transmission_filter) 

441 filterDataRefDict = {filterRef.dataId['physical_filter']: filterRef for 

442 filterRef in filterRefs} 

443 

444 lutCat = self._fgcmMakeLut(camera, 

445 opticsDataRef, 

446 sensorDataRefDict, 

447 filterDataRefDict) 

448 butlerQC.put(lutCat, outputRefs.fgcmLookUpTable) 

449 

450 def _fgcmMakeLut(self, camera, opticsDataRef, sensorDataRefDict, 

451 filterDataRefDict): 

452 """ 

453 Make a FGCM Look-up Table 

454 

455 Parameters 

456 ---------- 

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

458 Camera from the butler. 

459 opticsDataRef : `lsst.daf.persistence.ButlerDataRef` or 

460 `lsst.daf.butler.DeferredDatasetHandle` 

461 Reference to optics transmission curve. 

462 sensorDataRefDict : `dict` of [`int`, `lsst.daf.persistence.ButlerDataRef` or 

463 `lsst.daf.butler.DeferredDatasetHandle`] 

464 Dictionary of references to sensor transmission curves. Key will 

465 be detector id. 

466 filterDataRefDict : `dict` of [`str`, `lsst.daf.persistence.ButlerDataRef` or 

467 `lsst.daf.butler.DeferredDatasetHandle`] 

468 Dictionary of references to filter transmission curves. Key will 

469 be physical filter label. 

470 

471 Returns 

472 ------- 

473 fgcmLookUpTable : `BaseCatalog` 

474 The FGCM look-up table. 

475 """ 

476 # number of ccds from the length of the camera iterator 

477 nCcd = len(camera) 

478 self.log.info("Found %d ccds for look-up table" % (nCcd)) 

479 

480 # Load in optics, etc. 

481 self._loadThroughputs(camera, 

482 opticsDataRef, 

483 sensorDataRefDict, 

484 filterDataRefDict) 

485 

486 lutConfig = self._createLutConfig(nCcd) 

487 

488 # make the lut object 

489 self.log.info("Making the LUT maker object") 

490 self.fgcmLutMaker = fgcm.FgcmLUTMaker(lutConfig) 

491 

492 # generate the throughput dictionary. 

493 

494 # these will be in Angstroms 

495 # note that lambdaStep is currently in nm, because of historical 

496 # reasons in the code. Convert to Angstroms here. 

497 throughputLambda = np.arange(self.fgcmLutMaker.lambdaRange[0], 

498 self.fgcmLutMaker.lambdaRange[1]+self.fgcmLutMaker.lambdaStep*10, 

499 self.fgcmLutMaker.lambdaStep*10.) 

500 

501 self.log.info("Built throughput lambda, %.1f-%.1f, step %.2f" % 

502 (throughputLambda[0], throughputLambda[-1], 

503 throughputLambda[1] - throughputLambda[0])) 

504 

505 throughputDict = {} 

506 for i, physicalFilter in enumerate(self.config.physicalFilters): 

507 tDict = {} 

508 tDict['LAMBDA'] = throughputLambda 

509 for ccdIndex, detector in enumerate(camera): 

510 tDict[ccdIndex] = self._getThroughputDetector(detector, physicalFilter, throughputLambda) 

511 throughputDict[physicalFilter] = tDict 

512 

513 # set the throughputs 

514 self.fgcmLutMaker.setThroughputs(throughputDict) 

515 

516 # make the LUT 

517 self.log.info("Making LUT") 

518 self.fgcmLutMaker.makeLUT() 

519 

520 # and save the LUT 

521 

522 # build the index values 

523 comma = ',' 

524 physicalFilterString = comma.join(self.config.physicalFilters) 

525 stdPhysicalFilterString = comma.join(self._getStdPhysicalFilterList()) 

526 

527 atmosphereTableName = 'NoTableWasUsed' 

528 if self.config.atmosphereTableName is not None: 

529 atmosphereTableName = self.config.atmosphereTableName 

530 

531 lutSchema = self._makeLutSchema(physicalFilterString, stdPhysicalFilterString, 

532 atmosphereTableName) 

533 

534 lutCat = self._makeLutCat(lutSchema, physicalFilterString, 

535 stdPhysicalFilterString, atmosphereTableName) 

536 return lutCat 

537 

538 def _getStdPhysicalFilterList(self): 

539 """Get the standard physical filter lists from config.physicalFilters 

540 and config.stdPhysicalFilterOverrideMap 

541 

542 Returns 

543 ------- 

544 stdPhysicalFilters : `list` 

545 """ 

546 override = self.config.stdPhysicalFilterOverrideMap 

547 return [override.get(physicalFilter, physicalFilter) for 

548 physicalFilter in self.config.physicalFilters] 

549 

550 def _createLutConfig(self, nCcd): 

551 """ 

552 Create the fgcmLut config dictionary 

553 

554 Parameters 

555 ---------- 

556 nCcd: `int` 

557 Number of CCDs in the camera 

558 """ 

559 

560 # create the common stub of the lutConfig 

561 lutConfig = {} 

562 lutConfig['logger'] = self.log 

563 lutConfig['filterNames'] = self.config.physicalFilters 

564 lutConfig['stdFilterNames'] = self._getStdPhysicalFilterList() 

565 lutConfig['nCCD'] = nCcd 

566 

567 # atmosphereTable already validated if available 

568 if self.config.atmosphereTableName is not None: 

569 lutConfig['atmosphereTableName'] = self.config.atmosphereTableName 

570 else: 

571 # use the regular paramters (also validated if needed) 

572 lutConfig['elevation'] = self.config.parameters.elevation 

573 lutConfig['pmbRange'] = self.config.parameters.pmbRange 

574 lutConfig['pmbSteps'] = self.config.parameters.pmbSteps 

575 lutConfig['pwvRange'] = self.config.parameters.pwvRange 

576 lutConfig['pwvSteps'] = self.config.parameters.pwvSteps 

577 lutConfig['o3Range'] = self.config.parameters.o3Range 

578 lutConfig['o3Steps'] = self.config.parameters.o3Steps 

579 lutConfig['tauRange'] = self.config.parameters.tauRange 

580 lutConfig['tauSteps'] = self.config.parameters.tauSteps 

581 lutConfig['alphaRange'] = self.config.parameters.alphaRange 

582 lutConfig['alphaSteps'] = self.config.parameters.alphaSteps 

583 lutConfig['zenithRange'] = self.config.parameters.zenithRange 

584 lutConfig['zenithSteps'] = self.config.parameters.zenithSteps 

585 lutConfig['pmbStd'] = self.config.parameters.pmbStd 

586 lutConfig['pwvStd'] = self.config.parameters.pwvStd 

587 lutConfig['o3Std'] = self.config.parameters.o3Std 

588 lutConfig['tauStd'] = self.config.parameters.tauStd 

589 lutConfig['alphaStd'] = self.config.parameters.alphaStd 

590 lutConfig['airmassStd'] = self.config.parameters.airmassStd 

591 lutConfig['lambdaRange'] = self.config.parameters.lambdaRange 

592 lutConfig['lambdaStep'] = self.config.parameters.lambdaStep 

593 lutConfig['lambdaNorm'] = self.config.parameters.lambdaNorm 

594 

595 return lutConfig 

596 

597 def _loadThroughputs(self, camera, opticsDataRef, sensorDataRefDict, filterDataRefDict): 

598 """Internal method to load throughput data for filters 

599 

600 Parameters 

601 ---------- 

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

603 Camera from the butler 

604 opticsDataRef : `lsst.daf.persistence.ButlerDataRef` or 

605 `lsst.daf.butler.DeferredDatasetHandle` 

606 Reference to optics transmission curve. 

607 sensorDataRefDict : `dict` of [`int`, `lsst.daf.persistence.ButlerDataRef` or 

608 `lsst.daf.butler.DeferredDatasetHandle`] 

609 Dictionary of references to sensor transmission curves. Key will 

610 be detector id. 

611 filterDataRefDict : `dict` of [`str`, `lsst.daf.persistence.ButlerDataRef` or 

612 `lsst.daf.butler.DeferredDatasetHandle`] 

613 Dictionary of references to filter transmission curves. Key will 

614 be physical filter label. 

615 

616 Raises 

617 ------ 

618 ValueError : Raised if configured filter name does not match any of the 

619 available filter transmission curves. 

620 """ 

621 self._opticsTransmission = opticsDataRef.get() 

622 

623 self._sensorsTransmission = {} 

624 for detector in camera: 

625 self._sensorsTransmission[detector.getId()] = sensorDataRefDict[detector.getId()].get() 

626 

627 self._filtersTransmission = {} 

628 for physicalFilter in self.config.physicalFilters: 

629 self._filtersTransmission[physicalFilter] = filterDataRefDict[physicalFilter].get() 

630 

631 def _getThroughputDetector(self, detector, physicalFilter, throughputLambda): 

632 """Internal method to get throughput for a detector. 

633 

634 Returns the throughput at the center of the detector for a given filter. 

635 

636 Parameters 

637 ---------- 

638 detector: `lsst.afw.cameraGeom._detector.Detector` 

639 Detector on camera 

640 physicalFilter: `str` 

641 Physical filter label 

642 throughputLambda: `np.array(dtype=np.float64)` 

643 Wavelength steps (Angstrom) 

644 

645 Returns 

646 ------- 

647 throughput: `np.array(dtype=np.float64)` 

648 Throughput (max 1.0) at throughputLambda 

649 """ 

650 

651 c = detector.getCenter(afwCameraGeom.FOCAL_PLANE) 

652 c.scale(1.0/detector.getPixelSize()[0]) # Assumes x and y pixel sizes in arcsec are the same 

653 

654 throughput = self._opticsTransmission.sampleAt(position=c, 

655 wavelengths=throughputLambda) 

656 

657 throughput *= self._sensorsTransmission[detector.getId()].sampleAt(position=c, 

658 wavelengths=throughputLambda) 

659 

660 throughput *= self._filtersTransmission[physicalFilter].sampleAt(position=c, 

661 wavelengths=throughputLambda) 

662 

663 # Clip the throughput from 0 to 1 

664 throughput = np.clip(throughput, 0.0, 1.0) 

665 

666 return throughput 

667 

668 def _makeLutSchema(self, physicalFilterString, stdPhysicalFilterString, 

669 atmosphereTableName): 

670 """ 

671 Make the LUT schema 

672 

673 Parameters 

674 ---------- 

675 physicalFilterString: `str` 

676 Combined string of all the physicalFilters 

677 stdPhysicalFilterString: `str` 

678 Combined string of all the standard physicalFilters 

679 atmosphereTableName: `str` 

680 Name of the atmosphere table used to generate LUT 

681 

682 Returns 

683 ------- 

684 lutSchema: `afwTable.schema` 

685 """ 

686 

687 lutSchema = afwTable.Schema() 

688 

689 lutSchema.addField('tablename', type=str, doc='Atmosphere table name', 

690 size=len(atmosphereTableName)) 

691 lutSchema.addField('elevation', type=float, doc="Telescope elevation used for LUT") 

692 lutSchema.addField('physicalFilters', type=str, doc='physicalFilters in LUT', 

693 size=len(physicalFilterString)) 

694 lutSchema.addField('stdPhysicalFilters', type=str, doc='Standard physicalFilters in LUT', 

695 size=len(stdPhysicalFilterString)) 

696 lutSchema.addField('pmb', type='ArrayD', doc='Barometric Pressure', 

697 size=self.fgcmLutMaker.pmb.size) 

698 lutSchema.addField('pmbFactor', type='ArrayD', doc='PMB scaling factor', 

699 size=self.fgcmLutMaker.pmb.size) 

700 lutSchema.addField('pmbElevation', type=np.float64, doc='PMB Scaling at elevation') 

701 lutSchema.addField('pwv', type='ArrayD', doc='Preciptable Water Vapor', 

702 size=self.fgcmLutMaker.pwv.size) 

703 lutSchema.addField('o3', type='ArrayD', doc='Ozone', 

704 size=self.fgcmLutMaker.o3.size) 

705 lutSchema.addField('tau', type='ArrayD', doc='Aerosol optical depth', 

706 size=self.fgcmLutMaker.tau.size) 

707 lutSchema.addField('lambdaNorm', type=np.float64, doc='AOD wavelength') 

708 lutSchema.addField('alpha', type='ArrayD', doc='Aerosol alpha', 

709 size=self.fgcmLutMaker.alpha.size) 

710 lutSchema.addField('zenith', type='ArrayD', doc='Zenith angle', 

711 size=self.fgcmLutMaker.zenith.size) 

712 lutSchema.addField('nCcd', type=np.int32, doc='Number of CCDs') 

713 

714 # and the standard values 

715 lutSchema.addField('pmbStd', type=np.float64, doc='PMB Standard') 

716 lutSchema.addField('pwvStd', type=np.float64, doc='PWV Standard') 

717 lutSchema.addField('o3Std', type=np.float64, doc='O3 Standard') 

718 lutSchema.addField('tauStd', type=np.float64, doc='Tau Standard') 

719 lutSchema.addField('alphaStd', type=np.float64, doc='Alpha Standard') 

720 lutSchema.addField('zenithStd', type=np.float64, doc='Zenith angle Standard') 

721 lutSchema.addField('lambdaRange', type='ArrayD', doc='Wavelength range', 

722 size=2) 

723 lutSchema.addField('lambdaStep', type=np.float64, doc='Wavelength step') 

724 lutSchema.addField('lambdaStd', type='ArrayD', doc='Standard Wavelength', 

725 size=len(self.fgcmLutMaker.filterNames)) 

726 lutSchema.addField('lambdaStdFilter', type='ArrayD', doc='Standard Wavelength (raw)', 

727 size=len(self.fgcmLutMaker.filterNames)) 

728 lutSchema.addField('i0Std', type='ArrayD', doc='I0 Standard', 

729 size=len(self.fgcmLutMaker.filterNames)) 

730 lutSchema.addField('i1Std', type='ArrayD', doc='I1 Standard', 

731 size=len(self.fgcmLutMaker.filterNames)) 

732 lutSchema.addField('i10Std', type='ArrayD', doc='I10 Standard', 

733 size=len(self.fgcmLutMaker.filterNames)) 

734 lutSchema.addField('i2Std', type='ArrayD', doc='I2 Standard', 

735 size=len(self.fgcmLutMaker.filterNames)) 

736 lutSchema.addField('lambdaB', type='ArrayD', doc='Wavelength for passband (no atm)', 

737 size=len(self.fgcmLutMaker.filterNames)) 

738 lutSchema.addField('atmLambda', type='ArrayD', doc='Atmosphere wavelengths (Angstrom)', 

739 size=self.fgcmLutMaker.atmLambda.size) 

740 lutSchema.addField('atmStdTrans', type='ArrayD', doc='Standard Atmosphere Throughput', 

741 size=self.fgcmLutMaker.atmStdTrans.size) 

742 

743 # and the look-up-tables 

744 lutSchema.addField('luttype', type=str, size=20, doc='Look-up table type') 

745 lutSchema.addField('lut', type='ArrayF', doc='Look-up table for luttype', 

746 size=self.fgcmLutMaker.lut['I0'].size) 

747 

748 return lutSchema 

749 

750 def _makeLutCat(self, lutSchema, physicalFilterString, stdPhysicalFilterString, 

751 atmosphereTableName): 

752 """ 

753 Make the LUT schema 

754 

755 Parameters 

756 ---------- 

757 lutSchema: `afwTable.schema` 

758 Lut catalog schema 

759 physicalFilterString: `str` 

760 Combined string of all the physicalFilters 

761 stdPhysicalFilterString: `str` 

762 Combined string of all the standard physicalFilters 

763 atmosphereTableName: `str` 

764 Name of the atmosphere table used to generate LUT 

765 

766 Returns 

767 ------- 

768 lutCat: `afwTable.BaseCatalog` 

769 Lut catalog for persistence 

770 """ 

771 

772 # The somewhat strange format is to make sure that 

773 # the rows of the afwTable do not get too large 

774 # (see DM-11419) 

775 

776 lutCat = afwTable.BaseCatalog(lutSchema) 

777 lutCat.table.preallocate(14) 

778 

779 # first fill the first index 

780 rec = lutCat.addNew() 

781 

782 rec['tablename'] = atmosphereTableName 

783 rec['elevation'] = self.fgcmLutMaker.atmosphereTable.elevation 

784 rec['physicalFilters'] = physicalFilterString 

785 rec['stdPhysicalFilters'] = stdPhysicalFilterString 

786 rec['pmb'][:] = self.fgcmLutMaker.pmb 

787 rec['pmbFactor'][:] = self.fgcmLutMaker.pmbFactor 

788 rec['pmbElevation'] = self.fgcmLutMaker.pmbElevation 

789 rec['pwv'][:] = self.fgcmLutMaker.pwv 

790 rec['o3'][:] = self.fgcmLutMaker.o3 

791 rec['tau'][:] = self.fgcmLutMaker.tau 

792 rec['lambdaNorm'] = self.fgcmLutMaker.lambdaNorm 

793 rec['alpha'][:] = self.fgcmLutMaker.alpha 

794 rec['zenith'][:] = self.fgcmLutMaker.zenith 

795 rec['nCcd'] = self.fgcmLutMaker.nCCD 

796 

797 rec['pmbStd'] = self.fgcmLutMaker.pmbStd 

798 rec['pwvStd'] = self.fgcmLutMaker.pwvStd 

799 rec['o3Std'] = self.fgcmLutMaker.o3Std 

800 rec['tauStd'] = self.fgcmLutMaker.tauStd 

801 rec['alphaStd'] = self.fgcmLutMaker.alphaStd 

802 rec['zenithStd'] = self.fgcmLutMaker.zenithStd 

803 rec['lambdaRange'][:] = self.fgcmLutMaker.lambdaRange 

804 rec['lambdaStep'] = self.fgcmLutMaker.lambdaStep 

805 rec['lambdaStd'][:] = self.fgcmLutMaker.lambdaStd 

806 rec['lambdaStdFilter'][:] = self.fgcmLutMaker.lambdaStdFilter 

807 rec['i0Std'][:] = self.fgcmLutMaker.I0Std 

808 rec['i1Std'][:] = self.fgcmLutMaker.I1Std 

809 rec['i10Std'][:] = self.fgcmLutMaker.I10Std 

810 rec['i2Std'][:] = self.fgcmLutMaker.I2Std 

811 rec['lambdaB'][:] = self.fgcmLutMaker.lambdaB 

812 rec['atmLambda'][:] = self.fgcmLutMaker.atmLambda 

813 rec['atmStdTrans'][:] = self.fgcmLutMaker.atmStdTrans 

814 

815 rec['luttype'] = 'I0' 

816 rec['lut'][:] = self.fgcmLutMaker.lut['I0'].flatten() 

817 

818 # and add the rest 

819 rec = lutCat.addNew() 

820 rec['luttype'] = 'I1' 

821 rec['lut'][:] = self.fgcmLutMaker.lut['I1'].flatten() 

822 

823 derivTypes = ['D_PMB', 'D_LNPWV', 'D_O3', 'D_LNTAU', 'D_ALPHA', 'D_SECZENITH', 

824 'D_PMB_I1', 'D_LNPWV_I1', 'D_O3_I1', 'D_LNTAU_I1', 'D_ALPHA_I1', 

825 'D_SECZENITH_I1'] 

826 for derivType in derivTypes: 

827 rec = lutCat.addNew() 

828 rec['luttype'] = derivType 

829 rec['lut'][:] = self.fgcmLutMaker.lutDeriv[derivType].flatten() 

830 

831 return lutCat