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 physicalFilters = pexConfig.ListField( 

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

235 dtype=str, 

236 default=[], 

237 ) 

238 stdPhysicalFilterOverrideMap = pexConfig.DictField( 

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

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

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

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

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

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

245 keytype=str, 

246 itemtype=str, 

247 default={}, 

248 ) 

249 atmosphereTableName = pexConfig.Field( 

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

251 dtype=str, 

252 default=None, 

253 optional=True, 

254 ) 

255 parameters = pexConfig.ConfigField( 

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

257 dtype=FgcmMakeLutParametersConfig, 

258 default=None, 

259 check=None) 

260 

261 def validate(self): 

262 """ 

263 Validate the config parameters. 

264 

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

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

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

268 directly from the specified atmosphereTableName. 

269 """ 

270 # check that filterNames and stdFilterNames are okay 

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

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

273 

274 if self.atmosphereTableName is None: 

275 # Validate the parameters 

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

277 

278 

279class FgcmMakeLutRunner(pipeBase.ButlerInitializedTaskRunner): 

280 """Subclass of TaskRunner for fgcmMakeLutTask 

281 

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

283 does not run on any data in the repository. 

284 This runner does not use any parallelization. 

285 """ 

286 

287 @staticmethod 

288 def getTargetList(parsedCmd): 

289 """ 

290 Return a list with one element, the butler. 

291 """ 

292 return [parsedCmd.butler] 

293 

294 def __call__(self, butler): 

295 """ 

296 Parameters 

297 ---------- 

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

299 

300 Returns 

301 ------- 

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

303 exitStatus (0: success; 1: failure) 

304 """ 

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

306 

307 exitStatus = 0 

308 if self.doRaise: 

309 task.runDataRef(butler) 

310 else: 

311 try: 

312 task.runDataRef(butler) 

313 except Exception as e: 

314 exitStatus = 1 

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

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

317 traceback.print_exc(file=sys.stderr) 

318 

319 task.writeMetadata(butler) 

320 

321 # The task does not return any results: 

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

323 

324 def run(self, parsedCmd): 

325 """ 

326 Run the task, with no multiprocessing 

327 

328 Parameters 

329 ---------- 

330 parsedCmd: ArgumentParser parsed command line 

331 """ 

332 

333 resultList = [] 

334 

335 if self.precall(parsedCmd): 

336 targetList = self.getTargetList(parsedCmd) 

337 # make sure that we only get 1 

338 resultList = self(targetList[0]) 

339 

340 return resultList 

341 

342 

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

344 """ 

345 Make Look-Up Table for FGCM. 

346 

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

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

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

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

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

352 

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

354 atmosphere table packaged with fgcm. 

355 """ 

356 

357 ConfigClass = FgcmMakeLutConfig 

358 RunnerClass = FgcmMakeLutRunner 

359 _DefaultName = "fgcmMakeLut" 

360 

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

362 super().__init__(**kwargs) 

363 

364 # no saving of metadata for now 

365 def _getMetadataName(self): 

366 return None 

367 

368 @pipeBase.timeMethod 

369 def runDataRef(self, butler): 

370 """ 

371 Make a Look-Up Table for FGCM 

372 

373 Parameters 

374 ---------- 

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

376 

377 Raises 

378 ------ 

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

380 available filter transmission curves. 

381 """ 

382 camera = butler.get('camera') 

383 opticsDataRef = butler.dataRef('transmission_optics') 

384 

385 sensorDataRefDict = {} 

386 for detector in camera: 

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

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

389 

390 filterDataRefDict = {} 

391 for physicalFilter in self.config.physicalFilters: 

392 # The physical filters map directly to dataId filter names 

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

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

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

396 if not dataRef.datasetExists(): 

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

398 filterDataRefDict[physicalFilter] = dataRef 

399 

400 lutCat = self._fgcmMakeLut(camera, 

401 opticsDataRef, 

402 sensorDataRefDict, 

403 filterDataRefDict) 

404 butler.put(lutCat, 'fgcmLookUpTable') 

405 

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

407 camera = butlerQC.get(inputRefs.camera) 

408 

409 # Instantiate the instrument to load filter information 

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

411 butlerQC.registry) 

412 opticsDataRef = butlerQC.get(inputRefs.transmission_optics) 

413 

414 sensorRefs = butlerQC.get(inputRefs.transmission_sensor) 

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

416 sensorRef in sensorRefs} 

417 

418 filterRefs = butlerQC.get(inputRefs.transmission_filter) 

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

420 filterRef in filterRefs} 

421 

422 lutCat = self._fgcmMakeLut(camera, 

423 opticsDataRef, 

424 sensorDataRefDict, 

425 filterDataRefDict) 

426 butlerQC.put(lutCat, outputRefs.fgcmLookUpTable) 

427 

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

429 filterDataRefDict): 

430 """ 

431 Make a FGCM Look-up Table 

432 

433 Parameters 

434 ---------- 

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

436 Camera from the butler. 

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

438 `lsst.daf.butler.DeferredDatasetHandle` 

439 Reference to optics transmission curve. 

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

441 `lsst.daf.butler.DeferredDatasetHandle`] 

442 Dictionary of references to sensor transmission curves. Key will 

443 be detector id. 

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

445 `lsst.daf.butler.DeferredDatasetHandle`] 

446 Dictionary of references to filter transmission curves. Key will 

447 be physical filter label. 

448 

449 Returns 

450 ------- 

451 fgcmLookUpTable : `BaseCatalog` 

452 The FGCM look-up table. 

453 """ 

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

455 nCcd = len(camera) 

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

457 

458 # Load in optics, etc. 

459 self._loadThroughputs(camera, 

460 opticsDataRef, 

461 sensorDataRefDict, 

462 filterDataRefDict) 

463 

464 lutConfig = self._createLutConfig(nCcd) 

465 

466 # make the lut object 

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

468 self.fgcmLutMaker = fgcm.FgcmLUTMaker(lutConfig) 

469 

470 # generate the throughput dictionary. 

471 

472 # these will be in Angstroms 

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

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

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

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

477 self.fgcmLutMaker.lambdaStep*10.) 

478 

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

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

481 throughputLambda[1] - throughputLambda[0])) 

482 

483 throughputDict = {} 

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

485 tDict = {} 

486 tDict['LAMBDA'] = throughputLambda 

487 for ccdIndex, detector in enumerate(camera): 

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

489 throughputDict[physicalFilter] = tDict 

490 

491 # set the throughputs 

492 self.fgcmLutMaker.setThroughputs(throughputDict) 

493 

494 # make the LUT 

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

496 self.fgcmLutMaker.makeLUT() 

497 

498 # and save the LUT 

499 

500 # build the index values 

501 comma = ',' 

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

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

504 

505 atmosphereTableName = 'NoTableWasUsed' 

506 if self.config.atmosphereTableName is not None: 

507 atmosphereTableName = self.config.atmosphereTableName 

508 

509 lutSchema = self._makeLutSchema(physicalFilterString, stdPhysicalFilterString, 

510 atmosphereTableName) 

511 

512 lutCat = self._makeLutCat(lutSchema, physicalFilterString, 

513 stdPhysicalFilterString, atmosphereTableName) 

514 return lutCat 

515 

516 def _getStdPhysicalFilterList(self): 

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

518 and config.stdPhysicalFilterOverrideMap 

519 

520 Returns 

521 ------- 

522 stdPhysicalFilters : `list` 

523 """ 

524 override = self.config.stdPhysicalFilterOverrideMap 

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

526 physicalFilter in self.config.physicalFilters] 

527 

528 def _createLutConfig(self, nCcd): 

529 """ 

530 Create the fgcmLut config dictionary 

531 

532 Parameters 

533 ---------- 

534 nCcd: `int` 

535 Number of CCDs in the camera 

536 """ 

537 

538 # create the common stub of the lutConfig 

539 lutConfig = {} 

540 lutConfig['logger'] = self.log 

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

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

543 lutConfig['nCCD'] = nCcd 

544 

545 # atmosphereTable already validated if available 

546 if self.config.atmosphereTableName is not None: 

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

548 else: 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

572 

573 return lutConfig 

574 

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

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

577 

578 Parameters 

579 ---------- 

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

581 Camera from the butler 

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

583 `lsst.daf.butler.DeferredDatasetHandle` 

584 Reference to optics transmission curve. 

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

586 `lsst.daf.butler.DeferredDatasetHandle`] 

587 Dictionary of references to sensor transmission curves. Key will 

588 be detector id. 

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

590 `lsst.daf.butler.DeferredDatasetHandle`] 

591 Dictionary of references to filter transmission curves. Key will 

592 be physical filter label. 

593 

594 Raises 

595 ------ 

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

597 available filter transmission curves. 

598 """ 

599 self._opticsTransmission = opticsDataRef.get() 

600 

601 self._sensorsTransmission = {} 

602 for detector in camera: 

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

604 

605 self._filtersTransmission = {} 

606 for physicalFilter in self.config.physicalFilters: 

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

608 

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

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

611 

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

613 

614 Parameters 

615 ---------- 

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

617 Detector on camera 

618 physicalFilter: `str` 

619 Physical filter label 

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

621 Wavelength steps (Angstrom) 

622 

623 Returns 

624 ------- 

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

626 Throughput (max 1.0) at throughputLambda 

627 """ 

628 

629 c = detector.getCenter(afwCameraGeom.FOCAL_PLANE) 

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

631 

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

633 wavelengths=throughputLambda) 

634 

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

636 wavelengths=throughputLambda) 

637 

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

639 wavelengths=throughputLambda) 

640 

641 # Clip the throughput from 0 to 1 

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

643 

644 return throughput 

645 

646 def _makeLutSchema(self, physicalFilterString, stdPhysicalFilterString, 

647 atmosphereTableName): 

648 """ 

649 Make the LUT schema 

650 

651 Parameters 

652 ---------- 

653 physicalFilterString: `str` 

654 Combined string of all the physicalFilters 

655 stdPhysicalFilterString: `str` 

656 Combined string of all the standard physicalFilters 

657 atmosphereTableName: `str` 

658 Name of the atmosphere table used to generate LUT 

659 

660 Returns 

661 ------- 

662 lutSchema: `afwTable.schema` 

663 """ 

664 

665 lutSchema = afwTable.Schema() 

666 

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

668 size=len(atmosphereTableName)) 

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

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

671 size=len(physicalFilterString)) 

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

673 size=len(stdPhysicalFilterString)) 

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

675 size=self.fgcmLutMaker.pmb.size) 

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

677 size=self.fgcmLutMaker.pmb.size) 

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

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

680 size=self.fgcmLutMaker.pwv.size) 

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

682 size=self.fgcmLutMaker.o3.size) 

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

684 size=self.fgcmLutMaker.tau.size) 

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

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

687 size=self.fgcmLutMaker.alpha.size) 

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

689 size=self.fgcmLutMaker.zenith.size) 

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

691 

692 # and the standard values 

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

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

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

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

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

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

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

700 size=2) 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

717 size=self.fgcmLutMaker.atmLambda.size) 

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

719 size=self.fgcmLutMaker.atmStdTrans.size) 

720 

721 # and the look-up-tables 

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

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

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

725 

726 return lutSchema 

727 

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

729 atmosphereTableName): 

730 """ 

731 Make the LUT schema 

732 

733 Parameters 

734 ---------- 

735 lutSchema: `afwTable.schema` 

736 Lut catalog schema 

737 physicalFilterString: `str` 

738 Combined string of all the physicalFilters 

739 stdPhysicalFilterString: `str` 

740 Combined string of all the standard physicalFilters 

741 atmosphereTableName: `str` 

742 Name of the atmosphere table used to generate LUT 

743 

744 Returns 

745 ------- 

746 lutCat: `afwTable.BaseCatalog` 

747 Lut catalog for persistence 

748 """ 

749 

750 # The somewhat strange format is to make sure that 

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

752 # (see DM-11419) 

753 

754 lutCat = afwTable.BaseCatalog(lutSchema) 

755 lutCat.table.preallocate(14) 

756 

757 # first fill the first index 

758 rec = lutCat.addNew() 

759 

760 rec['tablename'] = atmosphereTableName 

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

762 rec['physicalFilters'] = physicalFilterString 

763 rec['stdPhysicalFilters'] = stdPhysicalFilterString 

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

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

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

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

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

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

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

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

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

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

774 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

792 

793 rec['luttype'] = 'I0' 

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

795 

796 # and add the rest 

797 rec = lutCat.addNew() 

798 rec['luttype'] = 'I1' 

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

800 

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

802 'D_PMB_I1', 'D_LNPWV_I1', 'D_O3_I1', 'D_LNTAU_I1', 'D_ALPHA_I1', 

803 'D_SECZENITH_I1'] 

804 for derivType in derivTypes: 

805 rec = lutCat.addNew() 

806 rec['luttype'] = derivType 

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

808 

809 return lutCat