Coverage for python/lsst/fgcmcal/fgcmMakeLut.py: 23%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

268 statements  

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 

40import lsst.pex.config as pexConfig 

41import lsst.pipe.base as pipeBase 

42from lsst.pipe.base import connectionTypes 

43import lsst.afw.table as afwTable 

44import lsst.afw.cameraGeom as afwCameraGeom 

45from lsst.utils.timer import timeMethod 

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 @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 opticsDataRef = butlerQC.get(inputRefs.transmission_optics) 

410 

411 sensorRefs = butlerQC.get(inputRefs.transmission_sensor) 

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

413 sensorRef in sensorRefs} 

414 

415 filterRefs = butlerQC.get(inputRefs.transmission_filter) 

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

417 filterRef in filterRefs} 

418 

419 lutCat = self._fgcmMakeLut(camera, 

420 opticsDataRef, 

421 sensorDataRefDict, 

422 filterDataRefDict) 

423 butlerQC.put(lutCat, outputRefs.fgcmLookUpTable) 

424 

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

426 filterDataRefDict): 

427 """ 

428 Make a FGCM Look-up Table 

429 

430 Parameters 

431 ---------- 

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

433 Camera from the butler. 

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

435 `lsst.daf.butler.DeferredDatasetHandle` 

436 Reference to optics transmission curve. 

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

438 `lsst.daf.butler.DeferredDatasetHandle`] 

439 Dictionary of references to sensor transmission curves. Key will 

440 be detector id. 

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

442 `lsst.daf.butler.DeferredDatasetHandle`] 

443 Dictionary of references to filter transmission curves. Key will 

444 be physical filter label. 

445 

446 Returns 

447 ------- 

448 fgcmLookUpTable : `BaseCatalog` 

449 The FGCM look-up table. 

450 """ 

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

452 nCcd = len(camera) 

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

454 

455 # Load in optics, etc. 

456 self._loadThroughputs(camera, 

457 opticsDataRef, 

458 sensorDataRefDict, 

459 filterDataRefDict) 

460 

461 lutConfig = self._createLutConfig(nCcd) 

462 

463 # make the lut object 

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

465 self.fgcmLutMaker = fgcm.FgcmLUTMaker(lutConfig) 

466 

467 # generate the throughput dictionary. 

468 

469 # these will be in Angstroms 

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

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

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

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

474 self.fgcmLutMaker.lambdaStep*10.) 

475 

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

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

478 throughputLambda[1] - throughputLambda[0])) 

479 

480 throughputDict = {} 

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

482 tDict = {} 

483 tDict['LAMBDA'] = throughputLambda 

484 for ccdIndex, detector in enumerate(camera): 

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

486 throughputDict[physicalFilter] = tDict 

487 

488 # set the throughputs 

489 self.fgcmLutMaker.setThroughputs(throughputDict) 

490 

491 # make the LUT 

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

493 self.fgcmLutMaker.makeLUT() 

494 

495 # and save the LUT 

496 

497 # build the index values 

498 comma = ',' 

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

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

501 

502 atmosphereTableName = 'NoTableWasUsed' 

503 if self.config.atmosphereTableName is not None: 

504 atmosphereTableName = self.config.atmosphereTableName 

505 

506 lutSchema = self._makeLutSchema(physicalFilterString, stdPhysicalFilterString, 

507 atmosphereTableName) 

508 

509 lutCat = self._makeLutCat(lutSchema, physicalFilterString, 

510 stdPhysicalFilterString, atmosphereTableName) 

511 return lutCat 

512 

513 def _getStdPhysicalFilterList(self): 

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

515 and config.stdPhysicalFilterOverrideMap 

516 

517 Returns 

518 ------- 

519 stdPhysicalFilters : `list` 

520 """ 

521 override = self.config.stdPhysicalFilterOverrideMap 

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

523 physicalFilter in self.config.physicalFilters] 

524 

525 def _createLutConfig(self, nCcd): 

526 """ 

527 Create the fgcmLut config dictionary 

528 

529 Parameters 

530 ---------- 

531 nCcd: `int` 

532 Number of CCDs in the camera 

533 """ 

534 

535 # create the common stub of the lutConfig 

536 lutConfig = {} 

537 lutConfig['logger'] = self.log 

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

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

540 lutConfig['nCCD'] = nCcd 

541 

542 # atmosphereTable already validated if available 

543 if self.config.atmosphereTableName is not None: 

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

545 else: 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

569 

570 return lutConfig 

571 

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

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

574 

575 Parameters 

576 ---------- 

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

578 Camera from the butler 

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

580 `lsst.daf.butler.DeferredDatasetHandle` 

581 Reference to optics transmission curve. 

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

583 `lsst.daf.butler.DeferredDatasetHandle`] 

584 Dictionary of references to sensor transmission curves. Key will 

585 be detector id. 

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

587 `lsst.daf.butler.DeferredDatasetHandle`] 

588 Dictionary of references to filter transmission curves. Key will 

589 be physical filter label. 

590 

591 Raises 

592 ------ 

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

594 available filter transmission curves. 

595 """ 

596 self._opticsTransmission = opticsDataRef.get() 

597 

598 self._sensorsTransmission = {} 

599 for detector in camera: 

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

601 

602 self._filtersTransmission = {} 

603 for physicalFilter in self.config.physicalFilters: 

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

605 

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

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

608 

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

610 

611 Parameters 

612 ---------- 

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

614 Detector on camera 

615 physicalFilter: `str` 

616 Physical filter label 

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

618 Wavelength steps (Angstrom) 

619 

620 Returns 

621 ------- 

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

623 Throughput (max 1.0) at throughputLambda 

624 """ 

625 

626 c = detector.getCenter(afwCameraGeom.FOCAL_PLANE) 

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

628 

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

630 wavelengths=throughputLambda) 

631 

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

633 wavelengths=throughputLambda) 

634 

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

636 wavelengths=throughputLambda) 

637 

638 # Clip the throughput from 0 to 1 

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

640 

641 return throughput 

642 

643 def _makeLutSchema(self, physicalFilterString, stdPhysicalFilterString, 

644 atmosphereTableName): 

645 """ 

646 Make the LUT schema 

647 

648 Parameters 

649 ---------- 

650 physicalFilterString: `str` 

651 Combined string of all the physicalFilters 

652 stdPhysicalFilterString: `str` 

653 Combined string of all the standard physicalFilters 

654 atmosphereTableName: `str` 

655 Name of the atmosphere table used to generate LUT 

656 

657 Returns 

658 ------- 

659 lutSchema: `afwTable.schema` 

660 """ 

661 

662 lutSchema = afwTable.Schema() 

663 

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

665 size=len(atmosphereTableName)) 

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

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

668 size=len(physicalFilterString)) 

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

670 size=len(stdPhysicalFilterString)) 

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

672 size=self.fgcmLutMaker.pmb.size) 

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

674 size=self.fgcmLutMaker.pmb.size) 

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

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

677 size=self.fgcmLutMaker.pwv.size) 

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

679 size=self.fgcmLutMaker.o3.size) 

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

681 size=self.fgcmLutMaker.tau.size) 

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

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

684 size=self.fgcmLutMaker.alpha.size) 

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

686 size=self.fgcmLutMaker.zenith.size) 

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

688 

689 # and the standard values 

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

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

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

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

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

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

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

697 size=2) 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

714 size=self.fgcmLutMaker.atmLambda.size) 

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

716 size=self.fgcmLutMaker.atmStdTrans.size) 

717 

718 # and the look-up-tables 

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

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

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

722 

723 return lutSchema 

724 

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

726 atmosphereTableName): 

727 """ 

728 Make the LUT schema 

729 

730 Parameters 

731 ---------- 

732 lutSchema: `afwTable.schema` 

733 Lut catalog schema 

734 physicalFilterString: `str` 

735 Combined string of all the physicalFilters 

736 stdPhysicalFilterString: `str` 

737 Combined string of all the standard physicalFilters 

738 atmosphereTableName: `str` 

739 Name of the atmosphere table used to generate LUT 

740 

741 Returns 

742 ------- 

743 lutCat: `afwTable.BaseCatalog` 

744 Lut catalog for persistence 

745 """ 

746 

747 # The somewhat strange format is to make sure that 

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

749 # (see DM-11419) 

750 

751 lutCat = afwTable.BaseCatalog(lutSchema) 

752 lutCat.table.preallocate(14) 

753 

754 # first fill the first index 

755 rec = lutCat.addNew() 

756 

757 rec['tablename'] = atmosphereTableName 

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

759 rec['physicalFilters'] = physicalFilterString 

760 rec['stdPhysicalFilters'] = stdPhysicalFilterString 

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

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

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

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

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

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

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

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

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

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

771 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

789 

790 rec['luttype'] = 'I0' 

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

792 

793 # and add the rest 

794 rec = lutCat.addNew() 

795 rec['luttype'] = 'I1' 

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

797 

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

799 'D_PMB_I1', 'D_LNPWV_I1', 'D_O3_I1', 'D_LNTAU_I1', 'D_ALPHA_I1', 

800 'D_SECZENITH_I1'] 

801 for derivType in derivTypes: 

802 rec = lutCat.addNew() 

803 rec['luttype'] = derivType 

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

805 

806 return lutCat